CoursesIntegrated Visual Editing with Next.jsFetching preview content in draft mode
Track
Work-ready Next.js

Integrated Visual Editing with Next.js

Log in to watch a video walkthrough of this lesson
Video thumbnail

Add perspectives to your Sanity data fetches to query for draft content, when Draft Mode is enabled.

Log in to mark your progress for each Lesson and Task

For interactive live preview to be truly immersive, the same fast, cached web application your end users interact with must be put into an API-first, fully dynamic state. Thankfully, Next.js provides "Draft Mode."

See the Next.js documentation for more details on Draft Mode.

The entire web application must act differently when Draft Mode is enabled. Queries must use a different perspective and entirely skip the cache. Additional UI will be rendered into the page for clickable overlays.

In this lesson, you'll set up the app to behave differently when Draft Mode is enabled—but it will not be enabled until the next lesson.

First, you'll need to update the way content is fetched. The updates below enable Stega encoding from within the Sanity Client and the sanityFetch function. The URL is also set so that clickable links in the preview open to the correct document and field from which the content came.

The perspective is set to fetch draft content and overlay draft content on already-published content.

Update the Sanity Client file to enable Stega encoding – and modify cache settings – when Draft Mode is enabled
src/sanity/lib/client.ts
import { createClient, QueryOptions, type QueryParams } from 'next-sanity'
import { draftMode } from 'next/headers'
import { apiVersion, dataset, projectId } from '../env'
import { token } from './token'
export const client = createClient({
projectId,
dataset,
apiVersion, // https://www.sanity.io/docs/api-versioning
useCdn: true, // Set to false if statically generating pages, using ISR or tag-based revalidation
stega: {
enabled: process.env.NEXT_PUBLIC_VERCEL_ENV === 'preview',
studioUrl: '/studio',
},
})
export async function sanityFetch<QueryResponse>({
query,
params = {},
revalidate = 60, // default revalidation time in seconds
tags = [],
}: {
query: string
params?: QueryParams
revalidate?: number | false
tags?: string[]
}) {
const isDraftMode = draftMode().isEnabled
if (isDraftMode && !token) {
throw new Error('Missing environment variable SANITY_API_READ_TOKEN')
}
let queryOptions: QueryOptions = {}
let maybeRevalidate = revalidate
if (isDraftMode) {
queryOptions.token = token
queryOptions.perspective = 'previewDrafts'
queryOptions.stega = true
maybeRevalidate = 0 // Do not cache in Draft Mode
} else if (tags.length) {
maybeRevalidate = false // Cache indefinitely if tags supplied
}
return client.fetch<QueryResponse>(query, params, {
...queryOptions,
next: {
revalidate: maybeRevalidate,
tags,
},
})
}

Interactive live preview works by listening client-side to changes from your dataset and, when detected, prefetching data server-side. The machinery to do this can be configured manually if you like, but it gets a little complicated, so thankfully, it's been packaged up for us in next-sanity.

All you need is one import.

Update your root layout to import the VisualEditing component from next-sanity
src/app/(frontend)/layout.tsx
import '@/app/globals.css'
import { Header } from '@/components/Header'
import { VisualEditing } from 'next-sanity'
import { draftMode } from 'next/headers'
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode
}>) {
return (
<html lang="en">
<body className="bg-white min-h-screen">
{draftMode().isEnabled && (
<a
className="fixed right-0 bottom-0 bg-blue-500 text-white p-4 m-4"
href="/api/draft-mode/disable"
>
Disable preview mode
</a>
)}
<Header />
{children}
{draftMode().isEnabled && <VisualEditing />}
</body>
</html>
)
}

The API route code below securely enables draft mode. The validatePreviewUrl function takes in an authenticated Sanity Client to check if a private document with a rotating "secret" passed along in the request URL matches one currently in your dataset.

This function is designed to work with the Presentation plugin you will install in the next lesson. The Presentation plugin will create and rotate this secret on your behalf and request this route in the correct way to have it validated. Securing draft mode only to users who are already authenticated in the Sanity Studio.

It is unlikely that you will ever have to interact directly with this secret.

Create a new API route to enable draft mode
src/app/api/draft-mode/enable/route.ts
import { validatePreviewUrl } from '@sanity/preview-url-secret'
import { draftMode } from 'next/headers'
import { NextRequest, NextResponse } from 'next/server'
import { client } from '@/sanity/lib/client'
import { token } from '@/sanity/lib/token'
const clientWithToken = client.withConfig({ token })
export async function GET(request: NextRequest) {
if (!process.env.SANITY_API_READ_TOKEN) {
return new Response('Missing environment variable SANITY_API_READ_TOKEN', {
status: 500,
})
}
const { isValid, redirectTo = '/' } = await validatePreviewUrl(
clientWithToken,
request.url,
)
if (!isValid) {
return new Response('Invalid secret', { status: 401 })
}
draftMode().enable()
return NextResponse.redirect(new URL(redirectTo, request.url))
}

Once your browser is authenticated to view the web application in draft mode, you will see it in all other tabs in that browser.

The earlier update to the root layout included a button to exit preview mode. This is useful when content authors have finished their changes and want to see the application with the same published content that end users will see.

Create a new API route to disable draft mode
src/app/api/draft-mode/disable/route.ts
import { draftMode } from 'next/headers'
import { NextRequest, NextResponse } from 'next/server'
export function GET(request: NextRequest) {
draftMode().disable()
return NextResponse.redirect(new URL('/', request.url))
}

Your app is ready to start querying draft content—the next step is to actually make that happen. It's easiest to do this within the Presentation plugin; let's set that up in the next lesson.

Courses in the "Work-ready Next.js" track