Code driven CMS powered by GraphQL & React.
- How to use
- Plugins Hooks
- Production deployment
- Browser support
- Contributing
- Authors
How to use
Setup
Install it:
npm install --save smooth react react-dom
and add a script to your package.json like this:
After that, the file-system is the main API. Every .js
file becomes a route that gets automatically processed and rendered.
Populate ./pages/index$.js
inside your project:
<div>Welcome to smoothjs!</div>
and then just run npm run dev
and go to http://localhost:3000
. To use another port, you can run npm run dev -- -p <your port here>
.
So far, we get:
- Automatic transpilation and bundling (with webpack and babel)
- Hot code reloading
- Server rendering and indexing of
./pages
- Static file serving.
./static/
is mapped to/static/
(given you create a./static/
directory inside your project)
Structure
Directives
@content
Can be used on a GraphQL type definition. Indicates that this type will be available in backoffice.
# A "Book" content will be available in backofficetype Book @content { title: String @field} # This type will not be available in backofficetype User { name: String}
Specify icon
Some backoffices like Wordpress supports an icon. @content
supports it as a parameter:
type Book @content(icon: "dashicons-format-gallery") { title: String @field}
Specify slug
Slug is automatically generated from the name but you can also specify a specific one. Slug has an impact in the API and in the backoffice. It is the internal name of the content.
type Book @content(slug: "awesome-book") { title: String @field}
Specify label
Label is automatically generated from the name but you can also specify a specific one. Label has an impact in the backoffice. It is the name used to display the name of the content.
type Book @content(label: "Awesome Book") { title: String @field}
Specify a description
GraphQL comments are automatically converted in description in the backoffice.
"It is just a book, relax."type Book @content { title: String @field}
@field
Can be used on a GraphQL field definition. Indicates that this field will be available in the backoffice. @field
must only be used in a type marked as @content
.
type Book @content { # This field is editable in backoffice title: String @field # This field is not editable in backoffice # you have to write a custom resolver for it likes: Int}
Specify type explicitely
Most of field types are inferred from the GraphQL type of the field. A String
will generate a text input, a Boolean
will display a checkbox, etc... Sometimes you have to precise the exact type of field. For an example, a String
display a shortText
, but you could also want a richText
.
For this specific use-case you can specify a type
argument in the @field
directive:
type Book @content { # Will display a "shortText" in backoffice title: String @field # Will display a "longText" in backoffice description: String @field(type: longText)}
Available types are: shortText
, longText
and richText
, all other types are inferred from the GraphQL types, including special ones like Image
, Link
and Media
.
Specify a label
Exactly like for @content
you may want to display a custom label for this field. The label is used only in the backoffice.
type Book @content { title: String @field(label: "My awesome title")}
Specify a description
GraphQL comments are automatically converted in description in the backoffice.
type Book @content { "The title of the book, yeah the big title!" title: String @field}
@block
Can be used on a GraphQL type definition. Indicates that this type will be available in the special Block
type.
type Page @content { # The special type "Block" indicates that all blocks will be available in backoffice blocks: [Block] @field} # This type will be available as a block in the "Page" contenttype Hero @block { text: String @field}
Additional GraphQL types
Date
RFC 3339 compliant date. See graphql-iso-date for more information.
DateTime
RFC 3339 compliant date time. See graphql-iso-date for more information.
Image
Represents an image, with several pre-configured sizes.
type ImageSize { width: Int! height: Int! url: String!} type Image { id: ID! url: String! mimeType: String! alt: String title: String thumbnail: ImageSize! medium: ImageSize! large: ImageSize!}
Media
Represents a media, just a file.
type Media { title: String url: String!}
Link
Represents a link to another page or content.
type Link { title: String url: String! target: String!}
Metadata
Additional metadata accessible on contents.
type Metadata { id: ID! slug: String!}
Block
Special type to indicates all defined blocks.
Schemas
All your GraphQL schemas must be placed in src/schemas
, you can place them in separated files or in a single files. The only requirement is to place them in src/schemas
.
You must specify your type definition in a named export typeDefs
:
const typeDefs = gql` type Actor { name: String! @field } type Movie @content { title: String! @field description: String! @field(type: richText) cover: Image! @field actors: [Actor!]! @field }`
And you can specify resolvers in a named export resolvers
:
const typeDefs = gql` type Movie @content { title: String! @field likes: Int }` const resolvers = Movie: async { // This is an example of an external call to get movie likes return api }
resolvers
are only required for field definition not marked as@field
.
Content API
Sometimes, you may want to be able to request a set of contents. For example, a block that display three movies automatically.
const typeDefs = gql` type MovieListBlock @block { # This type is not displayed in backoffice movies: [Movie!]! }`const resolvers = MovieListBlock: // This resolvers automatically display a list of movies async { // The "type" is the slug of your content return api }
Filters
Available filters depends of your backoffice, using Wordpress
we rely on wp-rest-filter. To use it, add a query
to api.getContents
:
// Get last 30 news ordered by "publicationDate"api // Get the last 4 projects:// - ordered by "date"// - with "hasPage" flag marked as true// - and exclude the current objectapi
Pages
All your pages must be placed in src/pages
. The name of the page determines the route of the page. Pages offers several possibilities, display a content or create a page from scratch.
All pages must have a default export that represents the component used to display the page.
// src/pages/hello.js { return 'Hello world!'}
When I access to /hello
, I see "Hello world!".
Content Pages
A content page is a page with a named export contentFragment
that contains a fragment on a GraphQL type marked as @content
. And of course a component as the default export. All fields specified in fragments are available as props.
// The name of the fragment "MovieProps" does not matter// but it is recommended to named it like that.const contentFragment = gql` fragment MovieProps on Movie { title description cover { url alt } }` { return <div> <img src=coverurl alt=coveralt /> <div>title</div> <p>description</p> </div> )
Fixed slug pages
All pages are "wildcard" pages by default. It means that the page matches for all urls. For example, if I create a page books.js
. The page will matches for URL "/books/foo/bar". In fact if this page has a content, it will look for a content with the slug "foo/bar". Most of time this is the correct behaviour, but sometimes you may want to be able to control it and to create a dedicated page.
To create a dedicated page, not wildcard, you have to add a $
at the end of the name. Let's take the same page books$.js
. The page will matches only URL "/books/foo/bar" but it will only look for the content with the slug "books".
Customize slug
To change the slug looked for by a fixed slug page, you can use contentSlug
variable. For example, a page named best-book$.js
will look for best-book
by default. But you can customize it.
// The page is still accessible under "/best-book", but it will look for "harry-potter" bookconst contentSlug = 'harry-potter'
You can also specify a function to compute slug from the url.
const contentSlug = locationpathname
Query
You can write your GraphQL queries using the Query
component. For example, in _app.js
you can choose to display the settings.
// src/_app.js const PAGE = gql` query Settings { settings(slug: "main") { title } }` { return <Query query=PAGE> data && <> <h1>datasettingstitle</h1> <Component ...props /> </> </Query> }
Content
Link to another content
To link a content, you have to know two things: the slug and the name of the content page.
You can find the content slug in metadata and the name of the content page is just the name of the page file.
The Link
component take care of the language for you, you can safely use it to create a link to another page.
const contentFragment = gql` fragment PageProps on Page { books { metadata { id slug } name } }` { return <ul> allBooks </ul> }
Automatic code splitting
Every import
you declare gets bundled and served with each page. That means pages never load unnecessary code!
import cowsay from 'cowsay-browser' <pre>cowsay</pre>
CSS
Built-in CSS support
Smooth.js doesn't provides a built-in CSS in JS solution. But emotion is the most easy solution, because it doesn't require any SSR configuration. You can install it and use in the project.
Examples
/** @jsx jsx */import jsx css from '@emotion/core' <div = > <p>Hello World</p> </div>
Please see the emotion documentation for more examples.
CSS-in-JS
Examples
It's possible to use any existing CSS-in-JS solution. The simplest one is inline styles:
<p =>hi there</p>
Importing CSS / Sass / Less / Stylus files
Importing .css
, .scss
, .less
or .styl
files is not yet supported. You can probably achieve it by adding some webpack configuration but it is not recommended. CSS in JS is much more powerful with SSR rendering.
Static file serving (e.g.: images)
Create a folder called static
in your project root directory. From your code you can then reference those files with /static/
URLs:
<img ="/static/my-image.png" ="my image" />
Note: Don't name the static
directory anything else. The name is required and is the only directory that Smooth.js uses for serving static assets.
Routing
Examples
Client-side transitions between routes can be enabled via a <Link>
component. Consider these two pages:
// pages/index$.jsimport Link from 'smooth/router' <div> Click <Link ="/about">About</Link> to read more </div>
// pages/about$.js <p>Welcome to About!</p>
Note: "smooth/router" exposes all methods from "react-router-dom".
Dynamic Import
Examples
Smooth.js supports TC39 dynamic import proposal for JavaScript. With that, you could import JavaScript modules (inc. React Components) dynamically and work with them.
You can think dynamic imports as another way to split your code into manageable chunks. Since Smooth.js supports dynamic imports with SSR, you could do amazing things with it.
Here are a few ways to use dynamic imports.
1. Basic Usage (Also does SSR)
import loadable from 'smooth/loadable' const DynamicComponent = <div> <Header /> <DynamicComponent /> <p>HOME PAGE is here!</p> </div>
2. With Custom Loading Component
import loadable from 'smooth/loadable' const DynamicComponentWithCustomLoading = <div> <Header /> <DynamicComponentWithCustomLoading /> <p>HOME PAGE is here!</p> </div>
Note: "smooth/loadable" exposes all methods from "@loadable/component".
<App>
Custom Examples
Smooth.js uses the App
component to initialize pages. You can override it and control the page initialization. Which allows you to do amazing things like:
- Persisting layout between page changes
- Keeping state when navigating pages
To override, create the ./src/_app.js
file and override the App class as shown below:
<div className="layout"> <Component ...props /> </div>
Custom error handling
404 or 500 errors are handled both client and server side by a default component error.js
. If you wish to override it, define a _error.js
in the src folder:
⚠️ The default error.js
component is only used in production ⚠️
import React from 'react' error <p> errorstatusCode ? `An error occurred on server` : 'An error occurred on client' </p>
Custom configuration
For custom advanced behavior of Smooth.js, you can create a smooth.config.js
in the root of your project directory (next to src/
and package.json
).
Note: smooth.config.js
is a regular Node.js module, not a JSON file. It gets used by the Smooth server and build phases, and not included in the browser build.
// smooth.config.jsmoduleexports = /* config options here */
Customizing webpack config
Examples
In order to extend our usage of webpack
, you can define a function that extends its config via smooth.config.js
.
// smooth.config.js is not transformed by Babel. So you can only use javascript features supported by your version of Node.js. moduleexports = { // Perform customizations to webpack config // Important: return the modified config return config } { // Perform customizations to webpack dev middleware config // Important: return the modified config return config }
The second argument to webpack
is an object containing properties useful when customizing its configuration:
dev
-Boolean
shows if the compilation is done in development modeisServer
-Boolean
shows if the resulting configuration will be used for server side (true
), or client size compilation (false
).defaultLoaders
-Object
Holds loader objects Smooth.js uses internally, so that you can use them in custom configurationbabel
-Object
thebabel-loader
configuration for Smooth.js.
Example usage of defaultLoaders.babel
:
// Example smooth.config.js for adding a loader that depends on babel-loadermoduleexports = { configmodulerules return config }
Customizing babel config
Examples
In order to extend our usage of babel
, you can simply define a .babelrc
file at the root of your app. This file is optional.
If found, we're going to consider it the source of truth, therefore it needs to define what smooth needs as well, which is the smooth/babel
preset.
This is designed so that you are not surprised by modifications we could make to the babel configurations.
Here's an example .babelrc
file:
The smooth/babel
preset includes everything needed to transpile React applications. This includes:
- preset-env
- preset-react
- plugin-proposal-class-properties
- @loadable/babel-plugin
These presets / plugins should not be added to your custom .babelrc
. Instead, you can configure them on the smooth/babel
preset:
The modules
option on "preset-env"
should be kept to false
otherwise webpack code splitting is disabled.
Plugins Hooks
Plugins API
- resolveOptions
smooth-node.js
- onCreateServer
- onCreateApolloServerConfig
- onRenderBody
- onServerError
- onCreateBabelConfig
- onCreateWebpackConfig
- getContents
- getContent
- onBuild
- wrapRootElement
smooth-browser.js
- onRouteUpdate
- onSelectContentFields
- wrapContentElement
Production deployment
To deploy, instead of running smooth
, you want to build for production usage ahead of time. Therefore, building and starting are separate commands:
smooth buildsmooth start
For example, to deploy with now
a package.json
like follows is recommended:
Then run now
and enjoy!
Browser support
Smooth.js supports IE11 and all modern browsers out of the box using @babel/preset-env
.
Contributing
Please see our contributing.md
Authors
- Greg Bergé (@neoziro) – Smooth Code