See Sanity in action 👀 Join us for a live product demo + Q&A →
August 26, 2021

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

By Simeon Griggs & Knut Melvær

Live-as-you-type previewing of draft content is the ultimate upgrade for your content creators so that they can see changes in real-time and gain increased confidence to hit that publish button.

Composable content management is a powerful tool. Content separated from its presentation is the right way to think about content modelling. However, confidence can suffer when the edited content lacks the context of how those edits will affect real-world outputs.

Protip

The final code for this tutorial is available as a repository.

Walkthrough

Scope of this guide and possible alternatives

This guide is deliberately focused on the experience of manually creating a new Next.js 13 application along with a separate Sanity Studio project and writing the code to bring the two together.

All the instructions below could also be adapted to an existing Next.js application:

  • Want working code faster? Our Next.js starter has an example blog schema and live preview already set up and can be instantly deployed to Vercel.
  • Interested in using Next.js’ app directory? This guide will only cover the pages directory approach. Fortunately, though the app directory strategy is still in beta, next-sanity already supports it! You can find instructions on configuring next-sanity with the app directory in that package’s readme.
  • Looking to embed Sanity Studio v3 into Next.js? If you’re interested in merging both projects into one, it’s possible to embed your Sanity Studio as a route in your Next.js application. The simplest way to do this is also with next-sanity.
  • TypeScript is not required. The code examples in this guide are all in TypeScript; However, TypeScript is not necessary for any of this to work. You need to remove the types from these examples if working with JavaScript.

Assumptions

  • You already have a Sanity account
  • You have some familiarity with both Sanity Studio and Next.js
  • You are reasonably confident with JavaScript in general and React in particular.

Create a new Next.js 13 project

Create a new project using the command below. The code examples in this project will assume you’ve selected TypeScript and eslint support – but they’re not necessary to continue.

# from the command line
npx create-next-app@latest

# give your project a name
What is your project named? nextjs-live-preview

# enter the new project's directory
cd nextjs-live-preview

# run the development server
npm run dev

Need more help? See the Next.js docs for getting started.

At the time of writing, this will configure a Next.js project using the pages directory.

Visit http://localhost:3000 in your web browser, and you should see this landing screen to show it’s been installed correctly.

The default home page of a new Next.js 13 project

The new Next.js website comes with some code boilerplate. So that you can more easily see what’s Sanity and what’s Next.js – you will remove almost all of it.

First, let’s simplify the home page route file and update it with the code below.

// ./pages/index.tsx

export default function Home() {
  return (
    <main className="flex items-center justify-center min-h-screen">
      Populate me with Sanity Content
    </main>
  )
}

Secondly, to simplify styling, you'll add Tailwind CSS’s Play CDN to the entire App. Update the _app.tsx file with the below.

// ./pages/_app.tsx

import type { AppProps } from "next/app";
import Head from "next/head";
import Script from "next/script";

export default function App({ Component, pageProps }: AppProps) {
  return (
    <>
      <Head>
        <meta name="viewport" content="width=device-width, initial-scale=1" />
      </Head>
      <Script src="https://cdn.tailwindcss.com?plugins=typography"></Script>
      <Component {...pageProps} />
    </>
  );
}

Note: This is a fast way – but not recommended for production – to add Tailwind CSS to a Next.js project. If you’re taking this project through to production, follow the guide from the Tailwind CSS documentation to install it properly.

Now our Next.js app at http://localhost:3000 should look much simpler:

A blank webpage other than the words "Populate me with Sanity Content"

Create a new Sanity Studio v3 project

You‘ll create a new Sanity Studio in its dedicated folder for this tutorial. Using the “Movies” template and dataset so that you have some data to start with.

# from the command line
npm create sanity@latest -- --template moviedb --create-project "Sanity Live Preview" --dataset production

# follow prompts during install
# this tutorial uses TypeScript
# and choose to import the sample data

# enter the new project's directory
cd sanity-live-preview

