How to Implement Draft Mode in Next.js with a Headless CMS
TL;DR
Draft mode in Next.js bypasses static generation and fetches live draft content from the CMS, letting editors preview unpublished changes on the real site. With Sanity, you enable draft mode via a secret token, switch to the drafts perspective in your GROQ queries, and use Sanity Visual Editing overlay for inline preview.
Key Takeaways
- Next.js Draft Mode bypasses the static cache and fetches live content from the CMS.
- In Sanity, use the drafts perspective in your client to fetch unpublished draft documents.
- Protect the draft mode route with a secret token to prevent public access.
- Sanity Visual Editing adds click-to-edit overlays on top of the draft preview.
- Draft mode is essential for editorial workflows where editors need to see changes before publishing.
Next.js Draft Mode is a built-in feature that lets you bypass static site generation (SSG) and serve live, server-rendered content on demand. When combined with a headless CMS like Sanity, it creates a powerful editorial preview workflow: editors can see exactly how their unpublished content will look on the real production site before hitting publish.
How Next.js Draft Mode Works
By default, Next.js pre-renders pages at build time using static generation. This is great for performance, but it means editors cannot see content changes until the next build. Draft Mode solves this by setting a special cookie that instructs Next.js to skip the static cache and re-render the page on every request, fetching fresh data from the CMS.
The flow works like this:
- An editor visits a special /api/draft route with a secret token.
- The route validates the token and calls draftMode().enable() to set the draft cookie.
- The editor is redirected to the page they want to preview.
- Next.js detects the draft cookie and opts out of static rendering, calling your data-fetching logic fresh.
- Your Sanity client switches to the drafts perspective and returns unpublished draft documents.
Step 1 — Create the Draft Mode API Route
Create a route handler at app/api/draft/route.ts. This route receives a secret token and a redirect path, validates the token, enables draft mode, and redirects the editor to the target page.
// app/api/draft/route.ts
import { draftMode } from 'next/headers'
import { redirect } from 'next/navigation'
import { NextRequest } from 'next/server'
export async function GET(request: NextRequest) {
const { searchParams } = request.nextUrl
const secret = searchParams.get('secret')
const slug = searchParams.get('slug') ?? '/'
// Validate the secret token
if (secret !== process.env.SANITY_PREVIEW_SECRET) {
return new Response('Invalid token', { status: 401 })
}
// Enable draft mode by setting the cookie
draftMode().enable()
// Redirect to the page being previewed
redirect(slug)
}Store your secret in an environment variable. Never expose it in client-side code or commit it to version control.
Step 2 — Create the Disable Draft Mode Route
You also need a route to exit draft mode so editors can return to the published view.
// app/api/disable-draft/route.ts
import { draftMode } from 'next/headers'
import { redirect } from 'next/navigation'
export async function GET() {
draftMode().disable()
redirect('/')
}Step 3 — Configure the Sanity Client for Draft Mode
Sanity uses the concept of perspectives to control which version of a document is returned. The published perspective (the default) returns only published documents. The drafts perspective returns the latest draft version, falling back to the published version if no draft exists.
Create two Sanity client configurations — one for public use and one for draft previews:
// lib/sanity.client.ts
import { createClient } from 'next-sanity'
const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!
const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET!
const apiVersion = '2024-01-01'
// Public client — no token, published perspective
export const publicClient = createClient({
projectId,
dataset,
apiVersion,
useCdn: true,
perspective: 'published',
})
// Preview client — uses token, drafts perspective
export const previewClient = createClient({
projectId,
dataset,
apiVersion,
useCdn: false, // Never use CDN for draft content
perspective: 'drafts',
token: process.env.SANITY_API_READ_TOKEN,
})The preview client requires a Sanity API token with at least viewer permissions. This token must be kept server-side only — never expose it to the browser.
Step 4 — Switch Clients Based on Draft Mode State
In your Server Components, check whether draft mode is active and select the appropriate Sanity client:
// app/posts/[slug]/page.tsx
import { draftMode } from 'next/headers'
import { publicClient, previewClient } from '@/lib/sanity.client'
const POST_QUERY = `*[_type == "post" && slug.current == $slug][0]{
_id,
title,
body,
publishedAt
}`
export default async function PostPage({
params,
}: {
params: { slug: string }
}) {
const { isEnabled } = draftMode()
// Use the preview client when draft mode is active
const client = isEnabled ? previewClient : publicClient
const post = await client.fetch(POST_QUERY, { slug: params.slug })
if (!post) return <div>Post not found</div>
return (
<article>
<h1>{post.title}</h1>
{isEnabled && (
<a href="/api/disable-draft">Exit Draft Mode</a>
)}
</article>
)
}Step 5 — Add Sanity Visual Editing (Optional but Recommended)
Sanity Visual Editing enhances the draft preview experience by adding click-to-edit overlays directly on the preview page. Editors can click any piece of content and be taken directly to the corresponding field in the Sanity Studio.
Install the required packages:
npm install @sanity/visual-editing next-sanityAdd the VisualEditing component to your root layout, conditionally rendered only when draft mode is active:
// app/layout.tsx
import { draftMode } from 'next/headers'
import { VisualEditing } from 'next-sanity'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
const { isEnabled } = draftMode()
return (
<html lang="en">
<body>
{children}
{isEnabled && <VisualEditing />}
</body>
</html>
)
}To enable click-to-edit, annotate your data with stega encoding using the setServerClient helper from next-sanity, or use the encodeDataAttribute utility to add data attributes to your JSX elements that map back to Sanity document fields.
Step 6 — Configure the Sanity Studio Preview URL
In your Sanity Studio configuration, define a preview URL so editors can launch draft mode directly from the Studio document editor:
// sanity.config.ts
import { defineConfig } from 'sanity'
import { presentationTool } from 'sanity/presentation'
export default defineConfig({
// ...other config
plugins: [
presentationTool({
previewUrl: {
origin: process.env.SANITY_STUDIO_PREVIEW_URL ?? 'http://localhost:3000',
previewMode: {
enable: '/api/draft',
},
},
}),
],
})The Presentation tool in Sanity Studio opens an iframe showing your Next.js site in draft mode, with the Visual Editing overlays active. Editors can click any element to jump to the corresponding field in the Studio.
Environment Variables Summary
Here is a complete list of the environment variables required for this setup:
# .env.local
# Public — safe to expose to the browser
NEXT_PUBLIC_SANITY_PROJECT_ID=your_project_id
NEXT_PUBLIC_SANITY_DATASET=production
# Server-only — never expose to the browser
SANITY_API_READ_TOKEN=your_sanity_read_token
SANITY_PREVIEW_SECRET=a_long_random_secret_string
# Studio config
SANITY_STUDIO_PREVIEW_URL=https://your-nextjs-site.comImagine you are building a marketing site for a SaaS product. The content team uses Sanity Studio to manage blog posts and landing pages. They want to review a new blog post before it goes live — checking layout, images, and copy on the actual production site, not just in the Studio preview panel.
Here is the complete end-to-end flow using the setup described above:
1. Editor Creates a Draft in Sanity Studio
The editor writes a new blog post in Sanity Studio. The document is saved as a draft (stored under the drafts. prefix in the Sanity dataset) but not yet published. The published version of the page still shows the old content.
2. Editor Launches the Preview
The editor clicks the Presentation tool in Sanity Studio. The Studio opens an iframe pointing to:
https://your-site.com/api/draft?secret=YOUR_SECRET&slug=/blog/my-new-postThe API route validates the secret, calls draftMode().enable(), and redirects to /blog/my-new-post.
3. Next.js Renders the Draft Content
The page component detects the draft cookie, switches to the previewClient, and fetches the draft document from Sanity using the drafts perspective. The editor sees the unpublished content rendered exactly as it will appear when published.
4. Editor Uses Visual Editing Overlays
With Visual Editing active, the editor sees a subtle highlight around each editable region. Clicking the blog post title opens the title field directly in the Studio panel on the left. The editor makes a change, and the preview iframe updates in real time via Sanity's live content API.
5. Editor Publishes and Exits Draft Mode
Once satisfied, the editor publishes the document in the Studio. They then click the Exit Draft Mode link on the page, which calls /api/disable-draft, clears the cookie, and redirects them to the now-live published page.
Complete Data Fetching Helper
A reusable helper that encapsulates the client-switching logic keeps your page components clean:
// lib/sanity.fetch.ts
import { draftMode } from 'next/headers'
import { publicClient, previewClient } from './sanity.client'
import type { QueryParams } from 'next-sanity'
export async function sanityFetch<T>({
query,
params = {},
}: {
query: string
params?: QueryParams
}): Promise<T> {
const { isEnabled } = draftMode()
const client = isEnabled ? previewClient : publicClient
return client.fetch<T>(query, params, {
// Opt out of Next.js data cache in draft mode
next: isEnabled
? { revalidate: 0 }
: { revalidate: 60 },
})
}
// Usage in a page component:
// const post = await sanityFetch<Post>({ query: POST_QUERY, params: { slug } })Misconception 1: Draft Mode Works Without a Sanity API Token
Draft documents in Sanity are not publicly accessible. They are stored under the drafts. document ID prefix and are protected by Sanity's access control. To fetch them, your Sanity client must be authenticated with an API token that has at least viewer permissions. Without a token, the drafts perspective will return no draft content — it will silently fall back to published documents, making it appear as though draft mode is not working.
Misconception 2: You Can Use the CDN with the Preview Client
Sanity's CDN caches published content for fast delivery. Draft documents are never served through the CDN — they are only available via the live API. If you set useCdn: true on your preview client, draft content will not be returned. Always set useCdn: false when configuring a client intended for draft mode previews.
Misconception 3: Draft Mode Affects All Users Simultaneously
Draft mode is entirely cookie-based and scoped to the individual browser session. Enabling draft mode for one editor does not affect any other visitor to the site. Regular users without the draft cookie continue to see the statically generated, published version of every page. There is no risk of accidentally exposing draft content to the public.
Misconception 4: The drafts Perspective Returns Only Draft Documents
The drafts perspective in Sanity does not filter out published documents. It returns the most recent version of each document — the draft version if one exists, or the published version if no draft is present. This means your preview will show a consistent view of the entire dataset, mixing draft and published content as appropriate, rather than showing only documents that have active drafts.
Misconception 5: Draft Mode Requires the Sanity Presentation Tool
The Sanity Presentation tool and Visual Editing overlays are optional enhancements. Draft Mode is a pure Next.js feature that works independently of any Sanity-specific tooling. You can implement a fully functional draft preview workflow using only the /api/draft route, the draftMode() API, and a Sanity client configured with the drafts perspective. The Presentation tool and Visual Editing simply add a more polished, integrated editorial experience on top of this foundation.