Destino Dev

Blog

Preview CMS com Next.js App Router: Server Actions, Middleware e ISR na prática

Por John Pires · 14 de março de 2026

Preview CMS com Next.js App Router: Server Actions, Middleware e ISR na prática

Contexto e motivacao

Problema prático que precisamos resolver

A necessidade de um modo de preview em sites que usam headless CMSs surge com frequência: editores querem ver alterações em rascunho (draft) antes de publicar, sem expor drafts para usuários finais e sem sacrificar o desempenho das páginas já em produção. Em um aplicativo Next.js que usa o App Router e ISR, isso envolve três responsabilidades distintas: ativar/desativar o modo preview de forma segura, garantir que o conteúdo de preview seja servido sob demanda quando apropriado, e evitar impacto no cache e nas rotas públicas.

Alternativas e trade-offs

Existem abordagens diferentes: usar a API tradicional de Preview Mode (cookies com token), rotas internas que expõem drafts via query string, ou usar o novo conjunto de utilitários do App Router (draftMode / cookies / revalidatePath). Cada alternativa tem trade-offs sobre segurança, complexidade e compatibilidade com ISR. O objetivo aqui é balancear segurança e simplicidade, aproveitando middleware para roteamento condicional e Server Actions para operações de escrita e revalidação.

Resumo dos requisitos práticos:

  • Ativar preview apenas para editores autenticados.

  • Servir rascunhos sem invalidar o cache público.

  • Permitir revalidação seletiva (ISR) ao publicar.

Ao projetar preview, pense em dois mundos: o público (cacheado, ISR) e o dos editores (session/cookie de preview). A separação clara evita vazamento de conteúdo e problemas de performance.

Como funciona por baixo dos panos

O papel do cookie de preview e do middleware

A técnica mais direta é marcar sessões de preview com um cookie seguro (HttpOnly, SameSite=strict idealmente). O middleware do Next.js, que roda na borda, inspeciona esse cookie em cada requisição e modifica o comportamento — por exemplo, definindo um header interno, reescrevendo para um subpath de preview ou sinalizando ao sistema de rendering para buscar conteúdo draft do CMS. Isso mantém a lógica de roteamento centralizada e performática.

ISR e revalidação: como conciliar com previews

ISR (Incremental Static Regeneration) depende de revalidação quando páginas publicadas mudam. Em Next.js App Router, fetch com a opção next: { revalidate: N } ou funções como revalidatePath/revalidateTag permitem disparar revalidação programaticamente. Quando um editor publica, você precisa revalidar as páginas públicas. Mas durante preview não deve-se forçar a revalidação do cache público — apenas servir conteúdo dinamicamente para o cookie de preview.

Conceito importante: preview = runtime rendering para usuários com cookie de preview; produção = ISR / cache para todos os outros.

Implementacao passo a passo

1) Rota de API para ativar / desativar preview

Crie handlers no App Router que setam/removem cookies de preview. Esses endpoints verificam um token (assinatura) fornecido pelo CMS ou pelo backend de autenticação antes de conceder preview. Exemplo funcional abaixo: cria um cookie seguro e redireciona de volta ao editor.

O código a seguir é o handler em /app/api/preview/route.ts. Ele valida um token simples, seta cookie HttpOnly e redireciona.

// app/api/preview/route.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export async function POST(request: NextRequest) {
  const body = await request.json();
  const { token, slug } = body || {};

  // Contexto: valida um token gerado pelo CMS ou backend
  if (!token || token !== process.env.PREVIEW_TOKEN) {
    return new NextResponse('Invalid token', { status: 401 });
  }

  // O que faz: seta cookie de preview e redireciona para a página
  const url = new URL((slug && `/posts/${slug}`) || '/', request.url);
  const res = NextResponse.redirect(url);
  // Cookie HttpOnly para não expor ao client-side JS
  res.cookies.set({
    name: 'preview_mode',
    value: '1',
    path: '/',
    httpOnly: true,
    sameSite: 'lax',
    secure: process.env.NODE_ENV === 'production',
    maxAge: 60 * 60 // 1 hora
  });

  return res;
}

O que acontece depois: o navegador recebe um cookie seguro. O middleware (abaixo) vai ler esse cookie e ativar comportamento de preview para requisições subsequentes.

2) Middleware que detecta preview e reescreve/rotas

Coloque a lógica de entrega de preview no middleware para interceptar rápido e evitar carregar rotas pesadas. O exemplo mostra como marcar a requisição com um header interno e, opcionalmente, reescrever para uma rota que fará fetch do draft do CMS.

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const preview = request.cookies.get('preview_mode');

  const url = request.nextUrl.clone();

  if (preview) {
    // Marca a req para os handlers a seguir saberem que é preview
    const res = NextResponse.next();
    res.headers.set('x-preview-mode', '1');
    return res;
  }

  // Sem preview: comportamento padrão
  return NextResponse.next();
}

export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico).*)']
};

O que acontece depois: qualquer rota do App Router pode checar o header x-preview-mode (ou ler cookies) para decidir buscar draft ao invés de conteúdo publicado.

3) Pegar conteúdo com ISR e suportar preview

Na página, use fetch com a opção next: { revalidate: N }. Para preview, force fetch direto do CMS (bypass do cache) e entregue o resultado. Exemplo de componente servidor que busca conteúdo, detecta preview e escolhe a fonte.

// app/posts/[slug]/page.tsx
import { cookies } from 'next/headers';

