SEO Programático · Capítulo 19 de 21 · 43 min
Escolher quando cada página nasce no Next.js 16
Três alavancas do framework decidem se a página nasce no build, na primeira visita ou nunca, com efeito direto no custo de hospedagem.
Este capítulo faz parte do curso gratuito SEO Programático. Para marcar como concluído e salvar o progresso, abra este capítulo na página do curso.
Escolher mal o momento em que cada página nasce multiplica o tempo de build e a conta de hospedagem de um acervo com milhares de endereços. O sintoma costuma chegar disfarçado de erro técnico. Você copia um exemplo de 2024, cola no projeto e o resultado desmente o texto. O params reclama de tipo porque virou Promise. A flag experimental.ppr que o tutorial manda ligar não existe mais. O build trava por cerca de 50 segundos e cospe uma mensagem sobre cache durante prerender que não aparece em nenhum lugar do artigo. Nesse ponto o praticante trava numa dúvida improdutiva: o erro é meu ou é a versão?
Quase sempre é a versão. Metade do material público sobre rotas dinâmicas em Next.js foi escrita antes do 16 e usa vocabulário que o framework aposentou. Antes de escrever qualquer linha do Radar, então, vale acertar os nomes das coisas, porque nome errado leva a busca errada, que leva a solução que não compila.
Se a figura ultrapassar a área visível, deslize para os lados. Pelo teclado, foque a figura e use as setas.
Partial Prerendering deixou de ser flag. A documentação do cacheComponents diz que ele "implements Partial Prerendering (PPR) as the default behavior in the App Router" e que a flag experimental.ppr e a configuração de segmento experimental_ppr "have been removed". A configuração é top-level, não vive mais dentro de experimental, e controla ppr, useCache e dynamicIO como uma coisa só, segundo a página oficial do cacheComponents atualizada em 13/05/2026.
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
// top-level. NAO dentro de experimental.
// Liga PPR como comportamento padrao do App Router, mais 'use cache' e dynamicIO.
cacheComponents: true,
};
export default nextConfig;Existe um efeito colateral que pega gente desprevenida e que não tem nada a ver com cache de dados. Com cacheComponents ligado, o Next passa a usar o <Activity> do React para preservar estado de componente durante a navegação client-side: rotas recentes ficam montadas em modo hidden em vez de serem desmontadas. Na prática, um dropdown que você achava que fechava sozinho ao trocar de página continua aberto, um dialog mantém o estado e efeitos que você esperava rodar de novo não rodam. Se você ligar a flag e a navegação passar a se comportar de um jeito estranho, esse é o primeiro lugar para olhar.
Se a figura ultrapassar a área visível, deslize para os lados. Pelo teclado, foque a figura e use as setas.
- Tudo no build.
generateStaticParamsretorna a lista completa. Cada página vira HTML no CDN e a primeira visita é instantânea. O custo é proporcional ao número de páginas e aparece inteiro no seu CI. - Subconjunto no build. Você retorna uma fatia (a documentação exemplifica com
posts.slice(0, 10)) e o resto renderiza sob demanda na primeira visita, ficando cacheado depois. É o modo natural de um catálogo com cobertura desigual, onde poucas páginas concentram a busca. - Nada no build. Retorna array vazio e deixa tudo para a primeira visita.
A regra que vale nos três modos é literal na documentação: "You must always return an array from generateStaticParams, even if it's empty. Otherwise, the route will be dynamically rendered." Esquecer o return não gera erro barulhento, gera uma rota inteira renderizando por request sem que ninguém perceba.
A pegadinha nova do Next 16, e ela derruba o conselho clássico. Com Cache Components ligado, generateStaticParams em rota dinâmica precisa retornar pelo menos um param: "Empty arrays cause a build error". Ou seja, o modo 3 deixa de existir nessa configuração. A documentação sugere contornar com um param placeholder (algo como [{ slug: "__placeholder__" }]) somado a notFound() na página, mas avisa no mesmo parágrafo que isso "prevents build time validation from working effectively and may cause runtime errors". Traduzindo: o placeholder compra o build às custas de perder a validação que justifica o build. Você vai encontrar as duas realidades em material público, porque a maioria dos textos foi escrita quando array vazio era a resposta certa. Confira sempre se o exemplo pressupõe cacheComponents ligado ou desligado antes de copiar.
Vamos resolver o caso do Radar por inteiro antes de pedir qualquer coisa de você. O projeto publica uma página por município e por tipo de combustível, alimentado pela série semanal da ANP cruzada com a base de localidades do IBGE, que tem 5.570 municípios. Gerar a matriz inteira no build é inviável no primeiro deploy, e gerar nada é proibido pela regra acima. Então a rota nasce no modo 2: entra no build o lote priorizado que você montou em [Priorizar páginas sem depender do volume de busca](#keywords-sem-volume-confiavel), e todo o resto fica disponível sob demanda.
// src/app/combustivel/[municipio]/[produto]/page.tsx
import { notFound } from "next/navigation";
import { lotePrioritario, serieSemanal } from "@/lib/radar-anp";
// interruptor do catálogo: true aceita cauda longa sob demanda,
// false devolve 404 para qualquer par fora da lista abaixo.
export const dynamicParams = true;
export async function generateStaticParams() {
// o lote priorizado que você já definiu, e não a matriz inteira.
// fetch é memoizado entre generateStaticParams, generateMetadata e a page,
// então ler a mesma fonte aqui e lá embaixo não dobra a chamada de rede.
const lote = await lotePrioritario();
return lote.map((registro) => ({
municipio: registro.municipioSlug, // ASCII, ex.: "sao-paulo-sp"
produto: registro.produtoSlug, // ASCII, ex.: "gasolina-comum"
}));
}
type Params = { municipio: string; produto: string };
export async function generateMetadata({ params }: { params: Promise<Params> }) {
// params é Promise desde o Next 15. Sem o await, não compila.
const { municipio, produto } = await params;
const serie = await serieSemanal(municipio, produto);
if (!serie) return {};
return {
title: `Preço de ${serie.produtoNome} em ${serie.municipioNome} (${serie.uf})`,
alternates: { canonical: `/combustivel/${municipio}/${produto}` },
};
}
export default async function Page({ params }: { params: Promise<Params> }) {
const { municipio, produto } = await params;
const serie = await serieSemanal(municipio, produto);
// par válido no IBGE mas sem coleta da ANP naquele município: não existe página.
if (!serie) notFound();
return (
<article>
<h1>
Preço de {serie.produtoNome} em {serie.municipioNome}
</h1>
<p>
Média da semana de {serie.semanaReferencia}: R$ {serie.precoMedio.toFixed(2)}
</p>
</article>
);
}O dynamicParams é o interruptor entre dois projetos diferentes. Com export const dynamicParams = false, qualquer caminho fora da lista do generateStaticParams devolve 404, e você tem um catálogo fechado: só existe o que você declarou. Com true, que é o padrão, a rota aceita qualquer par de slugs e renderiza sob demanda, e você tem cauda longa. Note que o padrão é generoso, e generosidade sem controle é como uma matriz combinatória vira doorway: sem o notFound() do exemplo acima, um par que não tem coleta da ANP viraria uma página publicada com um vazio dentro.
O diagnóstico deste repositório mostra o quanto isso passa despercebido. Existe uma única ocorrência de generateStaticParams, em src/app/artigos/[slug]/page.tsx:15, com 152 slugs declarados, e zero ocorrências de dynamicParams no projeto inteiro. Consequência direta: a rota de artigos aceita slug arbitrário por padrão, e é o código de dentro da página que decide o que acontece quando o slug não existe.
Se a figura ultrapassar a área visível, deslize para os lados. Pelo teclado, foque a figura e use as setas.
Quatro fatos versionados que mudam decisão de arquitetura e que raramente aparecem juntos
- Durante revalidação incremental,
generateStaticParamsnão é chamado de novo. A lista de rotas conhecidas é uma foto do build. Página nova no seu banco não vira rota estática sozinha, ela depende dedynamicParamsou de um novo deploy. - A função também funciona em
layout.tsxe em Route Handlers. Emroute.tsisso permite pré-gerar respostas de API no build, o que é útil quando o front consome um JSON por município. - Escopo por arquivo. Um
page.tsxem[category]/[product]pode gerar params para os dois níveis; umlayout.tsxem[category]só gera para o próprio nível. Nunca se gera params abaixo do próprio nível. fetché memoizado automaticamente entre funçõesgenerate*, layouts, pages e Server Components. Ler a mesma URL nogenerateStaticParamse depois na página não dobra a chamada de rede, o que muda o cálculo de quando vale a pena passar dado por argumento.
As três variantes de cache que existem hoje: 'use cache' (padrão, LRU em memória no runtime), 'use cache: remote' (handler dedicado da plataforma, com roundtrip de rede para checar o cache e, segundo a própria documentação, tipicamente com custo de plataforma) e 'use cache: private', para quando não dá para refatorar e tirar o dado de request de dentro do escopo. As restrições que quebram código real: não chame cookies(), headers() nem leia searchParams dentro de escopo cacheado, leia fora e passe como argumento; argumentos e retornos precisam ser serializáveis, com instâncias de URL, funções, Symbols, WeakMap e WeakSet explicitamente recusados; e variáveis capturadas por closure entram na chave de cache automaticamente, o que produz o bug silencioso mais chato do conjunto, o cache que nunca acerta porque a chave muda sem você ver.
// src/lib/radar-anp.ts
import { cacheLife, cacheTag } from "next/cache";
type Serie = {
municipioNome: string;
uf: string;
produtoNome: string;
semanaReferencia: string;
precoMedio: number;
};
export async function serieSemanal(
municipio: string,
produto: string,
): Promise<Serie | null> {
"use cache";
// a ANP publica a série histórica uma vez por semana, na sexta-feira.
// 'days' revalida a cada 1 dia e expira em 1 semana: o dado novo entra
// no máximo um dia depois de publicado e nada fica servido além do ciclo.
cacheLife("days");
// tag por município permite invalidar só o que mudou quando a planilha sai.
cacheTag(`radar-anp:${municipio}`);
const resposta = await fetch(
`https://exemplo.interno/radar/anp/${municipio}/${produto}`,
{ headers: { accept: "application/json" } },
);
if (resposta.status === 404) return null;
if (!resposta.ok) throw new Error(`ANP respondeu ${resposta.status}`);
return (await resposta.json()) as Serie;
}Se a figura ultrapassar a área visível, deslize para os lados. Pelo teclado, foque a figura e use as setas.
A linha do seconds merece atenção porque ela decide arquitetura, e não desempenho. Caches de vida muito curta, com revalidate zero ou expire abaixo de 5 minutos, são automaticamente excluídos do prerender e viram dynamic holes. Isso inclui o perfil seconds inteiro. A diferença entre escolher seconds e escolher days não é o dado ficar mais fresco por alguns minutos: é a página ser HTML estático ou ser uma função que roda a cada request, com o custo e a latência que vêm junto.
O Radar tem um ritmo declarado pela fonte, e a fonte é semanal. Escolher seconds porque "quanto mais fresco melhor" transforma cada página de município em invocação de função para servir um número que só muda na sexta-feira. days revalida uma vez por dia e expira em uma semana, o que cobre o ciclo com folga. weeks revalida a cada semana e expira em 30 dias, mais barato e alinhado à publicação, com o risco de servir o número da semana anterior se a ANP atrasar. Qualquer uma das duas se defende; o que não se defende é escolher sem citar o ciclo do dado.
Duas regras que evitam erro de configuração: expire precisa ser maior que revalidate, senão o Next levanta erro; e o router client impõe mínimo de 30 segundos de stale para que links já prefetchados continuem utilizáveis. Em cache aninhado sem cacheLife explícito no externo, o interno mais curto encurta o externo, e o interno mais longo não estende além do default. Quando o build travar com "Filling a cache during prerender timed out", a leitura é uma só: alguma Promise de dado dinâmico está atravessando a fronteira do use cache. Rode com NEXT_PRIVATE_DEBUG_CACHE=1 para ver qual.
ALAVANCA | ESCOLHA | POR QUE ----------------- | --------------- | ---------------------------------------------------- cacheComponents | true | PPR virou padrao no 16; a flag antiga nem existe mais generateStaticP. | modo 2 (lote) | 5.570 x N nao cabe no build inicial; array vazio e erro dynamicParams | true | cauda longa sob demanda, com notFound() contra vazio cacheLife | days | ANP publica na sexta; 1 dia de revalidate cobre o ciclo cacheTag | por municipio | invalida so o que mudou quando a planilha sai fronteira use cache| sem cookies() | escopo cacheado nao le request; leia fora e passe arg
Sua etapa do projeto. Coloque a rota dinâmica do Radar no ar gerando o lote priorizado de [Priorizar páginas sem depender do volume de busca](#keywords-sem-volume-confiavel), com as quatro alavancas decididas e registradas.
Critérios de aceite
- O lote priorizado entra no build e um par fora do lote responde 200 na primeira visita, provando que
dynamicParamsestá no modo que você escolheu. - Um par que existe no IBGE mas não tem coleta da ANP devolve 404, e não uma página com espaço vazio.
- O perfil de
cacheLifeestá escrito no código com um comentário de uma linha ligando a escolha ao ciclo semanal da fonte. - O tempo de build está medido e anotado, com o número de páginas do lote ao lado. Rode
npm run buildcronometrado, guarde o par (páginas, segundos) e você terá a única base honesta para decidir, no apêndice técnico [Testar o template no pior caso e achar o teto](#performance-e-teto-de-plataforma), quanto do catálogo cabe no build.
Leve quatro perguntas à próxima reunião com o time técnico. (1) Um acervo de 12.000 páginas cujo dado muda uma vez por mês pede qual modo de generateStaticParams, qual valor de dynamicParams e qual perfil de cacheLife, com um argumento para cada escolha? (2) Qual é o preço do contorno com param provisório, agora que array vazio virou erro de build? (3) Com um segmento pai de 300 params e um filho de 8 por pai, quantas vezes o filho executa no build, e quanto isso pesa no tempo de CI? (4) O que muda no artefato servido quando alguém troca days por seconds numa página que já está no ar?
O guia abaixo é para quem contrata ou cobra a equipe que implanta, sem precisar abrir o código. Ele converte as quatro alavancas deste capítulo em entregas verificáveis e destrava uma decisão concreta: quanto do catálogo cabe no build sem estourar prazo de publicação nem conta de hospedagem.
Guia de implementação: contratar a arquitetura de rotas do acervo
Cinco entregas que qualquer executivo confere sem ler uma linha de código.
Passo 1: Peça a ficha das quatro alavancas por rota
Cache Components, modo do generateStaticParams, dynamicParams e perfil de cacheLife, cada um com uma linha de justificativa.
Deu certo quando: Cada escolha cita o ciclo de atualização da fonte, como a publicação semanal da ANP.
Erro comum: Aceitar "deixamos o padrão do framework" como justificativa.
Passo 2: Exija o teste do par sem dado
Um endereço válido na lista de municípios, mas sem coleta, precisa responder 404.
Deu certo quando: O desenvolvedor mostra a resposta 404 ao vivo, no navegador ou no terminal.
Erro comum: Página publicada com um espaço vazio no lugar do dado, que é o padrão de doorway.
Passo 3: Meça o build com o lote piloto
Tempo de build cronometrado, anotado ao lado do número de páginas geradas.
Deu certo quando: O par páginas e segundos está registrado no repositório com data.
Erro comum: Projetar o tempo da matriz inteira sem ter medido nenhum lote.
Passo 4: Confronte o perfil de cache com a conta da plataforma
Perfil curto transforma página estática em função executada, e cobrada, a cada visita.
Deu certo quando: Nenhuma rota de acervo usa o perfil seconds sem justificativa escrita.
Erro comum: Escolher frescor máximo por garantia e pagar execução em toda visita.
Passo 5: Registre a decisão com dono e data de revisão
A ficha entra no registro do comitê trimestral que decide entre escalar, podar ou parar o acervo.
Deu certo quando: Ficha datada, com nome do responsável e data da próxima revisão.
Erro comum: Decisão verbal que desaparece na troca de fornecedor.
No fim você tem: Uma rota de acervo com regime de renderização escolhido por escrito, tempo de build medido e custo de hospedagem previsível.
Quem faz, quanto custa, como conferir
| Etapa | Quem faz | Prazo e esforço | Como conferir |
|---|---|---|---|
| Ficha das quatro alavancas | Desenvolvedor, revisado pelo líder técnico | 2 a 4 horas; sem custo de licença | Uma linha de justificativa por alavanca, citando o ciclo da fonte |
| Teste do par sem dado | Desenvolvedor | 1 hora | Resposta 404 para um par sem coleta, mostrada ao vivo |
| Medição do build | Desenvolvedor | 1 hora por medição; minutos de CI já contratados | Par páginas e segundos anotado no repositório |
| Perfil de cache contra a conta | Líder técnico com quem paga a plataforma | 2 horas | Nenhum perfil seconds em rota de acervo sem justificativa |
| Registro no comitê | Dono do projeto, do marketing ou de produto | 30 minutos | Ficha datada, com responsável e data de revisão |
Guia prático: o pacote de segurança do Next.js de 30/09/2026. A versão estável passou a ser a 16.3.8, e a linha 15.5 recebeu a 15.5.27. Das sete falhas corrigidas, quatro tocam diretamente um acervo programático, porque envolvem o cache de páginas geradas e o controle de quais parâmetros viram página:
- envenenamento de cache em páginas SSG e ISR do Pages Router hospedadas fora da Vercel, que pode servir o conteúdo de outra rota a todos os visitantes até a próxima revalidação;
- envenenamento de cache quando uma rota catch-all na raiz convive com páginas SSG ou ISR, com uma única requisição forjada;
- rotas de imagem como opengraph-image ignorando dynamicParams em build com webpack, o que gera imagem para parâmetros excluídos de propósito do generateStaticParams;
- com Cache Components, conteúdo de rascunho do Draft Mode vazando para páginas publicadas.

Se a figura ultrapassar a área visível, deslize para os lados. Pelo teclado, foque a figura e use as setas.
Atualizar o acervo e provar que nenhuma página serviu conteúdo trocado
Cinco passos para o desenvolvedor, com a conferência final feita pelo analista de SEO. Prioridade alta para quem hospeda o Next.js fora da Vercel.
Passo 1: Descubra a versão em produção
Rode npm ls next no repositório e compare com a versão do último deploy.
Deu certo quando: A versão anotada é 16.3.8 ou 15.5.27, ou existe um chamado aberto para chegar lá.
Erro comum: Conferir o package.json com faixa de versão e supor que a produção já pegou a correção.
Passo 2: Classifique o acervo pelas quatro condições de risco
Hospedagem própria com Pages Router, catch-all na raiz, imagem de compartilhamento por rota com build webpack, Cache Components com Draft Mode.
Deu certo quando: Cada rota do acervo tem uma linha dizendo quais condições se aplicam.
Erro comum: Descartar o risco porque o site usa App Router, sem olhar o catch-all e as rotas de imagem.
Passo 3: Atualize e faça o deploy fora do horário de pico
Um build completo regenera as páginas estáticas e descarta entradas de cache antigas.
Deu certo quando: O deploy novo está no ar e a versão aparece correta no registro do build.
Erro comum: Atualizar só o ambiente de prévia e esquecer a produção.
Passo 4: Teste a rota de imagem com um parâmetro fora da lista
Peça a opengraph-image de um município ou produto que o generateStaticParams exclui com dynamicParams false.
Deu certo quando: A resposta é 404, igual à da página correspondente.
Erro comum: Testar só a página e deixar a rota de imagem gerando conteúdo para parâmetros proibidos.
Passo 5: Confira uma amostra de páginas contra o registro
Sorteie 20 páginas de grupos diferentes e compare título e dado principal com o registro de origem.
Deu certo quando: As 20 páginas mostram o dado da própria rota, sem conteúdo de outra página.
Erro comum: Conferir só a página inicial, que raramente passa pelo cache das rotas do acervo.
No fim você tem: Um acervo na versão corrigida, com prova escrita de que as páginas servem o próprio dado e de que nenhuma imagem nasce fora da lista.
![Página de referência do generateStaticParams na documentação do Next.js, em inglês, marcada como última versão 16.3.8 e atualizada em 25 de agosto de 2026, com a explicação de que a função gera rotas estáticas no build e o exemplo em app/blog/[slug]/page.tsx.](/educacao/seo-programatico/guias/nextjs-generate-static-params.webp)
Se a figura ultrapassar a área visível, deslize para os lados. Pelo teclado, foque a figura e use as setas.
Peça hoje ao time técnico a ficha das quatro alavancas da rota principal do acervo, com entrega em até 14 dias. Na entrega, confira três provas: o par páginas e segundos medido no build, um 404 real para um par sem dado e nenhum perfil de cache mais curto que o ciclo da fonte.
Perguntas frequentes deste capítulo
Liguei cacheComponents e o build passou a reclamar do generateStaticParams de uma rota que eu queria totalmente sob demanda. O que faço?
Meu build trava cerca de 50 segundos e termina com 'Filling a cache during prerender timed out'. Por onde começo?
Publiquei uma página nova no banco e ela não apareceu como rota estática. O deploy não pegou?
Cache Components mudou o comportamento de um dropdown do meu site. Isso é bug do Next?
Vale a pena usar 'use cache: remote' no meu projeto programático?
Seu caderno neste capítulo
Abrir o caderno completoSelecione um trecho do capítulo para destacar ou anotar. Nos vídeos e áudios, use Anotar este momento. No teclado, selecione com Shift e as setas e use Alt+Shift+D para destacar ou Alt+Shift+N para anotar.
Salvo neste navegador. Entre na sua conta para levar o caderno a outros aparelhos.
Entre na sua conta para compartilhar o que aprendeu e convidar alguém para estudar com você.
Conexões deste capítulo
Explore os conceitos e compare abordagens em outros cursos. As conexões indicam assuntos relacionados; a sequência de estudo continua no índice do curso.
Conceitos deste capítulo
O mesmo assunto em outros cursos
- Frontends com VibecodingEscolher a stack e projetar suas dependênciasExaminar conexões de Escolher a stack e projetar suas dependências
- MCP com Chrome: Automação com IADescobrir o que a IA não vê na página que você vêExaminar conexões de Descobrir o que a IA não vê na página que você vê
- Node.js: JavaScript no ServidorIntrodução ao Next.jsExaminar conexões de Introdução ao Next.js
Voltar ao capítulo anterior: Decidir em comitê entre escalar, podar ou parar
Capítulos vizinhos em SEO Programático
- 17Blindar o acervo contra risco jurídico e de marca
- 18Decidir em comitê entre escalar, podar ou parar
- 19Escolher quando cada página nasce no Next.js 16
- 20Anunciar o acervo com sitemap particionado e frescor honesto
- 21Testar o template no pior caso e achar o teto