# run the development server
npm run dev

For complete instructions and troubleshooting, see “Create a Sanity project” in the Documentation.

Pre-flight check

You should now have two folders:

/nextjs-live-preview -> contains our Next.js 13 site
/sanity-live-preview -> contains our Sanity Studio v3

From which you can run both projects separately

  • http://localhost:3000 to view the Next.js website
  • http://localhost:3333 to view the Sanity Studio

Linking Sanity to Next.js with next-sanity

The next-sanity toolkit and associated packages include helpers to query and display Sanity content. You’ll start by using the install script provided in the repository.

# in /nextjs-live-preview
npm install next-sanity @portabletext/react @sanity/image-url

This will install:

  • next-sanity
    The toolkit with helpers for previewing content and embedding the Sanity Studio within the Next.js App
  • @portabletext/react
    A React component for rendering the contents of a portable text field
  • @sanity/image-url
    A helper library to generate complete image URLs from just the Project ID, Dataset, and ID of a Sanity image asset.

You'll now need to create some new files within the project.

Setup Sanity client for us to query content:

// ./src/lib/sanity.client.ts

import {createClient} from 'next-sanity'

export const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID
export const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET // "production"
export const apiVersion = process.env.NEXT_PUBLIC_SANITY_API_VERSION // "2023-01-01"

export const client = createClient({projectId, dataset, apiVersion, useCdn: true})

Set up an environment variables file to read project details from. You can find these values from sanity.config.ts inside your new Sanity Studio project or by logging into sanity.io/manage

# ./.env.development

NEXT_PUBLIC_SANITY_PROJECT_ID=replace-with-your-project-id
NEXT_PUBLIC_SANITY_DATASET=production
NEXT_PUBLIC_SANITY_API_VERSION=2023-01-01

Note: You’ll need to recreate these environment variables when deploying the site to your hosting.

Fetching data from Sanity

Before you set up Live Preview to see draft content from Sanity, you should confirm you can fetch published data.

Update the index route to include a getStaticProps function. In it, you’ll import the Sanity Client to query for every movie document that has a valid slug.

// ./pages/index.tsx

import { groq } from "next-sanity";
import type { SanityDocument } from "@sanity/client";
import { client } from "../lib/sanity.client";
import Movies from "../components/Movies";

const query = groq`*[_type == "movie" && defined(slug.current)]{
  _id,
  title, 
  slug
}`;

export const getStaticProps = async () => {
  const data = await client.fetch(query);

  return { props: { data } };
};

export default function Home({ data }: { data: SanityDocument[] }) {
  return <Movies movies={data} />
}

Create a new Component file to display the list of movie data:

// ./components/Movies.tsx

import Link from "next/link";
import type { SanityDocument } from "@sanity/client";
import Head from "next/head";

export default function Movies({ movies }: { movies: SanityDocument[] }) {
  return (
    <>
      <Head>
        <title>{movies.length} Movies</title>
      </Head>
      <main className="container mx-auto grid grid-cols-1 divide-y divide-blue-100">
        {movies.map((movie) => (
          <Link key={movie._id} href={movie.slug.current} className="p-4 hover:bg-blue-50">
            <h2>{movie.title}</h2>
          </Link>
        ))}
      </main>
    </>
  );
}

Your home page at http://localhost:3000 should now look like this:

Content queried from a Sanity dataset and displayed in a Next.js application

If so, great … until you click on one of those links.

You can fix that 404 by adding another route. Create a new file in ./pages

// ./pages/[slug].tsx

import { SanityDocument } from "@sanity/client";
import { GetStaticPaths, GetStaticProps } from "next";
import { groq } from "next-sanity";
import { client } from "../lib/sanity.client";
import Movie from "../components/Movie";

const query = groq`*[_type == "movie" && slug.current == $slug][0]{
  title,
  poster,
  overview
}`;

