August 26, 2021

Live Preview with Next.js and Sanity.io: A Complete Guide

By Simeon Griggs & Knut Melvær

It's super useful to see your content changes in context before they go live. Using Sanity's SDK for Next.js you can set up real-time content previews for your logged-in content creators. It works without a preview server, only using a serverless function to activate it.

This tutorial will walk you through how to set up live preview for content creators in your Next.js web application. By using the next-sanity toolkit, you'll be able to offer a client-side preview that leverages the same authentication that you have for Sanity Studio.

The only server(less) component you have to set up is a dedicated route to activate the preview.

Sanity.io and Next.js work so well together we have released a set of helpful utilities to pair the two platforms even more seamlessly.

One of the best features is the usePreviewSubscription() hook. Since it's used to update the properties in your page templates, it requires some careful setup. But don't worry, we'll take you through it step-by-step!

Protip

The final code you will assemble is available as a public repository on GitHub. Use it to start, or as a reference while you walk through the guide.

Requirements

To follow this tutorial you will need:

  • Basic knowledge of JavaScript, React, and Next.js (we have a comprehensive video tutorial to help get you started)
  • Node.js installed, as well as the Sanity CLI
  • Your Next.js web app will need to use GROQ for fetching data from your Sanity.io project

How it works

Next.js comes with a Preview Mode which sets a browser cookie. When set, we can use this to activate the preview functionality in the usePreviewSubscription() hook. So long as your browser is also authenticated (logged into your Sanity Studio) it will continually replace published content with the latest draft content and rerender the page you're looking at. It will even let you change referenced data and have that updated as well.

You'll set up a link in your Sanity Studio to a Next.js API Route, which will set the Preview Mode cookie and show the latest version of your content in your Next.js web app, as you edit it.

If you are not authenticated, the same route will show latest draft content on page load, but will not keep updating as changes are made unless they go through the same link again. This can be a useful link to share with content reviewers.

Installing Sanity.io and Next.js

If you have an existing Sanity + Next.js studio, skip this step

You can use this guide to add Live Preview to an existing Next.js website, but we will focus on an entirely new project.

Let's start a new project, in a folder my-project

Create a new folder, in it create a studio folder and run sanity init inside to start a new Sanity project.

Back in the my-project folder, run npx create-next-app and give it the name web. You should now have a folder structure like this:

/my-project
- /studio
- /web

In separate terminal windows you can now run:

  • sanity start from the studio folder, and view the studio at http://localhost:3333
  • npm run dev from the web folder, and view the frontend at http://localhost:3000

Setting up schemas

If you already have a document type with slugs, skip this step

If you've started from a completely blank dataset, we'll need content schemas to have something to view and edit.

Let's create the most basic document schema:

// ./studio/schemas/article.js

export default {
  name: 'article',
  title: 'Article',
  type: 'document',
  fields: [
    {name: 'title', type: 'string'},
    // Important! Document needs a slug for Next.js to query for.
    {name: 'slug', type: 'slug', options: {source: 'title'}},
    {name: 'content', type: 'text'},
  ],
}

And register it to the studio:

// ./studio/schemas/schema.js

import createSchema from 'part:@sanity/base/schema-creator'
import schemaTypes from 'all:part:@sanity/base/schema-type'
import article from './article'

export default createSchema({
  name: 'default',
  types: schemaTypes.concat([article]),
})
Your Sanity Studio should look like this, with one Schema to create Documents in.

Setting up next/sanity

In the web folder, install the next/sanity utilities...

npm install next-sanity

...and follow the guide in the Readme. Come back once you have these files setup:

./web/lib/
- config.js
- sanity.js
- sanity.server.js

And with the correct values in an .env.local file for the Sanity Project ID, Dataset, and an API token. The file should look something like this, and you'll need to restart your dev environment to load them.

// ./web/.env.local

// Find these in sanity.json
NEXT_PUBLIC_SANITY_DATASET='production'
NEXT_PUBLIC_SANITY_PROJECT_ID='...'

// Create this in sanity.io/manage
SANITY_API_TOKEN='...'

// Blank for now, we'll get back to this!
SANITY_PREVIEW_SECRET='' 

Tokens and CORS

You'll need to create an API token in sanity.io/manage and allow CORS from your Next.js website.

  1. Create a "Read" token, and save it to SANITY_API_TOKEN in .env.local
  2. Add a CORS origin from http://localhost:3000 with Credentials Allowed, so that your front-end can communicate with the Studio.
Generate a Token for Next.js to read draft content when Preview mode is enabled. Don't forget to setup CORS while you're here!.

Gotcha

  1. Don't commit your API tokens to your Git repository.
  2. You'll need to add these API token environment variables to wherever you deploy your website (Vercel, Netlify, etc).
  3. You'll also need to add a CORS origin for each front-end domain that you expect to use preview on (example: my-project.com).

