Code driven CMS powered by GraphQL & React.
- How to use
- Plugins Hooks
- Production deployment
- Browser support
- Contributing
- Authors
Install it:
npm install --save smooth react react-dom
and add a script to your package.json like this:
{
"scripts": {
"dev": "smooth dev",
"build": "smooth build",
"start": "smooth start"
}
}
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:
export default () => <div>Welcome to smooth.js!</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)
Can be used on a GraphQL type definition. Indicates that this type will be available in backoffice.
# A "Book" content will be available in backoffice
type Book @content {
title: String @field
}
# This type will not be available in backoffice
type 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
}
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
}
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" content
type Hero @block {
text: String @field
}
RFC 3339 compliant date. See graphql-iso-date for more information.
RFC 3339 compliant date time. See graphql-iso-date for more information.
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!
}
Represents a media, just a file.
type Media {
title: String
url: String!
}
Represents a link to another page or content.
type Link {
title: String
url: String!
target: String!
}
Additional metadata accessible on contents.
type Metadata {
id: ID!
slug: String!
}
Special type to indicates all defined blocks.
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
:
import gql from 'graphql-tag'
export 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
:
import gql from 'graphql-tag'
export const typeDefs = gql`
type Movie @content {
title: String! @field
likes: Int
}
`
export const resolvers = {
Movie: {
async likes(object, params, { slug }) {
// This is an example of an external call to get movie likes
return api.getMovieLikes(slug)
},
},
}
resolvers
are only required for field definition not marked as@field
.
Sometimes, you may want to be able to request a set of contents. For example, a block that display three movies automatically.
import gql from 'graphql-tag'
export const typeDefs = gql`
type MovieListBlock @block {
# This type is not displayed in backoffice
movies: [Movie!]!
}
`
export const resolvers = {
MovieListBlock: {
// This resolvers automatically display a list of movies
async movies(object, params, { api }) {
// The "type" is the slug of your content
return api.getContents({ type: 'movie' })
},
},
}
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.getContents({
type: 'news',
query: {
per_page: 30,
meta_key: 'publicationDate',
orderby: 'meta_value',
order: 'desc',
},
})
// Get the last 4 projects:
// - ordered by "date"
// - with "hasPage" flag marked as true
// - and exclude the current object
api.getContents({
type: 'projects',
query: {
per_page: 4,
orderby: 'date',
order: 'desc',
filter: {
meta_query: [{ key: 'hasPage', value: 1 }],
},
exclude: [object.id],
},
})
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
export default function Hello() {
return 'Hello world!'
}
When I access to /hello
, I see "Hello world!".
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.
import React from 'react'
import gql from 'graphql-tag'
// The name of the fragment "MovieProps" does not matter
// but it is recommended to named it like that.
export const contentFragment = gql`
fragment MovieProps on Movie {
title
description
cover {
url
alt
}
}
`
export default function Movie({ title, description, cover }) {
return (
<div>
<img src={cover.url} alt={cover.alt} />
<div>{title}</div>
<p>{description</p>
</div>
)
}
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" book
export const contentSlug = 'harry-potter'
You can also specify a function to compute slug from the url.
export const contentSlug = ({ location }) => location.pathname
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
import React from 'react'
import gql from 'graphql-tag'
import { Query } from 'smooth/query'
const PAGE = gql`
query Settings {
settings(slug: "main") {
title
}
}
`
export default function Page({ Component, ...props }) {
return (
<Query query={PAGE}>
{({ data }) =>
data && (
<>
<h1>{data.settings.title}</h1>
<Component {...props} />
</>
)
}
</Query>
)
}
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.
import React from 'react'
import gql from 'graphql-tag'
import { Link } from 'smooth/router'
export const contentFragment = gql`
fragment PageProps on Page {
books {
metadata {
id
slug
}
name
}
}
`
export default function Page({ books }) {
return (
<ul>
{allBooks.map(book => (
<li key={book.metadata.id}>
<Link to={`/books/${book.metadata.slug}`}>{book.name}</Link>
</li>
))}
</ul>
)
}
Every import
you declare gets bundled and served with each page. That means pages never load unnecessary code!
import cowsay from 'cowsay-browser'
export default () => <pre>{cowsay.say({ text: 'hi there!' })}</pre>
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'
export default () => (
<div
css={css`
font: 15px Helvetica, Arial, sans-serif;
background: #eee;
padding: 100px;
text-align: center;
transition: 100ms ease-in background;
&:hover {
background: #ccc;
}
`}
>
<p>Hello World</p>
</div>
)
Please see the emotion documentation for more examples.
Examples
It's possible to use any existing CSS-in-JS solution. The simplest one is inline styles:
export default () => <p style={{ color: 'red' }}>hi there</p>
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.
Create a folder called static
in your project root directory. From your code you can then reference those files with /static/
URLs:
export default () => <img src="/static/my-image.png" alt="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.
Examples
Client-side transitions between routes can be enabled via a <Link>
component. Consider these two pages:
// pages/index$.js
import { Link } from 'smooth/router'
export default () => (
<div>
Click <Link to="/about">About</Link> to read more
</div>
)
// pages/about$.js
export default () => <p>Welcome to About!</p>
Note: "smooth/router" exposes all methods from "react-router-dom".
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.
import loadable from 'smooth/loadable'
const DynamicComponent = loadable(() => import('../components/hello'))
export default () => (
<div>
<Header />
<DynamicComponent />
<p>HOME PAGE is here!</p>
</div>
)
import loadable from 'smooth/loadable'
const DynamicComponentWithCustomLoading = loadable(
() => import('../components/hello2'),
{ fallback: <p>...</p> },
)
export default () => (
<div>
<Header />
<DynamicComponentWithCustomLoading />
<p>HOME PAGE is here!</p>
</div>
)
Note: "smooth/loadable" exposes all methods from "@loadable/component".
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:
import React from 'react'
export default ({ Component, ...props }) => (
<div className="layout">
<Component {...props} />
</div>
)
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:
error.js
component is only used in production
import React from 'react'
export default ({ error }) => (
<p>
{error.statusCode
? `An error ${error.statusCode} occurred on server`
: 'An error occurred on client'}
</p>
)
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.js
module.exports = {
/* config options here */
}
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.
module.exports = {
webpack: (config, { dev, isServer, defaultLoaders }) => {
// Perform customizations to webpack config
// Important: return the modified config
return config
},
webpackDevMiddleware: 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-loader
module.exports = {
webpack: (config, {}) => {
config.module.rules.push({
test: /\.mdx/,
use: [
options.defaultLoaders.babel,
{
loader: '@mdx-js/loader',
options: pluginOptions.options,
},
],
})
return 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:
{
"presets": ["smooth/babel"],
"plugins": []
}
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:
{
"presets": [
[
"smooth/babel",
{
"preset-env": {},
"transform-runtime": {}
}
]
],
"plugins": []
}
The modules
option on "preset-env"
should be kept to false
otherwise webpack code splitting is disabled.
- resolveOptions
- onCreateServer
- onCreateApolloServerConfig
- onRenderBody
- onServerError
- onCreateBabelConfig
- onCreateWebpackConfig
- getContents
- getContent
- onBuild
- wrapRootElement
- onRouteUpdate
- onSelectContentFields
- wrapContentElement
To deploy, instead of running smooth
, you want to build for production usage ahead of time. Therefore, building and starting are separate commands:
smooth build
smooth start
For example, to deploy with now
a package.json
like follows is recommended:
{
"name": "my-app",
"dependencies": {
"smooth": "latest"
},
"scripts": {
"dev": "smooth dev",
"build": "smooth build",
"start": "smooth start"
}
}
Then run now
and enjoy!
Smooth.js supports IE11 and all modern browsers out of the box using @babel/preset-env
.
Please see our contributing.md
- Greg Bergé (@neoziro) – Smooth Code