Using Prismic with Next.js

This article discusses how to use Prismic with Next.js, an exciting React framework that allows for Server Side Rendering (SSR). This is the biggest difference it presents from React, since most web apps built with React work with Client Side Rendering (CSR). This approach allows to render pages faster for end users, as well as fetch data from varied sources with little difficulty. Prismic is of course one of these possible data sources. We will go through considerations to keep in mind when working with Prismic and Next.js, as well as using the powerful (and trendy!) serverless mode to produce efficient and lighting-fast web applications.

Fetching data to build pages

In order to query the data you require from your Prismic repository, Next provides the getInitialProps method. This asynchronous static method allows the application to perform a query, return the data object and store it as props for the component to use. This method can only be called from files located in the /pages folder, which will be used to generate all of the different pages that make up the website. This is of course quite different from how React's lifecycle methods work, but this approach ends up providing a more streamlined development experience.

Here's an example of a simple query for a single document of type 'homepage'

Copy
import React from 'react'
import Prismic from 'prismic-javascript'
import { RichText } from 'prismic-reactjs'
import { apiEndpoint } from '../prismic-configuration'

export default class extends React.Component {
  static async getInitialProps(context) {
    const req = context.req
    const home = await this.getHomePage(req)
    return {
      doc: home
    }
  }

  static async getHomePage (req) {
    const API = await Prismic.getApi(apiEndpoint, { req })
    return await API.getSingle('homepage')
  }

  // Render method ...
}

Rendering the data you have fetched is straightforward and unchanged from React. You just have to access the props defined by getInitialProps

Copy
render () {
  return (
    <div>
      <h1>{RichText.asText(this.props.doc.data.title)}</h1>
      <h3>{RichText.asText(this.props.doc.data.description)}</h3>
    </div>
  );
}

It is highly recommended to use Next's Link component for all links inside your web application, which will allow for client-side routing, giving a far faster and consistent navigation experience for users without requiring a full reload of all of the site's assets when switching pages.

In order to build <Link> components correctly, it's required to pass two attributes: as which will determine how the link's url is shown in the browser, and href which is the actual link in the format required by Next to know both where to route and what page to render, in the form of the path inside the pages folder plus the query string which will contain the slug/UID. href usually takes the form of page?uid=my-page, but given that this will change depending on the document type you're trying to link to, it is a good idea to simplify things with an hrefResolver. This helper function works in a similar way to linkResolver in that depending on the document's type it will generate a different string.

Here's an example that will be defined as a function in prismic-configuration.js

Copy
hrefResolver: function(doc) {
  if (doc.type === 'post') {
    return `/post?uid=${doc.uid}`
  }
  return '/'
}

With /pages/post.js being the file that will take care of rendering pages for documents of type 'post'.

So building a Link to an internal document in your application would look like

Copy
<Link as={linkResolver(post)} href={hrefResolver(post)} passHref>
  Click here to go read more!
</Link>

Take care of not confusing this Link component with Prismic's own Link component which is part of the prismic-reactjs package. For this reason, always use an alias when importing it

Copy
import { RichText, Date, Link } from 'prismic-reactjs'
import { default as NextLink } from 'next/link'
import { linkResolver, hrefResolver } from '../prismic-configuration'

// ...