Ready?

Create a page route to display an article

Let's set up a Dynamic Route to display your content.

Note: In the code below, the normal order of functions you'd usually see in a Next.js page has been inverted, so that it reads more easily from top-to-bottom. It still works the same.

Read through the comments below to see exactly what each function is doing:

// ./web/pages/[slug].js

import React from 'react'
import {groq} from 'next-sanity'

import {usePreviewSubscription} from '../lib/sanity'
import {getClient} from '../lib/sanity.server'

/**
 * Helper function to return the correct version of the document
 * If we're in "preview mode" and have multiple documents, return the draft
 */
function filterDataToSingleItem(data, preview) {
  if (!Array.isArray(data)) return data

  return data.length > 1 && preview
    ? data.filter((item) => item._id.startsWith(`drafts.`)).slice(-1)[0]
    : data.slice(-1)[0]
}

/**
 * Makes Next.js aware of all the slugs it can expect at this route
 *
 * See how we've mapped over our found slugs to add a `/` character?
 * Idea: Add these in Sanity and enforce them with validation rules :)
 * https://www.simeongriggs.dev/nextjs-sanity-slug-patterns
 */
export async function getStaticPaths() {
  const allSlugsQuery = groq`*[defined(slug.current)][].slug.current`
  const pages = await getClient().fetch(allSlugsQuery)

  return {
    paths: pages.map((slug) => `/${slug}`),
    fallback: true,
  }
}

/**
 * Fetch the data from Sanity based on the current slug
 *
 * Important: You _could_ query for just one document, like this:
 * *[slug.current == $slug][0]
 * But that won't return a draft document!
 * And you get a better editing experience 
 * fetching draft/preview content server-side
 *
 * Also: Ignore the `preview = false` param!
 * It's set by Next.js "Preview Mode" 
 * It does not need to be set or changed here
 */
export async function getStaticProps({params, preview = false}) {
  const query = groq`*[_type == "article" && slug.current == $slug]`
  const queryParams = {slug: params.slug}
  const data = await getClient(preview).fetch(query, queryParams)

  // Escape hatch, if our query failed to return data
  if (!data) return {notFound: true}

  // Helper function to reduce all returned documents down to just one
  const page = filterDataToSingleItem(data, preview)

  return {
    props: {
      // Pass down the "preview mode" boolean to the client-side
      preview,
      // Pass down the initial content, and our query
      data: {page, query, queryParams}
    }
  }
}

/**
 * The `usePreviewSubscription` takes care of updating
 * the preview content on the client-side
 */
export default function Page({data, preview}) {
  const {data: previewData} = usePreviewSubscription(data?.query, {
    params: data?.queryParams ?? {},
    // The hook will return this on first render
    // This is why it's important to fetch *draft* content server-side!
    initialData: data?.page,
    // The passed-down preview context determines whether this function does anything
    enabled: preview,
  })

  // Client-side uses the same query, so we may need to filter it down again
  const page = filterDataToSingleItem(previewData, preview)

  // Notice the optional?.chaining conditionals wrapping every piece of content? 
  // This is extremely important as you can't ever rely on a single field
  // of data existing when Editors are creating new documents. 
  // It'll be completely blank when they start!
  return (
    <div style={{maxWidth: `20rem`, padding: `1rem`}}>
      {page?.title && <h1>{page.title}</h1>}
      {page?.content && <p>{page.content}</p>}
    </div>
  )
}

Phew! All going well you should now be able to view this page, or whatever slug you created...

 http://localhost:3000/its-preview-time

And it should look like this...

It doesn't look like much, but we're successfully querying data from Sanity and displaying it in Next.js – and that's good!

What have you achieved so far?

  • ✅ You have Sanity Studio up and running
  • ✅ Your Next.js frontend runs
  • ✅ You have queried content from your Sanity.io project in Next.js

Now, let's add Preview Context so you can watch edits happen live!

Preview Mode and API Routes

Next.js has a Preview mode which we can use to set a cookie, which will set the preview mode to true in the above Dynamic Route. Preview mode can be activated by entering the site through an API Route, which will then redirect the visitor to the correct page.

Create two files inside ./web/pages/api

// ./web/pages/api/preview.js

export default function preview(req, res) {
  if (!req?.query?.secret) {
    return res.status(401).json({message: 'No secret token'})
  }

  // Check the secret and next parameters
  // This secret should only be known to this API route and the CMS
  if (req.query.secret !== process.env.SANITY_PREVIEW_SECRET) {
    return res.status(401).json({message: 'Invalid secret token'})
  }

  if (!req.query.slug) {
    return res.status(401).json({message: 'No slug'})
  }

  // Enable Preview Mode by setting the cookies
  res.setPreviewData({})

  // Redirect to the path from the fetched post
  // We don't redirect to req.query.slug as that might lead to open redirect vulnerabilities
  res.writeHead(307, {Location: `/${req?.query?.slug}` ?? `/`})

  return res.end()
}
// ./web/pages/api/exit-preview.js