// Prepare Next.js to know which routes already exist
export const getStaticPaths: GetStaticPaths = async () => {
  const paths = await client.fetch(
    groq`*[_type == "movie" && defined(slug.current)][]{
      "params": { "slug": slug.current }
    }`
  );

  return { paths, fallback: true };
};

export const getStaticProps: GetStaticProps = async ({ params }) => {
  const queryParams = { slug: params?.slug ?? `` };

  const movie = await client.fetch(query, queryParams);

  return {
    props: {
      data: { movie },
    },
  };
};

export default function Page({ data }: { data: { movie: SanityDocument } }) {
  return <Movie movie={data.movie} />
}

Create another component, this one to display a single movie document’s content

// ./components/Movie.tsx

import { PortableText } from "@portabletext/react";
import Image from "next/image";
import imageUrlBuilder from "@sanity/image-url";
import { SanityDocument } from "@sanity/client";
import { client } from "../lib/sanity.client";
import Head from "next/head";

const builder = imageUrlBuilder(client);

export default function Movie({ movie }: { movie: SanityDocument }) {
  return (
    <>
      <Head>
        <title>{movie.title}</title>
      </Head>
      <main className="container mx-auto prose prose-lg p-4">
        <h1>{movie.title}</h1>
        <Image
          className="float-left m-0 w-1/3 mr-4 rounded-lg"
          src={builder.image(movie.poster).width(300).height(300).url()}
          width={300}
          height={300}
          alt={movie.title}
        />
        <PortableText value={movie.overview} />
      </main>
    </>
  );
}

You’ll likely also need to make an update to next.config.ts so that next/image will load images from the Sanity CDN:

images: {
  domains: ['cdn.sanity.io'],
}

You should now be able to click links on the home page and see pages just like these:

Our Next.js application can now display a Sanity image and block content!

Notice how this route uses the <PortableText /> component to render block content styled by Tailwind CSS’s Typography prose class names.

Status update

You should now have the following:

  • A working Sanity Studio with placeholder content
  • A Next.js application with a home page that lists published pages of content – and those individual pages show rich text and an image.

Now let’s see changes made in Sanity Studio on the Next.js website!

Setup next-sanity’s preview functionality

Add a CORS origin for our project

Any application that needs to make authenticated requests to our Sanity Project must have its URL added as a valid CORS origin.

This can be done inside sanity.io/manage; navigate to the API tab and enter http://localhost:3000, the default URL for developing a local Next.js project.

Ensure you check ”Allow credentials.”

Note: You must do this again when you deploy your application to a hosting provider, adding its URL to the list of CORS origins.

If you see the error “Failed to fetch” from your front end while setting up Live Preview, likely, this step wasn’t performed.

You'll need to add a new CORS origin for every URL your Next.js application is deployed to where you'd like to use live preview

Add more routes to the Next.js application

Following the instructions from the next-sanity README, you'll need to create new routes so that users can enter and exit preview mode – which will display draft content.

The next-sanity readme details trade-offs on authenticating users into preview mode. This tutorial relies on the user being logged into the same project’s Sanity Studio. So you won't need to add additional authentication logic or a token to this project.

Create the following new files in your project.

Two API Routes to enter and exit “preview mode”:

// ./pages/api/preview.ts

export default function preview(req, res) {
    res.setPreviewData({})
    res.writeHead(307, {Location: '/'})
    res.end()
}
// ./pages/api/exit-preview.ts

export default function exit(req, res) {
  res.clearPreviewData()
  res.writeHead(307, {Location: '/'})
  res.end()
}

A usePreview() custom hook for fetching preview data when authenticated:

// ./src/lib/sanity.preview.ts

import {definePreview} from 'next-sanity/preview'
import {projectId, dataset} from './sanity.client'

function onPublicAccessOnly() {
  throw new Error(`Unable to load preview as you're not logged in`)
}
export const usePreview = definePreview({projectId, dataset, onPublicAccessOnly})

