draftMode()
Enables, disables and checks Draft Mode so editors can preview unpublished CMS content.
function draftMode(): Promise<DraftMode>- Added in Next.js v13.4.0
Return value
Promise<DraftMode>A promise resolving to an object with the boolean property isEnabled and the methods enable() and disable().
Errors
Calling enable() or disable() inside a caching directive scope such as "use cache" throws an error.
Changelog
- PHP v15.0.0-RCdraftMode is now an async function and must be awaited. A codemod is available; synchronous access still works in 15 but is deprecated.
- PHP v13.4.0draftMode introduced.
Draft Mode is a per-browser switch that makes a route render live, uncached content. enable() sets a signed __prerender_bypass cookie; every later request carrying it re-executes cached work instead of serving stored output, so a CMS editor sees a draft while everyone else sees the published page.
When to reach for it
Build a Route Handler at something like app/draft/route.ts, validate a secret and a content id from the query string, call enable(), then redirect to the page. Your CMS's "Preview" button points at that URL. In the page itself, read isEnabled to decide whether to request drafts or published documents from the CMS.
What trips people up
- The bypass cookie is regenerated on every
next build, so previews stop working after a deploy until the editor clicks Preview again. - Link to your disable route with
prefetch={false}, or a prefetch will silently end the session. isEnabledis readable inside a"use cache"scope, butcookies()andheaders()are not, and callingenable()ordisable()there throws.- Always check a secret in the enable route — an open one lets anyone bypass your cache.
Example
import { draftMode } from 'next/headers'
import { redirect } from 'next/navigation'
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const secret = searchParams.get('secret')
const slug = searchParams.get('slug')
if (secret !== process.env.DRAFT_SECRET || !slug) {
return new Response('Invalid token', { status: 401 })
}
const draft = await draftMode()
draft.enable()
redirect(`/posts/${slug}`)
}When to use it
- Wire a headless CMS Preview button to a route that enables Draft Mode and redirects to the draft
- Render unpublished fields in a page only when isEnabled is true
- Expose an exit-preview link that calls disable() and returns to the live page
- Force fresh CMS reads for editors while everyone else is served the cached page
Frequently asked questions
Why did Draft Mode stop working after I deployed?
Because a new bypass cookie value is generated on every next build, which invalidates the cookie editors are still holding. That is intentional — it keeps the value unguessable. Editors just need to click Preview again to get a fresh cookie.
How do I secure my Draft Mode enable route?
Require a secret in the query string and compare it against an environment variable before calling enable(), returning 401 otherwise. Without that check anyone who finds the URL can bypass your cache and read unpublished content.
Can I use draftMode() inside a "use cache" function?
You can read isEnabled inside a caching directive scope, but calling enable() or disable() there throws. cookies() and headers() are also disallowed in that scope even when Draft Mode is active.
More examples
Branching on isEnabled in a page
Note that params is awaited: it is a Promise in Next.js 15 and later. isEnabled is the only part of draftMode() safe to read inside a caching scope, and when it is true every function under a caching directive re-runs per request and nothing is written back to the cache, so the editor always sees fresh CMS data.
import { draftMode } from 'next/headers'
export default async function Post({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
const { isEnabled } = await draftMode()
const post = await getPost(slug, { preview: isEnabled })
return (
<article>
{isEnabled && <p>Previewing an unpublished draft</p>}
<h1>{post.title}</h1>
<div>{post.body}</div>
</article>
)
}An exit-preview route
disable() deletes the bypass cookie, so subsequent requests are served from the cache again. If you link here with next/link, pass prefetch={false} — the router prefetches links in the viewport, and a prefetch of this route would end the preview session before the editor ever clicked it.
import { draftMode } from 'next/headers'
export async function GET() {
const draft = await draftMode()
draft.disable()
return new Response(null, {
status: 307,
headers: { Location: '/' },
})
}A banner that offers the exit link safely
The component renders nothing for normal visitors, so it can sit in the root layout. prefetch={false} is load-bearing here: without it the banner would prefetch its own exit route as soon as it appeared and cancel Draft Mode immediately.
import Link from 'next/link'
import { draftMode } from 'next/headers'
export default async function DraftBanner() {
const { isEnabled } = await draftMode()
if (!isEnabled) return null
return (
<aside>
Draft Mode is on.{' '}
<Link href="/draft/disable" prefetch={false}>
Exit preview
</Link>
</aside>
)
}
Discussion