export default function exit(req, res) {
  res.clearPreviewData()

  res.writeHead(307, {Location: req?.query?.slug ?? `/`})
}

You should now be able to visit the preview route at http://localhost:3000/api/preview and be greeted with an error.

The Preview route requires two params:

  1. A secret which can be any random string known only to your Studio and Next.js
  2. A slug to redirect to once authenticated – that slug is then used to query the document

Protip

Document doesn't have a slug? No problem! Use the document's _id!

But, be aware that the usePreviewSubscription() hook cannot query for drafts. so update your query parameters for the published version of the _id and you'll get draft content.

Setting up the Production Preview plugin for Sanity Studio

Install the Production Preview plugin and follow the docs to get set up. Your sanity.json file should now have the following in parts:

// ./studio/sanity.json

parts: [
  // ...all other parts
  {
    "implements": "part:@sanity/production-preview/resolve-production-url",
    "path": "./resolveProductionUrl.js"
  },

You'll need to create a suitable resolveProductionUrl() function for your specific use case, but to match the demo we've created. so far, this does the trick:

// ./studio/resolveProductionUrl.js

// Any random string, must match SANITY_PREVIEW_SECRET in the Next.js .env.local file
const previewSecret = 'j8heapkqy4rdz6kudrvsc7ywpvfhrv022abyx5zgmuwpc1xv'

// Replace with your deployed studio when you go live
const remoteUrl = `https://your-studio.sanity.studio`
const localUrl = `http://localhost:3000`

export default function resolveProductionUrl(doc) {
  const baseUrl = window.location.hostname === 'localhost' ? localUrl : remoteUrl

  const previewUrl = new URL(baseUrl)

  previewUrl.pathname = `/api/preview`
  previewUrl.searchParams.append(`secret`, previewSecret)
  previewUrl.searchParams.append(`slug`, doc?.slug?.current ?? `/`)

  return previewUrl.toString()
}

So now in the studio, you should have this option in the top right of your document, which will open in a new tab, redirect you to the Next.js you were viewing before.

But this time, Preview mode is enabled!

Find this menu in the top right of your Document

Try making changes in the Studio (without Publishing) and see if they're happening live on the Next.js front end.

Want to make it even more obvious? Drop this into your [slug].js page.

import Link from 'next/link'

// ... everything else

{preview && <Link href="/api/exit-preview">Preview Mode Activated!</Link>}

This will give you a fast way to escape from Preview Mode, if it is enabled. Usually, you'd style this as a floating button on the page.

You could pass along a ?slug= param on the link to make the API route redirect back to the page they were on.

Preview in the Studio

Using the Structure Builder and Custom Document Views, you can even display the preview URL inside the Studio inside an <iframe>.

View Panes allow you to put anything alongside your Document in the Studio

Your sanity.json file will need the following added into parts:

// ./studio/sanity.json

parts: [
  // ...all other parts
  {
    "name": "part:@sanity/desk-tool/structure",
    "path": "./deskStructure.js"
  },

And you'll need a deskStructure.js file set up with some basic layout for article type documents. As well as including a React component to show in the Pane.

There's a handy plugin available to use as a Component. But you could write your own if you prefer.

Notice how we're reusing resolveProductionUrl() again here to take our document and generate a preview URL. This time instead of creating a link, this will load the URL right inside our view pane.

// ./studio/deskStructure.js

import S from '@sanity/desk-tool/structure-builder'
import Iframe from 'sanity-plugin-iframe-pane'

import resolveProductionUrl from './resolveProductionUrl'

export const getDefaultDocumentNode = () => {
  return S.document().views([
    S.view.form(),
    S.view
      .component(Iframe)
      .options({
        url: (doc) => resolveProductionUrl(doc),
      })
      .title('Preview'),
  ])
}

export default () =>
  S.list()
    .title('Content')
    .items([S.documentTypeListItem('article')])

Now, what do you have?

  • ✅ Live previews in Next.js when content changes in your Sanity Content Lake
  • ✅ A Link in Next.js to exit "Preview Mode"
  • ✅ "Open preview" link and a preview "Pane" in Sanity Studio

Where you go next is completely up to you!

Let us know if you found this tutorial useful, or if there's anything that can be improved. If you need help with Sanity and Next.js, do join our community and ask in either the #help or the #nextjs channels.

Other guides by authors

Hierarchies, Graphs, and Navigation

Hierarchies are handy for organizing, but they can also fence you in. Learn how to build them, when to use them, and why you might want to treat navigation as a separate concern.

Go to Hierarchies, Graphs, and Navigation