How this will work:

  1. Any visitor that navigates to localhost:3000/api/preview will be redirected to the home page, with the site put into “Preview Mode”
  2. Preview Mode is a preview true/false value available in getStaticProps and passed down to the document.
  3. If that user is authenticated into the Sanity Studio, they’ll see draft (unpublished) content and see the website update as changes are made to documents.
  4. With preview mode active, server-side queries are skipped and only made client-side by the usePreview() hook.

We’ll need to update our home page route (yes, again) to include:

  • A few new imports
  • Account for preview mode potentially being activated
  • A component that will perform an authenticated query client-side with usePreview()
// ./pages/index.tsx

import { lazy } from "react";
import { groq } from "next-sanity";
import type { SanityDocument } from "@sanity/client";
import { client } from "../lib/sanity.client";
import Movies from "../components/Movies";
import { PreviewSuspense } from "next-sanity/preview";

const PreviewMovies = lazy(() => import("../components/PreviewMovies"));
const query = groq`*[_type == "movie" && defined(slug.current)]{
  _id,
  title, 
  slug
}`;

export const getStaticProps = async ({ preview = false }) => {
  if (preview) {
    return { props: { preview } };
  }

  const data = await client.fetch(query);

  return { props: { preview, data } };
};

export default function Home({
  preview,
  data,
}: {
  preview: Boolean;
  data: SanityDocument[];
}) {

  // PreviewSuspense shows while data is being fetched
  // The fetch happens inside PreviewMovies
  return preview ? (
    <PreviewSuspense fallback="Loading...">
      <PreviewMovies query={query} />
    </PreviewSuspense>
  ) : (
    <Movies movies={data} />
  );
}

Create a new component for the authenticated query:

// ./components/PreviewMovies.tsx

import Link from "next/link";
import { usePreview } from "../lib/sanity.preview";
import Movies from "./Movies";

export default function PreviewMovies({ query }: { query: string }) {
  const data = usePreview(null, query);

  return (
    <>
      <Movies movies={data} />
      <Link
        className="bg-blue-500 p-6 text-white font-bold fixed bottom-0 right-0"
        href="/api/exit-preview"
      >
        Exit Preview
      </Link>
    </>
  );
}

Now when you visit the Next.js website at http://localhost:3000 you should still see the same list of movie pages.

However, if you visit http://localhost:3000/api/preview you’ll be redirected to the home page with preview mode activated. You’ll see the “Exit Preview” button in the bottom right.

Bring up your Sanity Studio in another browser and begin to make changes – or even create new documents – and see the list update as you type!

With Next.js in Preview Mode, we're now seeing new draft documents listed

However, if you click on any movie page links, those pages are not showing updates. We’ll need to update our [slug].tsx file similarly.

To detect if the preview mode is active:

// ./pages/[slug].tsx

import { PreviewSuspense } from "next-sanity/preview";
import { lazy } from "react";
import { SanityDocument } from "@sanity/client";
import { GetStaticPaths, GetStaticProps } from "next";
import { groq } from "next-sanity";

import { client } from "../lib/sanity.client";
import Movie from "../components/Movie";

const PreviewMovie = lazy(() => import("../components/PreviewMovie"));
const query = groq`*[_type == "movie" && slug.current == $slug][0]{
  title,
  poster,
  overview
}`;

export const getStaticPaths: GetStaticPaths = async () => {
  const paths = await client.fetch(
    groq`*[_type == "movie" && defined(slug.current)][]{
      "params": { "slug": slug.current }
    }`
  );

  return { paths, fallback: true };
};

export const getStaticProps: GetStaticProps = async ({
  params,
  preview = false,
}) => {
  const queryParams = { slug: params?.slug ?? `` };

  if (preview) {
    return { props: { preview, data: { queryParams } } };
  }

  const movie = await client.fetch(query, queryParams);

  return {
    props: {
      preview,
      data: {
        movie,
        queryParams: {},
      },
    },
  };
};

