Capítulo 40 de 456

Draft Mode

Core Idea

Use Draft Mode quando um editor de CMS precisa ver conteúdo não publicado sem esperar revalidação; ele contorna todas as camadas de cache do Next.js só para a requisição do editor, mantendo o cache normal para os demais visitantes.

Key Concepts

  • draftMode(): função de next/headers; retorna objeto com enable(), disable() e isEnabled.
  • Cookie __prerender_bypass: setado por draft.enable(); requisições subsequentes com esse cookie ignoram fetch cache, 'use cache', unstable_cache e o cache de resposta ISR.
  • Cache-Control em Draft Mode: página é servida com private, no-cache, no-store, max-age=0, must-revalidate.
  • Secret token + slug: contrato de segurança do Route Handler de draft, validado contra um token compartilhado e um slug que existe no CMS antes de habilitar o modo.
  • Redirect a partir do dado buscado, não do searchParams: evita open redirect vulnerability (redirecionar para post.slug obtido do CMS, não do parâmetro cru).
  • Draft Mode dentro de 'use cache': isEnabled pode ser lido dentro de um componente 'use cache' para mostrar indicador; o bypass de cache ainda se aplica, então o componente reexecuta a cada request de draft. enable()/disable() NÃO podem ser chamados dentro do escopo de uma diretiva de cache.
  • Forms vs. <Link> para exit: usar <form method="GET"> em vez de <Link> para rotas GET de draft, porque o Next.js faz prefetch de <Link> por padrão, o que limparia o cookie antes do clique; forms nunca são prefetchados.

Code Examples

// app/api/draft/route.ts
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 !== 'MY_SECRET_TOKEN' || !slug) {
    return new Response('Invalid token', { status: 401 })
  }

  const post = await getPostBySlug(slug)
  if (!post) {
    return new Response('Invalid slug', { status: 401 })
  }

  const draft = await draftMode()
  draft.enable()

  redirect(post.slug) // do dado buscado, não de searchParams
}
  • O que demonstra: validação do secret + existência do slug no CMS antes de habilitar draft e redirecionar, prevenindo endpoint público e open redirect.
// app/preview-banner.tsx
import { draftMode } from 'next/headers'
import { redirect } from 'next/navigation'

async function exitPreview() {
  'use server'
  const draft = await draftMode()
  draft.disable()
  redirect('/')
}

export async function PreviewBanner() {
  const { isEnabled } = await draftMode()
  if (!isEnabled) return null

  return (
    <aside role="status">
      Preview mode is on.{' '}
      <form action={exitPreview}>
        <button type="submit">Exit preview</button>
      </form>
    </aside>
  )
}
  • O que demonstra: banner de preview com saída via Server Action (POST implícito do form), padrão mais correto que um GET Route Handler para operação que muda estado.

Anti-patterns

  • Route Handler de draft sem secret: qualquer um que acesse /api/draft habilitaria Draft Mode para si mesmo; sempre validar token compartilhado.
  • Redirecionar usando slug direto de searchParams: abre a porta para open redirect; redirecionar a partir do dado retornado pelo CMS.
  • Sair do Draft Mode via <Link>: prefetch do Next.js dispara a requisição antes do clique, limpando o cookie de forma inesperada.
  • Chamar draftMode().enable()/disable() dentro de escopo 'use cache': não suportado; toggle deve ficar em Route Handler ou Server Action.

Key Takeaways

  1. Draft Mode não muda o código de data fetching se o CMS serve draft e publicado na mesma URL; a diferença já acontece no nível do cache.
  2. O fluxo padrão é: Route Handler valida secret/slug → enable() seta cookie → redirect para o slug → página busca fresh porque cache é ignorado.
  3. Use POST para o exit flow (Server Action ou Route Handler POST); o entry flow usa GET porque o CMS abre a URL numa nova aba.
  4. Quando o CMS usa endpoint de draft separado, ramifique o fetch com base em isEnabled em vez de depender só do bypass de cache.

Connects To

  • Data Security (ch037): o mesmo padrão de validar secret/token antes de agir se aplica a qualquer Route Handler sensível.
  • Deploying to Platforms (ch039): Draft Mode depende do mesmo modelo de fetch cache/ISR descrito na matriz de features daquele capítulo.
  • Forms (ch042): o exit flow do Draft Mode é um exemplo direto de Server Action acionada por <form>.