Sanity Studio v3 launches Dec 8th - See it in action at Sanity Product Day →
August 26, 2021

Live Preview with Next.js and A Complete Guide

By Simeon Griggs & Knut Melvær

Empower your authoring teams with live updates to content within your Next.js website or application. Without leaving Sanity Studio. The next-sanity toolkit enables real-time previews without the need to setup a dedicated preview server.

In this guide we'll cover setting up a new Sanity project, Next.js application and streaming live updates to content. The next-sanity toolkit contains all you'll need to query and display Sanity data. As well as handling your Studio authentication to allow live updates on the frontend.

A serverless function is required to put the app in "preview mode", but no additional services. Preview mode runs on the same hosting as your Production app.

The headline feature of next-sanity is the usePreviewSubscription() hook. It takes a few steps to get just right, so we'll walk through each one individually.


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.


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 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 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:

- /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:

- 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

// Create this in

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

Tokens and CORS

You'll need to create an API token in 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!.


  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:


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

  if (data.length === 1) {
    return data[0]

  if (preview) {
    return data.find((item) => item._id.startsWith(`drafts.`)) || data[0]

  return data[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 :)
export async function getStaticPaths() {
  const allSlugsQuery = groq`*[defined(slug.current)][].slug.current`
  const pages = await getClient().fetch(allSlugsQuery)

  return {
    paths: => `/${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
      // 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>}

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


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 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.


Please note that we don't redirect to req.query.slug in the example below. This is to avoid a so-called open redirect vulnerability, which would allow a user (or malicious actor) to control a redirect or forward to another URL.

Instead, we check for the existence of a document with the slug query parameter in our Sanity dataset first, and only then redirect to that path.

First, create a new file inside ./web/lib:

// ./web/lib/queries.js

export const articleBySlugQuery = `
  *[_type == "article" && slug.current == $slug][0] {
    "slug": slug.current

Then, create two files inside ./web/pages/api:

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

import {articleBySlugQuery} from '../../lib/queries'
import {previewClient} from '../../lib/sanity.server'

function redirectToPreview(res, Location) {
  // Enable preview mode by setting the cookies

  // Redirect to a preview capable route
  res.writeHead(307, {Location})

export default async function preview(req, res) {
  const {secret, slug} = req.query

  if (!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 (secret !== process.env.SANITY_PREVIEW_SECRET) {
    return res.status(401).json({message: 'Invalid secret'})

  if (!slug) {
    return redirectToPreview(res, '/')

  // Check if the article with the given `slug` exists
  const article = await previewClient.fetch(articleBySlugQuery, {slug})

  // If the slug doesn't exist prevent preview mode from being enabled
  if (!article) {
    return res.status(401).json({message: 'Invalid slug'})

  // Redirect to the path from the fetched article
  // We don't redirect to req.query.slug as that might lead to open redirect vulnerabilities
  return redirectToPreview(res, `/${article.slug}`)
// ./web/pages/api/exit-preview.js

export default function exit(req, res) {
  // Exit current user from preview mode

  // Redirect user back to the index page
  res.writeHead(307, {Location: '/'})

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 – after verifying it exists, that slug is then used to query the document


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 `remoteUrl` with your deployed Next.js site
const remoteUrl = ``
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)
  if (doc?.slug?.current) {
    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.

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([
        url: (doc) => resolveProductionUrl(doc),
        reload: {button: true},

export default () =>

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