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.

Note that these instructions require that you're using Next.js version 9 or above.

Create a Prismic config file

The first step you need to do is create a prismic-configuration.js file at the root of your project. Note that you will need to update this file with your Prismic endpoint and optional access token:

Copy
import Prismic from 'prismic-javascript'

export const apiEndpoint = 'https://your-repo-name.cdn.prismic.io/api/v2'
export const accessToken = ''

// Client method to query documents from the Prismic repo
export const Client = (req = null) => (
  Prismic.client(apiEndpoint, createClientOptions(req, accessToken))
)

const createClientOptions = (req = null, prismicAccessToken = null) => {
  const reqOption = req ? { req } : {}
  const accessTokenOption = prismicAccessToken ? { accessToken: prismicAccessToken } : {}
  return {
    ...reqOption,
    ...accessTokenOption,
  }
}

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 { Client } from 'path/to/prismic-configuration'

export default class extends React.Component {
  static async getInitialProps(context) {
    const req = context.req
    const home = await Client(req).getSingle('homepage')
    return {
      doc: home
    }
  }

  // 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>
  );
}

Next's Links for navigation

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. href usually takes the form of page/[uid], 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]'
  }
  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)}>
  <a>Click here to go read more!</a>
</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 { default as NextLink } from 'next/link'
import { linkResolver } from 'path/to/linkResolver'
import { hrefResolver } from 'path/to/hrefResolver'

// ...

render () {
  const link = this.props.doc.data.link
  return (
    <NextLink as={linkResolver(link)} href={hrefResolver(link}>
      <a>Click here</a>
    </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 } from 'path/to/linkResolver'
import { hrefResolver } from 'path/to/hrefResolver'
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

You can refer to the Next.js documentation for how to setup your dynamic routes.

Deploying to Now as a serverless web application

Now makes it super easy to deploy your application! All you need to do is install Now and run the command:

Copy
npm install -g now-cli
now

Handling document previews

There are a few steps you need to perform when configuring previews requests of both published documents and documents that have not yet been published.

Configure the preview route in Prismic

Start with setting up the previews in your Prismic repository. Once you have filled the Site name and Domain for your application, add the following route for the Link Resolver field:

Copy
/api/preview

Your configuration will look something like this:

Adding a new preview

Configure preview mode in your Next app

Then set the Next Preview mode configuration. Create an API directory: pages/api. Inside of this, add two new files:

  • preview.js
  • exit-preview.js (Required file to exit a preview session correctly)

preview.js

The preview configuration uses the NextsJS setPreviewData method which sets some cookies on the browser, turns on the preview mode, and redirects the request to the appropriate URL.

Copy
import Prismic from "prismic-javascript";
import { linkResolver } from 'path/to/linkResolver'

const apiEndpoint = "https://your-repo-name.cdn.prismic.io/api/v2";
const accessToken = "";

// Client method to query from the Prismic repo
const Client = (req = null) =>
  Prismic.client(apiEndpoint, createClientOptions(req, accessToken));

const createClientOptions = (req = null, prismicAccessToken = null) => {
  const reqOption = req ? { req } : {};
  const accessTokenOption = prismicAccessToken ? { accessToken: prismicAccessToken } : {};
  return {
    ...reqOption,
    ...accessTokenOption,
  };
};

const Preview = async (req, res) => {
  const { token: ref, documentId } = req.query;
  const redirectUrl = await Client(req)
    .getPreviewResolver(ref, documentId)
    .resolve(linkResolver, "/");

  if (!redirectUrl) {
    return res.status(401).json({ message: "Invalid token" });
  }

  res.setPreviewData({ ref });
  res.writeHead(302, { Location: `${redirectUrl}`  })
  res.end();
};

export default Preview;

getPreviewResolver

Note that in the code above we are processing the Preview Token with the getPreviewResolver method instead of the deprecated previewSession method.

Make sure that you are using the prismic-javascript kit v3.0.1 and above.

The Link Resolver function

This requires the use of a Link Resolver function so that the preview endpoint knows where to redirect to. You can learn more about link resolving by checking out the Link Resolving page.

You either need to define the Link Resolver in the Preview component or import it.

exit-preview.js

Whenever you have an active preview session and you want to exit you'll need to manually add /api/exit-preview to the URL in the browser. See the examples below:

  • In development mode: http://localhost:xxxx/api/exit-preview
  • In production: http://your-site-name.com/api/exit-preview
Copy
export default async (_, res) => {
  res.clearPreviewData();

  res.writeHead(307, { Location: "/" });
  res.end();
};

Pass the preview ref to all of your pages

Now that the preview cookie provided by the setPreviewData method is available in your project, you just need to read it inside your pages by retrieving the previewData cookie and getting the ref with this line of code: const { ref } = previewData.

Take this example of a simple index.js file:

Copy
import React from 'react'
import { Client } from '../route-to-client-config'

const HomePage = ({ doc }) => {
  if (doc) {
    return (
      <div>
        {RichText.asText(doc.data.title)}
      </div>
    )
  }
  return null
}

export async function getStaticProps({ preview = null, previewData = {} }) {

  const { ref } = previewData

  const client = Client()
  const doc = await client.getSingle('homepage', ref ? { ref } : null) || {}

  return {
    props: {
      doc,
      preview
    }
  }
}

export default HomePage

Exit the preview correctly

At this point, your previews will work. As mentioned earlier, the last thing to note is that whenever you have an active preview session and you want to exit you'll need to manually add /api/exit-preview to the URL in the browser.

The toolbar script must be present in all of your rendered pages. You can find the code to include it in your repository settings page, in the Previews section.

Working examples

We provide example projects using Next.js to develop a straightforward personal Blog web app or a Multi-Page Site with Navigation. You can review the code for working examples of all the topics touched upon in these articles:

Sample Multi-Page Site with Navigation in Next.js

Sample Blog with API-based CMS in Next.js

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