⚔️ Code Conqueror

💼 Next.js Content Management System (CMS)

Nov 11, 2019

We detail a general approach for connecting a Next.js application to a CMS that makes use of the stale-while-revalidate cache setting for near instant updates. Many CMS have guides on how to integrate to Next.js using their client libraries. This can work well depending on the client library, we’ll show a more general approach using the CMS’s API here.

Why use a CMS with Next.js

A Content Management System (CMS) makes it easy for non-technical users to edit the content on a website without having to create a pull request. Next.js has great, established, patterns for consuming data from various CMS and then it handles the rendering of that content. It allows engineers to focus on building the site or application, and writers / designers to focus on creating content. Even for an organization where the engineers are also the authors, often CMS provide a very nice interface for authoring content, as opposed to just editing raw markdown files.

One of the most established CMS out there is Wordpress, but there are a plethora of others like Sanity, Forestry, Prismic and Contentful.

How to integrate a CMS into Next.js

The traditional / standard way

Data fetching in Next.js typically takes place inside of the

getInitialProps
function. If your CMS provides a client library (eg
@sanity/client
) then you can simply start from there, otherwise you'll make a custom call to the API provided by your CMS. Some examples:

// pages/[post].js
// do a nice rendering of the CMS data here
const Post = ({postData, error}) => {
if (error) return <span>{error}</span>
if (!postData) return <span>Loading...</span>
return (
<div>{JSON.stringify(postData)}</div>
)
};
Post.getInitialProps = async ({req, res}) => {
try {
// get the data from the CMS, either with `fetch` or the client library
const response = await fetch('https://api.mycms.com');
const postData = await response.json();
return {postData}
} catch (e) {
return {error: e}
}
}

In this example, we fetch the data from

[api.mycms.com](http://api.mycms.com)
(wherever your data from the cms is, this could also just await some data from a client library). Then we send that data down to the
Post
component and handle the error state.

This is pretty good, but will require server rendering the page on each request so the performance will be much slower than a static site. We do have some options to get around this though by setting good caching policies.

The bleeding edge - SWR (stale while revalidate)

Zeit has introduced a new package SWR which promises to be a great way to hook up your CMS to a React application. The problem that it solves is "how do you render a page very fast when the backend data might be changing". The idea is to use a new caching directive

stale-while-revalidate
which says: serve a stale result, but simultaneously validate whether the underlying resource has been updated or not. To understand how to use SWR let's look back at our simple example and refactor it a bit with the new header:

// pages/[post].js
// do a nice rendering of the CMS data here
const Post = ({postData, error}) => {
if (error) return <span>{error}</span>
if (!postData) return <span>Loading...</span>
return (
<div>{JSON.stringify(postData)}</div>
)
};
Post.getInitialProps = async ({req, res}) => {
try {
// get the data from the CMS, either with `fetch` or the client library
const response = await fetch('https://api.mycms.com');
const postData = await response.json();
const etag = createHash('md5')
.update(JSON.stringify(data))
.digest('hex');
if (res) {
res.setHeader('Cache-Control', 's-maxage=1, stale-while-revalidate');
res.setHeader('X-version', etag);
}
return {postData, etag}
} catch (e) {
return {error: e}
}
}

This will take the content from our CMS and create a new content hash that we can check to see if the results are still the same during revalidation. The key to making the app responsive to the updates is to add some hooks to the page component to eg. check for updates when you focus the page (or poll at some interval etc.).

// pages/[post].js
// a hook to check when our page becomes focused / unfocused
const useFocus = () => {
const [state, setState] = React.useState(null);
const onFocusEvent = () => {
setState(true);
};
const onBlurEvent = () => {
setState(false);
};
React.useEffect(() => {
window.addEventListener('focus', onFocusEvent);
window.addEventListener('blur', onBlurEvent);
return () => {
window.removeEventListener('focus', onFocusEvent);
window.removeEventListener('blur', onBlurEvent);
};
});
return state;
};
// a hook to reload the page if a new fetch of the page has a different etag
const useServerSWR = etag => {
const focused = useFocus();
React.useEffect(() => {
if (focused) {
fetch(window.location, {
headers: {
pragma: 'no-cache'
}
}).then(res => {
if (res.ok && res.headers.get('x-version') !== etag) {
window.location.reload();
}
});
}
}, [etag, focused]);
};
const Post = ({postData, etag, error}) => {
useServerSWR(etag);
if (error) return <span>{error}</span>
if (!postData) return <span>Loading...</span>
return (
<div>{JSON.stringify(postData)}</div>
)
};
Post.getInitialProps ...

This logic is quite reusable and is the crux of what the SWR package is doing. However, at present SWR does not implement server-side-rendering (presumably because of the extra setup in

getInitialProps
that is a bit less simple). The SWR implementation (without SSR) looks like:

const Post = () => {
const { data, error } = useSWR('https://api.mycms.com', fetch)
if (error) return <span>{error}</span>
if (!data) return <span>Loading...</span>
return (
<div>{JSON.stringify(data)}</div>
)
}
export default Post

The team is working on the correct server side rendering implementation but for now this is 👌.

Recommended Content Management System for Next.js

This site uses Notion.so as a CMS to power blog posts. It is however not recommended since the Notion API is not yet public so does not have good stability guarantees.

We'll go through a few of the pros / cons of various CMS that you might consider using with a modern web stack. For each, we'll review 4 categories: the editor, the API, content organization, and integrations score on a 5 point scale.

Prismic

Editor (3):

  • Nothing super objectionable but lacks a lot of the hotkeys / interactive features of a real authoring app like Notion, Dropbox paper etc.

API (5):

  • Can use either REST or GraphQL (nice!), there are also custom client libraries for various languages / frameworks.
  • makes it very easy to get data out from your custom types.
  • This is where Prismic shines.
  • Easily SWR compatible

Content organization (3):

  • Basically you define content types which are collections of various widgets (eg image, text body, heading etc). Then you can create instances of those custom types.
  • Writing drafts / publishing is nice
  • Feels like you might get bottlenecked on some organization in a larger team.

Integrations (2):

  • Not too much apart from the client libraries
  • unclear on data portability (since it's so custom instead of eg markdown).

Bonus:

  • Has a great Youtube Channel with interviews of a lost of industry thought leaders.

Sanity.io

Editor (3):

  • Similar to Prismic, just a basic formatted text input

API (3):

  • Custom query language 😢
  • Graphql in beta

Content organization (2):

  • More limited content types than Prismic / less flexibility

Integrations (4):

  • Great Netlify / Github integration
  • Has generators for Next.js and Gatsby