export default function Page({
  preview,
  data,
}: {
  preview: Boolean;
  data: {
    movie: SanityDocument;
    queryParams: {};
  };
}) {
  return preview ? (
    <PreviewSuspense fallback="Loading...">
      <PreviewMovie query={query} queryParams={data.queryParams} />
    </PreviewSuspense>
  ) : (
    <Movie movie={data.movie} />
  );
}

Like our home page, we’ll need a wrapper for the Movie component to perform the client-side query.

// ./components/PreviewMovie.tsx

import Link from "next/link";
import { usePreview } from "../lib/sanity.preview";
import Movie from "./Movie";

export default function PreviewMovie({ query, queryParams }: { query: string, queryParams: {[key: string]: any} }) {
  const data = usePreview(null, query, queryParams);

  return (
    <>
      <Movie movie={data} />
      <Link
        className="bg-blue-500 p-6 text-white font-bold fixed bottom-0 right-0"
        href="/api/exit-preview"
      >
        Exit Preview
      </Link>
    </>
  );
}

With this setup, you should now not only see changes to existing documents but even be able to preview pages of new, not-yet-published documents as well!

View entire pages before publishing with the Next.js application in preview mode

Previewing Next.js from inside the Studio

Your setup works excellently so far, but putting these two browser windows side-by-side is more work than it needs to be. Using View Panes, you can embed the Next.js website into an iframe that can place it directly next to the document form editor.

For this, you’ll work in your Sanity Studio‘s folder and install the Iframe Pane plugin.

npm i sanity-plugin-iframe-pane

In your Sanity Studio project, create a new file to handle the Default Document Node, which defines the default settings when the editor for a particular schema type is loaded.

// ./src/defaultDocumentNode.ts

import {DefaultDocumentNodeResolver} from 'sanity/desk'
import Iframe from 'sanity-plugin-iframe-pane'

export const defaultDocumentNode: DefaultDocumentNodeResolver = (S, {schemaType}) => {
  switch (schemaType) {
    case `movie`:
      return S.document().views([
        S.view.form(),
        S.view
          .component(Iframe)
          .options({
            url: `http://localhost:3000/api/preview`,
          })
          .title('Preview'),
      ])
    default:
      return S.document().views([S.view.form()])
  }
}

Now import this into the deskTool() plugin in your Studio config file:

// ./sanity.config.ts

// ...other imports
import { defaultDocumentNode } from './src/defaultDocumentNode'

export default defineConfig({
  // ...other config settings
  plugins: [
    deskTool({
      defaultDocumentNode,
    }),
    // ...other plugins
  ],
})

Open up any Movie type document now, and you should be able to show the Next.js website side-by-side with the editor and see draft content in real-time.

Iframe pane can display the Next.js Application directly inside the Studio

Next steps

Write more resilient components by checking existing values before trying to display them. Assume no data exists because the author is previewing a new document. Swap for placeholders or remove the components altogether. For example:

// In preview mode, this title might not yet exist!
<h1>{movie.title}</h1>

// Swap with a placeholder
{movie?.title ? <h1>{movie.title}</h1> : <h1>Untitled</h1>}

// Or just remove the component
{movie?.title ? <h1>{movie.title}</h1> : null}

Update the options for your Iframe Pane to a function that will render a different URL based on the current document, valid for if you update your Next.js preview route to redirect a URL other than the home page.

.options({
  URL: (doc) => doc?.slug?.current 
    ? `http://localhost:3000/api/preview?slug=${doc.slug.current}`
    : `http://localhost:3000/api/preview`,
})

Other guides by authors

How to implement Multi-tenancy with Sanity

In this guide, you’ll see how Sanity separates organizations, projects, datasets, and members by working through a hypothetical example of a growing company that can expand its content model as they grow – without needing a complete overhaul.

Simeon Griggs
Go to How to implement Multi-tenancy with Sanity