draftMode()

Enables, disables and checks Draft Mode so editors can preview unpublished CMS content.

Signature
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.
  • isEnabled is readable inside a "use cache" scope, but cookies() and headers() are not, and calling enable() or disable() there throws.
  • Always check a secret in the enable route — an open one lets anyone bypass your cache.

Example

Example · typescript
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.

Example · typescript
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.

Example · typescript
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.

Example · typescript
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

  • Be the first to comment on this lesson.