Enhancing Content Creation: Sanity Studio Live Previews with Next.js App Router

July 20, 2023

Studio Live Preview with Next.js App Router

If you’ve landed on this blog post, you’re probably no stranger to Next.js and Sanity. It’s a pairing we’re seeing more and more often, and with good reason. Next.js is a popular and powerful framework for building server-side rendered and statically generated React applications with excellent developer experience and built-in optimized performance. Sanity offers real-time collaboration and highly flexible data modeling that’s enjoyed by content creators and developers alike.

Moving from a traditional CMS to a headless CMS often came with the downside of having to publish content first then check out those changes in production. Configuring live previews in Sanity Studio enable content creators to see real-time updates and changes to their content as they edit it within the CMS. This feature provides a dynamic and interactive editing experience, enhancing productivity and streamlining the content creation process. Live previews should be a crucial part of headless CMS authoring; your content creators and team need to feel confident in their changes before publishing. Below, I give a high level explanation of one possible solution for adding live previews to your Next.js and Sanity driven application.

Keep in mind, this example is especially useful for statically generated sites, where pages are generated at build time. Statically generating your site content will give a boost to end-user performance, but it also means any changes to the site won’t be visible until after a full build cycle. Establishing your live previews in the manner I outline below gives you the ability to preview content more efficiently by fetching data at request time, via Next.js draftMode (more on this in step #2).

Browser=Simple, Theme=Light.png

App Router and Draft Mode

New this year with Next.js v13 is the App Router architecture. Next.js has had draftMode functionality prior to the App Router change, but it looks slightly different in the latest version. Draft mode will be the foundation of your live preview setup. The Next.js docs are a great starting point to configure draft mode.

⚠️ This article assumes the user is logged into the same project’s Sanity Studio. You won't need to add additional authentication logic or a token for this process.


Set Up Your Live Preview

The basic functionality of this process is: your Next.js application is rendered in an iframe within Sanity Studio view frames. Let’s get into it!

Quick guide of the steps:

  1. Add the preview view to your sanity desk structure configuration
  2. Create a preview endpoint in Next.js for Sanity to hit and enable draftMode
  3. Resolve the preview path based on url parameters
  1. In your Studio code, wherever you define your desk structure (i.e. sanity.config.js, deskStructure.js), add a previewView with the iframe-pane plugin

    1. Define the previewUrl, this is where your Next.js code is deployed and accessible (not localhost). This could be a deployed staging url or simply window.location.origin . In the example below, we use an environment variable that allows developers to update the url for testing purposes
    2. This method can be extracted and added to individual views or written inline

    Example

    const makeLivePreviewViews = (S) => [ S.view .component(IFrame) .icon(RiImageFill) .options({ url: async (doc) => { const previewUrl = new URL(process.env.SANITY_STUDIO_PREVIEW_URL) || new URL(`${window.location.origin}/api/preview`) previewUrl.searchParams.append("_type", doc._type) previewUrl.searchParams.append("_id", doc._id) const previewPath = await fetch(previewUrl).then((res) => res.url) return previewPath }, reload: { revision: 300 }, }) .title("Preview"), ]
  2. Add an /api/preview route to your Next.js app and enable draft mode

    1. The Studio preview pane will send a request to this route with _type and _id url parameters that allow us to map to the proper page
    2. calling draftMode().enable() sets a cookie that enables fetching data at request time, instead of build time

    Example

    import { draftMode } from "next/headers" import { NextResponse } from "next/server" /** We'll talk about this helper function a little later */ import { documentPrimitivesToPath } from "@/utils/documentPrimitivesToPath" export async function GET(req: Request) { const { searchParams } = new URL(req.url) const _type = searchParams.get("_type") const _id = searchParams.get("_id") // Require all params if (!_type || !_id) return new NextResponse("Invalid params", { status: 400 }) // Check that _type and _id are valid, and grab enough info to determine preview path try { const path = await documentPrimitivesToPath({ _id, _type }) // Set draft mode cookie that enables fetching data at request time (instead of at build time) draftMode().enable() return new NextResponse(null, { status: 307, headers: { Location: path, }, }) } catch (e) { return new NextResponse("Not found", { status: 404 }) } }
  3. Map document primitives to their corresponding paths

    1. Create a utility function to map the documents’ _type and _id to a page.

    Example

    import { q } from "groqd" import { runQuery } from "@/sanity/client" import { SchemaTypes } from "@/sanity/schemas/schemaTypes" /** * Given a document's _id and _type, return the path to its page. */ export const documentPrimitivesToPath = async ({ _id, _type, }: { _id: string _type: string }) => { /** Retrieve documents from Sanity and define data to return */ const doc = await runQuery( q("*") .filter(`_type == $type && _id == $id`) .grab$( { _id: q.string() }, { "_type == 'blockPage'": { _type: q.literal("blockPage"), slug: q.slug("slug"), }, "_type == 'headerConfig'": { _type: q.literal("headerConfig"), }, "_type == 'blogPost'": { _type: q.literal("blogPost"), slug: q.slug("slug"), date: q.string().optional().default(DEFAULT_BLOG_DATE), } } ) .slice(0), { type: _type, id: _id, } ) if (!(doc && "_id" in doc)) throw new Error("Invalid document") // Determine preview path based on type. let path = "" if ("_type" in doc) { if (doc._type === "blockPage") path = doc.slug else if (doc._type === "headerConfig") path = "/" else if (doc._type === "blogPost") path = `/blog/${doc.date}` } return path }

🙌🏼 That’s it! Three steps to build live previews within your Sanity Studio using the power of Next.js App Router and draftMode. Deploy your app and checkout your new Preview Pane. You have ample room for customization to tailor this solution to your specific needs. For example, you could decide to only add the preview pane to certain document views, configure different paths for unique scenarios within your data model, or add more authentication protection with a preview secret. Live previews are also beneficial when combined with document-level localization, as a way to ensure your app looks as expected when a different language is selected.

Your Content Creators Will Thank You

With live previews in place, content creators can make changes in the Sanity Studio and see the immediate impact on the live website without having to navigate back and forth between tabs or windows. This real-time preview functionality improves collaboration, speeds up content creation, and allows for better visualizing and fine-tuning of the final output.

Related Posts

A New Perspective: Sanity Perspectives and Live Previews

July 7, 2023
In this post I'll walk through Sanity Perspectives and how they've drastically simplified our live preview workflows.

Green with Envy: Tracing Network Calls Across Your Application Stack

November 20, 2023
Envy is a zero config network tracing and telemetry viewer for development. It allows engineers to have a live view into the network requests your application stack is making during local development.

Build a Text Summarizer in 100 Lines of Code with Vercel AI SDK

June 16, 2023
Vercel recently announced their AI SDK. I wanted to see just how easy it was to get up and running with this Vercel AI SDK and build something simple, but with some potential usefulness.
Grant Sander
Carlos Kelly