How to preview content with Draft Mode in Next.js

Next.js has draft mode to toggle between static and dynamic pages. You can learn how it works with App Router here.

Draft Mode allows you to preview draft content from your headless CMS in your Next.js application. This is useful for static pages that are generated at build time as it allows you to switch to dynamic rendering and see the draft changes without having to rebuild your entire site.

This page walks through how to enable and use Draft Mode.

Step 1: Create a Route Handler

Create a Route Handler. It can have any name, for example, app/api/draft/route.ts.

export async function GET(request: Request) {
  return new Response('')
}

Then, import the draftMode function and call the enable() method.

import { draftMode } from 'next/headers'
 
export async function GET(request: Request) {
  const draft = await draftMode()
  draft.enable()
  return new Response('Draft mode is enabled')
}

This will set a cookie to enable draft mode. Subsequent requests containing this cookie will trigger draft mode and change the behavior of statically generated pages.

You can test this manually by visiting /api/draft and looking at your browser’s developer tools. Notice the Set-Cookie response header with a cookie named __prerender_bypass.

Step 2: Access the Route Handler from your Headless CMS

These steps assume that the headless CMS you’re using supports setting custom draft URLs. If it doesn’t, you can still use this method to secure your draft URLs, but you’ll need to construct and access the draft URL manually. The specific steps will vary depending on which headless CMS you’re using.

To securely access the Route Handler from your headless CMS:

  1. Create a secret token string using a token generator of your choice. This secret will only be known by your Next.js app and your headless CMS.
  2. If your headless CMS supports setting custom draft URLs, specify a draft URL (this assumes that your Route Handler is located at app/api/draft/route.ts). For example:
Terminal
https://<your-site>/api/draft?secret=<token>&slug=<path>
  • <your-site> should be your deployment domain.
  • <token> should be replaced with the secret token you generated.
  • <path> should be the path for the page that you want to view. If you want to view /posts/one, then you should use &slug=/posts/one.

Your headless CMS might allow you to include a variable in the draft URL so that <path> can be set dynamically based on the CMS’s data like so: &slug=/posts/{entry.fields.slug}

  1. In your Route Handler, check that the secret matches and that the slug parameter exists (if not, the request should fail), call draftMode.enable() to set the cookie. Then, redirect the browser to the path specified by slug:
import { draftMode } from 'next/headers'
import { redirect } from 'next/navigation'
 
export async function GET(request: Request) {
  // Parse query string parameters
  const { searchParams } = new URL(request.url)
  const secret = searchParams.get('secret')
  const slug = searchParams.get('slug')
 
  // Check the secret and next parameters
  // This secret should only be known to this Route Handler and the CMS
  if (secret !== 'MY_SECRET_TOKEN' || !slug) {
    return new Response('Invalid token', { status: 401 })
  }
 
  // Fetch the headless CMS to check if the provided `slug` exists
  // getPostBySlug would implement the required fetching logic to the headless CMS
  const post = await getPostBySlug(slug)
 
  // If the slug doesn't exist prevent draft mode from being enabled
  if (!post) {
    return new Response('Invalid slug', { status: 401 })
  }
 
  // Enable Draft Mode by setting the cookie
  const draft = await draftMode()
  draft.enable()
 
  // Redirect to the path from the fetched post
  // We don't redirect to searchParams.slug as that might lead to open redirect vulnerabilities
  redirect(post.slug)
}

If it succeeds, then the browser will be redirected to the path you want to view with the draft mode cookie.

Step 3: Preview the Draft Content

The next step is to update your page to check the value of draftMode().isEnabled.

If you request a page which has the cookie set, then data will be fetched at request time (instead of at build time).

Furthermore, the value of isEnabled will be true.

// page that fetches data
import { draftMode } from 'next/headers'
 
async function getData() {
  const { isEnabled } = await draftMode()
 
  const url = isEnabled
    ? 'https://draft.example.com'
    : 'https://production.example.com'
 
  const res = await fetch(url)
 
  return res.json()
}
 
export default async function Page() {
  const { title, desc } = await getData()
 
  return (
    <main>
      <h1>{title}</h1>
      <p>{desc}</p>
    </main>
  )
}

If you access the draft Route Handler (with secret and slug) from your headless CMS or manually using the URL, you should now be able to see the draft content. And, if you update your draft without publishing, you should be able to view the draft.

Draft Mode with Cache Components

When using caching directives, Draft Mode forces all cached functions and components to produce fresh results on every request. You do not need to add any special handling to your caching code.

When Draft Mode is enabled:

  • All functions and components under a caching directive scope re-execute on every request instead of serving from cache.
  • Results are not saved to the cache, so draft requests do not pollute cached content.
  • fetch() calls inside cached scopes use the original fetch implementation directly, without the Next.js fetch cache.
  • Response headers are set to private, no-cache, no-store, max-age=0, must-revalidate.

You can read if Draft Mode is enabled inside a caching directive scope, so you can conditionally fetch draft or published content:

import { draftMode } from 'next/headers'
 
async function Post({ slug }: { slug: string }) {
  'use cache'
 
  const { isEnabled } = await draftMode()
 
  // Fetch draft or published content based on draft mode status
  const post = isEnabled
    ? await fetchDraftPost(slug)
    : await fetchPublishedPost(slug)
 
  return (
    <article>
      <h1>{post.title}</h1>
      <div>{post.content}</div>
    </article>
  )
}

Good to know:

  • You can await the draftMode() promise inside a caching directive scope to read isEnabled, but other runtime APIs like cookies() and headers() are not allowed and will throw an error, even when Draft Mode is active. (The "use cache: private" directive does allow access to cookies() and headers().)
  • You cannot call draftMode().enable() or draftMode().disable() inside a caching directive scope. Draft Mode can only be toggled in Route Handlers or Server Actions.

If your cached function needs values from cookies() or headers(), read them outside the caching directive scope and pass them as arguments.

On this page