export default async function PostPage({ params }: { params: { slug: string } }) {
  const slug = params.slug;
  const isPreview = !!cookies().get('preview_mode');

  // O que faz: busca do CMS com ISR para público e pull direto para preview
  const url = `${process.env.CMS_URL}/posts/${slug}`;
  const res = await fetch(url, {
    // Revalida a cada 60s para público
    next: { revalidate: 60 }
  });

  let data = await res.json();

  if (isPreview) {
    // Re-fetch for preview to ensure draft content, não usar cache
    const r2 = await fetch(url + '?draft=true', { cache: 'no-store' });
    data = await r2.json();
  }

  return (
    {data.title} 
  );
}

O que acontece depois: visitantes normais recebem versão ISR cacheada; editores com cookie recebem o draft instantaneamente.

4) Server Action para salvar rascunho e revalidar publicacao

Server Actions são ótimas para operações server-only: gravar rascunho no CMS e acionar revalidação das páginas públicas. No processo de publicação, chamamos revalidatePath para forçar ISR a recarregar a versão pública.

// app/(editor)/editor-form.tsx
'use server';
import { revalidatePath } from 'next/cache';

export async function publishPostAction(data: FormData) {
  const slug = data.get('slug')?.toString();
  const body = {
    title: data.get('title'),
    content: data.get('content'),
    status: 'published'
  } as any;

  // O que faz: solicita ao CMS a publicação
  await fetch(`${process.env.CMS_URL}/posts/${slug}/publish`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
    // este fetch roda no servidor, não precisa de next.revalidate
  });

  // O que acontece: revalida a rota pública para servir conteúdo atualizado
  revalidatePath(`/posts/${slug}`);
}

Observação: revalidatePath roda no servidor e agenda a revalidação do cache da rota indicada. Use com parcimônia para não disparar muitas revalidações simultâneas.

Patterns e boas praticas

Autenticação e segurança

Nunca confie somente em um cookie sem validação adequada. O padrão mais seguro é emitir um token de preview assinado no backend (JWT com claims mínimas) e validá-lo no endpoint de preview antes de setar o cookie. Use HttpOnly, SameSite e expiry curto. Evite expor tokens sensíveis ao client JS.

Separação de leitura de conteúdo

Implemente duas rotas de conteúdo no CMS: uma pública (para produção) e outra para drafts. No fetch, use query param ?draft=true ou header específico quando em preview. Isso facilita caching e limites de read scopes no CMS.

Boas práticas adicionais:

  • Cookie HttpOnly para preview; se precisa acessar client-side, use um cookie secundário não-sensível.

  • Centralize a validação do token de preview em um serviço backend — o middleware só atua como encaminhador.

  • Use revalidateTag/revalidatePath para granularidade fina; evite revalidar toda a aplicação.

Fluxo de deploy e cache

Tenha cuidado com deploys que limpam caches; garanta que a revalidação seja compatível com seu CDN. Quando publicar, dispare webhooks para revalidar rotas márgenes (por publicação em lote) em vez de revalidar cada item individualmente.

Armadilhas comuns e como evitar

1) Preview vazando para usuários finais

Erro comum: cookie de preview não ser HttpOnly ou ter escopo muito amplo, fazendo com que links compartilhados contenham token. Evite colocar token em querystrings e use cookies com path fixo e menor tempo de vida.

2) Revalidação excessiva e custos altos

Acionar revalidações em massa ao publicar muitos itens pode sobrecarregar CDN / origin e aumentar custos. Estratégia: agrupe revalidações, use tags e debounce de revalidação para coleções grandes.

Exemplo prático de debounce simples antes de revalidar (pseudo):

// server utility: revalidate-queue.ts (pseudo)
const queue = new Set();
let timeout: NodeJS.Timeout | null = null;
export function scheduleRevalidate(path: string) {
  queue.add(path);
  if (!timeout) {
    timeout = setTimeout(() => {
      for (const p of queue) {
        // chama revalidatePath(p)
      }
      queue.clear();
      timeout = null;
    }, 2000);
  }
}

O que acontece depois: revalidações são agrupadas em um lote curto, reduzindo chamadas redundantes.

3) Server Actions sem efeito no cliente (confusão sobre cookies)

Server Actions rodam no servidor e não podem diretamente modificar cookies do navegador resultantes de uma ação invocada por fetch no servidor. Para ativar preview via fluxo cliente -> server action -> set cookie, prefira expor uma rota API que redirecione para o cliente com cookie setado, ou faça o client fetch direto para a rota que setará o cookie. Documente claramente esse comportamento para a equipe.

Conclusao prática

Preview em Next.js App Router é resolvível com uma combinação de rotas API que setam cookies seguros, middleware que marca requisições de preview e páginas que escolhem entre ISR e fetch no runtime conforme o modo. Use Server Actions para operações server-only (salvar, publicar, revalidar), mas não confunda sua capacidade de manipular cookies do cliente — para isso, prefira redirecionamentos ou rotas que retornem Set-Cookie diretamente.

Checklist final para implementação segura e eficiente:

  • Emitir token de preview assinado no backend.

  • Endpoint que valida token e seta cookie HttpOnly.

  • Middleware que detecta cookie e sinaliza preview.

  • Páginas que buscam draft com cache: 'no-store' quando em preview.

  • Usar next: { revalidate } e revalidatePath para ISR controlado.

Implementando esse padrão você mantém um fluxo de edição seguro para editores, preserva o desempenho com ISR para o público e reduz riscos de vazamento de conteúdo não publicado.

Se for adotar este padrão em produção, valide o fluxo de segurança com auditoria de cookies e monitore as taxas de revalidação para ajustar o debounce/agrupamento conforme a carga real.

Assuntos: Next.js, app-router, isr, preview, middleware, server-actions

Leia também