render () {
  const link = this.props.doc.data.link
  return (
    <NextLink as={linkResolver(link)} href={hrefResolver(link} passHref>
      Click here
    </NextLink>
  )
}

You may have noticed that we use the familiar linkResolver to render the path string for the as attribute. Since this is the aesthetic front-facing path that will be shown, it's important that it's presented in a proper way, so we use the link resolving function to render the expected url.

HTML Serializer

Content writers may add links to internal documents as part of Rich Text fields. By default, this will be simple <a href> elements which does not make for an optimal, consistent navigation experience. This is why it's important to provide an automated method for these user-created links to be handled with client-side routing as well, without the need of reloading the web application.

The best approach to do this is setting up an HTML serializer helper function that will modify the <a href> elements when rendering Rich Text. However, instead of outright replacing them for <Link> elements, it's better to modify their onClick behavior so that they perform an imperative router push, effectively replicating the behavior of Next's Link component. The reason for this roundabout approach is that replacing <a> directly for <Link> elements won't trigger the same associated behavior.

Below you can find an example HTML Serializer function that will handle both regular links and links added to images for internal documents.

Copy
import React from 'react'
import { RichText } from 'prismic-reactjs'
import { linkResolver, hrefResolver } from 'prismic-configuration'
import Router from 'next/router'

const Elements = RichText.Elements
const onClickHandler = function (href, as) {
  // Handler that will do routing imperatively on internal links
  return e => {
    e.preventDefault()
    Router.push(href, as)
  }
}

const propsWithUniqueKey = function (props, key) {
  return Object.assign(props || {}, { key })
}

export const htmlSerializer = function (type, element, content, children, key) {
  var props = {}
  switch (type) {
    case Elements.hyperlink: // Link
      if (element.data.link_type === 'Document') {
        // Only for internal links add the new onClick that will imperatively route to the appropiate page
        props = Object.assign({
          onClick: onClickHandler(hrefResolver(element.data), linkResolver(element.data)),
          href: linkResolver(element.data)
        })
        return React.createElement('a', propsWithUniqueKey(props, key), children)
      } else {
        // Default link handling
        const targetAttr = element.data.target ? { target: element.data.target } : {}
        const relAttr = element.data.target ? { rel: 'noopener' } : {}
        props = Object.assign({
          href: element.data.url || linkResolver(element.data)
        }, targetAttr, relAttr)
        return React.createElement('a', propsWithUniqueKey(props, key), children)
      }

    case Elements.image: // Image
      var props = {}
      var internal = false

      if (element.linkTo && element.linkTo.link_type === 'Document') {
        // Exclusively for internal links, build the object that can be used for router push
        internal = true
        props = Object.assign({
          onClick: onClickHandler(hrefResolver(element.linkTo), linkResolver(element.linkTo)),
          href: linkResolver(element.linkTo)
        })
      }
      // Handle images just like regular HTML Serializer
      const linkUrl = element.linkTo ? element.linkTo.url || linkResolver(element.linkTo) : null
      const linkTarget = (element.linkTo && element.linkTo.target) ? { target: element.linkTo.target } : {}
      const linkRel = linkTarget.target ? { rel: 'noopener' } : {}
      const img = React.createElement('img', { src: element.url, alt: element.alt || '' })
      return React.createElement(
        'p',
        propsWithUniqueKey({ className: [element.label || '', 'block-img'].join(' ') }, key),
        linkUrl ? React.createElement('a',
          // if it's an internal link, replace the onClick
          internal ? propsWithUniqueKey(props, key) : Object.assign({ href: linkUrl },
          linkTarget, linkRel), img) : img
      )

    default:
      return null
  }
}

export default htmlSerializer

You can use this function in Next.js just as with any other framework, pass it as part of the render function for a given rich text field.

Handling dynamic routing

Client-side routing as facilitated by Next.js provides a good navigation experience, and allows you to use custom URLs for pages generated by the app. However, when the user tries to reload the custom URL the browser won't load the page since it lacks a direct route. To handle this, it is necessary to handle routing with custom solutions following the official recommendations.

If the project's goal is to deploy to Now, it's safe to ignore the following method and jump ahead to the next section. If not, then a straightforward way to manage dynamic routing is by implementing a straightforward Express node application that will run server-side and provide for our custom routing needs by redirecting to the appropriate pages while passing along important variables, such as the uid, as part of the query url.

Find below an example server.js with routing rules for blog posts.

Copy
const express = require('express')
const dev = process.env.NODE_ENV !== 'production'
const next = require('next')
const app = next({ dev })
const handle = app.getRequestHandler()

app
  .prepare()
  .then(() => {
    const server = express()

    server.get('/blog/:uid', (req, res) => {
      const nextJsPage = '/post'
      const queryParams = { uid: req.params.uid }
      app.render(req, res, nextJsPage, queryParams)
    })

    server.get('*', (req, res) => handle(req, res))

    server.listen(3000, err => {
      if (err) throw err
      console.log('> Ready http://localhost:3000 <')
    })
  })
  .catch(ex => {
    console.error(ex.stack)
    process.exit(1)
  })

In this example, all urls that match /blog/[uid-of-the-post] will be redirected to /pages/post.js with the UID as part of the query which will be accessible in getInitialProps as part of the context that Next passes through.

However simple this method is to implement, it brings up the issue of requiring a node app to run server side to serve a Next web application that is meant to avoid such requirements. It is therefore recommended to use node server.js only for development environments.

The currently recommended approach for custom routing utilizes the Now platform for serverless deployment of Next applications. This is highly suggested given that the same team is behind both Now and Next and are designed to work at their best when used in tandem.


Deploying to Now as a serverless web application

There are a number of things to consider when configuring your Next.js application for deployment to Now. Start by creating a new next.config.js file in your root folder, which will specify that we're now aiming for a serverless build.

Copy
module.exports = {
  target: 'serverless'
}

As well as a now.json file

Copy
{
  "version": 2,
  "builds": [{ "src": "next.config.js", "use": "@now/next" }]
}

This will be the main configuration file for setting up your Now deployment, starting with the builder it will use, in this case Next. It will also include details for routing and aesthetic considerations like project name. You can review in detail all the different options available.

You will also have to add a now-build script as part of package.json, which will simplify the build process when deploying.

Copy
"scripts": {
  "now-build": "next build",
  (...)
}

Your application will be ready for deploying to the Now platform as a serverless application. Just install Now and run the command.

Copy
$ npm install -g now-cli
$ now

Handling dynamic routing in Now

When deploying to the serverless Now platform you can define a set of routes to follow, avoiding the issues of user's reloading the browser after following a client-side routing link. To achieve this, it's necessary to specify the routes that will be used in the now.json configuration file. These routes are typically a replication of the routing used in the Express server app.

Copy
{
  "version": 2,
  "builds": [{ "src": "next.config.js", "use": "@now/next" }],
  "routes": [
    { "src": "/blog/(?<uid>[^/]+)$", "dest": "/post?uid=$uid" }
  ]
}

You can check that these routes work correctly by using a regular expression tool to build and check your routing before deploying to the Now platform.

Using now dev for development

Zeit provides the now dev command line tool to run a local development environment that replicates a Now deployment. You can use this to check if your routing configuration in now.json works as intended before deploying to the Now platform. It also serves as a great tool to run a development environment that won't depend on maintaining an extra Express.js server application. If you intend to deploy exclusively in Now, you'll be safe ignoring entirely the redundant server application and use only now dev for your development environment.

Handling document previews

In order to prepare your web application to handle previews of both edited and not-yet-published documents, you'll have to prepare for handling preview requests. This requires a /pages/preview.js file that will take care of redirecting a preview request to the appropriate url.

You can find below an example Preview component which will receive the query url from the context object, get the proper url with the help of the link resolver function, and redirect to it.

Copy
import React from 'react'
import Prismic from 'prismic-javascript'
import { apiEndpoint, linkResolver } from 'prismic-configuration'

export default class Preview extends React.Component {
  static async getInitialProps (context) {
    const token = context.query.token
    const { req, res } = context

    const API = await Prismic.getApi(apiEndpoint, { req })
    const url = await API.previewSession(token, linkResolver, '/')

    res.writeHead(302, { Location: url })
    res.end()
    return {}
  }

  render () { return <div>Preview</div> }
}

Additionally, you should add a confirmation check before rendering any document. This way the page will not fail when trying to render drafts of documents.

Copy
render () {
  if (!this.props.post) {
    return <div>404 Error!</div> 
  } else {
    return (
      <div>
        <h1>{RichText.asText(this.props.post.data.title)}</h1>
        <h3>{RichText.asText(this.props.post.data.description)}</h3>
      </div>
    )
  }
}

The toolbar script must be present in all of your rendered pages. You can find the code to include in Prismic settings page, in the Previews section. Creating a layout component and using it to render all of your pages would be the best idea to make sure that the script is included.

Finally, your web application needs to know how to route to this component. For a serverless deployment this will be configured as part of the now.json file

Copy
"routes": [
  { "src": "/preview", "dest": "/preview" },
  // ... Other routes used
]

An equivalent to this routing needs to be configured as part of the Express server file as well, in order to successfully preview documents in your development environment. You should modify the server.js file accordingly.

Copy
// (...)
  server.get('/preview', (req, res) => {
    const token = req.query.token

    Prismic.getApi(apiEndpoint, {req})
      .then((api) => api.previewSession(token, linkResolver, '/'))
      .then((url) => {
        res.redirect(302, url)
      });
  });
  // ... Other routes used

Working example

We provide an example project using Next.js to develop a straightforward personal blog web app. You can review the code for working examples of all the topics touched upon in this article:

https://user-guides.prismic.io/examples/next-js-samples/sample-blog-with-api-based-cms-in-nextjs

Instructions on how to install and get this simple project off the ground can be found in detail, as well as a finished demo.