We recently integrated Prismic with Next.js. And because to us it's a great experience, we'd like to share our approach. Actually you just use the outcome right now as this blog uses Prismic and Next.js for SSR.
Why we chose Prismic
There's lots of headless CMS competition and quite different approaches. Before we decided to use Prismic, we had a look at other popular CMSaaS providers: Contentful, Butter CMS, Scrivito and Strapi to name a few. In the end Prisimc convinced us because of
Great multi-language support
Ease of use
Good preview and publishing functionalities
Nice pricing for small teams.
Specially multi-language support is regularly unsatisfying in other content management systems.
Next.js masters server side rendering for React apps like no other. Because Prismic has good React support and supports universal apps these two match perfectly.
Where to start
A typical example for Prismic is a blog where authors can dynamically add new contents. So we're having a look on how to implement a simple blog engine. Therefore we start by adding a new content type Blog to Prismic having two fields: Title and Body.
Routing
We have to make sure we can resolve Prismic content types to actual paths in our Next app. Prismic encourages us to implement a resolver function. Ours is straight forward for now:
// lib/prismic.js
export const linkResolver = (doc) => {
if (doc.type === 'blog_article') {
return `/article?uid=${doc.uid}`
}
return '/'
}
The basics
Now we add two pages to our next app: blog and article. Let's first have a look at our new article index, called blog:
// pages/blog.js
import React from 'react'
import Link from 'next/link'
import { RichText } from 'prismic-reactjs'
import { fetchBlogPosts, linkResolver } from '../lib/prismic.js'
import withPrismic from '../lib/with-prismic.js'
class BlogIndex extends React.Component {
static async getInitialProps (ctx) {
const posts = await fetchBlogPosts(ctx.prismic)
return { posts }
}
render () {
return (
<ul>
{posts.map(post => (
<li key={post.id}>
<h2>{RichText.asText(post.data.title)}</h2>
<Link href={linkResolver(post.uid)}>
<a>View Article</a>
</Link>
</li>
))}
</ul>
)
}
}
export default withPrismic(BlogIndex)
In Next' static getInitialProps method we query for blog posts from our not yet implemented API adapter for Prismic. getInitialProps is a great location to query for content because it gets resolved either on server on initial page load or on client for subsequent requests. Like this Next ensures great accessibility and index-ability for search engines.
In render we simply iterate over posts, show the headline in plaintext and use Link-Component to refer to an actual article page, passing the article's uid.
For now we ignore details of withPrismicHOC and just assume it enhances Next' context for getInitialProps with our Prismic API client (ctx.prismic) that we inject into our Prismic adapter.
Let's have a look at the article page, that looks quite similar:
// pages/article.js
import React from 'react'
import Error from 'next/error'
import { RichText } from 'prismic-reactjs'
import { fetchBlogPost, linkResolver } from '../lib/prismic.js'
import withPrismic from '../lib/with-prismic.js'
class BlogArticle extends React.Component {
static async getInitialProps (ctx) {
const { uid } = ctx.query
const post = await fetchBlogPost(ctx.prismic, uid)
if (!post && ctx.res) ctx.res.statusCode = 404 // Return 404 if on server no article was found
return { post }
}
render () {
const { post } = this.props
if (!post) {
return <Error statusCode={404} /> // Present adequate 404
}
return (
<article>
<h1>{RichText.render(post.data.title)}</h1>
{RichText.render(post.data.body, linkResolver)}
</article>
)
}
}
export default withPrismic(BlogArticle)
That was easy: we simply fetch the article by uid from our still to be implemented Prismic adapter and render it using the RichText renderer Prismic provides. Again we inject ctx.prismic API client from our withPrismicHOC to our adapter.
That's it. Now the details. You might have wondered how Prismic adapter and client look like. Let's have a look.
Prismic adapter
// lib/prismic.js
import { Predicates } from 'prismic-javascript'
const fetchBlogPosts = async ({ api, ref }) => {
const { results } = await api.query(
Predicates.at('document.type', 'blog_article'), { ref }
)
return results
}
const fetchBlogPost = ({ api, ref }, uid) => {
return api.getByUID('blog_article', uid, { ref })
}
export { fetchBlogPosts, fetchBlogPost }
Here we simply gather logics on how to fetch posts from the Prismic client. You might have a look at Prismic docs for further options. Obviously we expect the adapter to get a Prismic client as first argument. This is what we got from the HOC in getInitialProps context and simply passed to the adapter.
Let's see how that HOC actually works.
withPrismic HOC
// lib/with-prismic.js
import React from 'react'
import Prismic from 'prismic-javascript'
import cookie from 'cookie'
// Universal cookie getter
function parseCookie (req) {
return cookie.parse(
req ? req.headers.cookie || '' : document.cookie
)
}
const prismicApi = new Prismic.Api('YOUR_API_ENDPOINT')
export default Page => {
return class withPrismic extends React.Component {
static displayName = `WithPrismic(${Page.displayName})`
static async getInitialProps (ctx) {
const { req } = ctx
const api = await prismicApi.get()
const previewRef = parseCookies(req)[Prismic.previewCookie]
const ref = previewRef || api.master()
// Enhance Next context
ctx.prismic = { api, ref }
let pageProps = {}
if (Page.getInitialProps) {
pageProps = await Page.getInitialProps(ctx)
}
return { ...pageProps }
}
render () {
return <Page {...this.props} />
}
}
}
Let's dig into this a little deeper. Here we're patching the page's getInitialProps to enhance the original context with our Prismic client that we only initialise once in order to reuse it between requests. Now you might wonder what ref and api.master() are respectively. A ref token is utilized by Prismic to figure out user access. It is used for multiple features, including caching, preview, in-website editing and A/B testing. In this example we only use it for previews. We'll discuss how to save this token in a cookie in a minute. Here we simply fetch it from cookie (identified by the name we get from Prismic.previewCookie) or use api.master(), which on the other hand is the default-ref.
Preview Cookie
Prismic supports previews by calling a defined URL including a query param named token. Prismic supports a method called previewSession that generates the article url from this token. By saving this token in a cookie, we can keep the session alive for a specific amount of time.
// pages/preview.js
import React from 'react'
import Error from 'next/error'
import Router from 'next/router'
import Prismic from 'prismic-javascript'
import cookie from 'cookie'
import { fetchBlogPost, linkResolver } from '../lib/prismic.js'
import withPrismic from '../lib/withPrismic.js'
// Universal cookie setter
function setCookie (res, key, value) {
const options = { maxAge: 60 * 300, path: '/', httpOnly: false }
if (res) {
res.cookie(key, value, options)
} else {
document.cookie = cookie.serialize(key, value, options)
}
}
// Universal redirect
function redirect (ctx, target) {
if (ctx.res) {
// On server
// 303: "See other"
ctx.res.writeHead(303, { Location: target })
ctx.res.end()
} else {
// In the browser, we just pretend this never even happened
Router.replace(target)
}
}
class Preview extends React.Component {
static async getInitialProps (ctx) {
const { token } = ctx.query
const redirectUrl = await ctx.prismic.api.previewSession(token, linkResolver, '/')
setCookie(ctx.res, Prismic.previewCookie, token)
redirect(ctx, redirectUrl)
}
render () {
// This component is supposed to redirect to an actual preview location
// We don't want this to render anything
return <Error statusCode={400} />
}
}
export default withPrismic(Preview)
As you see, this page redirects the user to the actual preview URL and makes sure the session token (later used for ref) gets saved in a cookie.
If you found this article helpful or you want to discuss other approaches, let's connect on twitter or Github!