SEO programático em Next.js 16: metadata, sitemap dinâmico e schema.org
generateMetadata dinâmica, sitemap.ts lendo a REST API, JSON-LD BlogPosting e FAQPage automáticos. SEO técnico no Next.js 16, sem plugins.

TL;DR
SEO técnico em Next.js 16 não exige plugins. O App Router entrega generateMetadata dinâmica, sitemap.ts dinâmico, robots.ts, OG image gerada em runtime e JSON-LD via componente, tudo nativo. A combinação certa, alimentada pela REST API do WordPress, entrega rich results na SERP, sitemap sempre atualizado e metadata por página, sem que o time editorial precise saber o que é schema.org. Este artigo mostra a implementação completa, com tipos TypeScript e exemplos reais.
generateMetadata: o que sai automático
Toda página do App Router pode exportar uma função assíncrona generateMetadata que retorna o objeto Metadata. O Next.js usa esse objeto para gerar todas as tags HTML necessárias: title, description, canonical, Open Graph, Twitter Card, robots.
A função recebe os params da rota e pode buscar dados externos (incluindo a REST API do WordPress) antes de retornar a metadata. Em uma página de post, isso significa metadata personalizada por slug, sem código duplicado:
export async function generateMetadata(
{ params }: { params: Promise<{ slug: string }> }
): Promise<Metadata> {
const { slug } = await params;
const post = await getPostBySlug(slug);
if (!post) return {};
return {
title: stripHtml(post.title.rendered),
description: stripHtml(post.excerpt.rendered).slice(0, 160),
openGraph: {
type: "article",
images: [{ url: getFeaturedImageUrl(post), width: 1200, height: 630 }],
},
alternates: { canonical: `/blog/${slug}` },
};
}
Essa função roda no servidor, durante a geração da página. Como ISR está ativo, ela é executada uma vez a cada 5 minutos por slug, com o resultado cacheado no edge. Performance excelente.
JSON-LD em todo post: BlogPosting + autor + imagem
O Google entende uma página como sendo de blog quando encontra um JSON-LD BlogPosting no head. Esse schema desbloqueia exibição de data, autor e imagem nos resultados de busca, com aparência de “rich result” mais rica que o snippet padrão.
Em sites do nosso portfólio, o JSON-LD é emitido por um componente reutilizável que recebe o post e gera o schema completo:
function BlogPostingSchema({ post }: { post: WPPost }) {
const schema = {
"@context": "https://schema.org",
"@type": "BlogPosting",
headline: stripHtml(post.title.rendered),
image: getFullImageUrl(post),
datePublished: post.date,
dateModified: post.modified,
author: {
"@type": "Organization",
name: "Virtus Design",
url: "https://virtusdesign.com.br",
},
};
return <script type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }} />;
}
Em projetos com autoria nominal por post, o author é populado dinamicamente a partir do _embedded.author da REST API. Em sites institucionais, costuma fazer mais sentido manter o autor como organização.
FAQPage automático a partir do bloco Yoast
Como detalhamos no artigo sobre renderização de blocos Gutenberg, blocos Yoast FAQ são extraíveis do content.rendered via regex. O passo final é transformar essas FAQs em JSON-LD FAQPage, que o Google reconhece como rich result expandível.
O componente faz exatamente o mesmo padrão do BlogPosting, só que condicional. Se o post não tem FAQs, o schema não é emitido:
function FaqSchema({ faqs }: { faqs: Array<{ question: string; answer: string }> }) {
if (faqs.length === 0) return null;
const schema = {
"@context": "https://schema.org",
"@type": "FAQPage",
mainEntity: faqs.map(faq => ({
"@type": "Question",
name: faq.question,
acceptedAnswer: { "@type": "Answer", text: faq.answer },
})),
};
return <script type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }} />;
}
Resultado: o time editorial adiciona um bloco FAQ no post, e o site emite automaticamente o schema correto. Nenhuma decisão técnica passa pelo editor.
BreadcrumbList em listagens e detalhe
Breadcrumbs são úteis para o usuário e para o Google. No HTML eles aparecem como uma trilha de navegação no topo da página (“Blog > Categoria > Post”). No JSON-LD, viram BreadcrumbList, que o Google usa para mostrar a estrutura do site na SERP no lugar da URL bruta.
O componente recebe a lista de items e emite o schema:
function BreadcrumbSchema({ items }: { items: Array<{ name: string; url: string }> }) {
const schema = {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
itemListElement: items.map((item, i) => ({
"@type": "ListItem",
position: i + 1,
name: item.name,
item: item.url,
})),
};
return <script type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }} />;
}
Em sites do nosso portfólio, BreadcrumbList aparece em todas as páginas de detalhe (post, página de categoria, página de produto) e nas listagens principais.
sitemap.ts dinâmico lendo a REST API
O Next.js 16 oferece um arquivo especial app/sitemap.ts que exporta uma função geradora de sitemap. Ela roda no servidor, pode buscar dados externos (a REST API do WordPress) e retorna a lista completa de URLs do site.
O resultado é exposto automaticamente em /sitemap.xml. O Next.js gera o XML válido a partir do array retornado:
// app/sitemap.ts
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = await getPosts(1, 100);
const categories = await getCategories();
const baseUrl = "https://virtusdesign.com.br";
return [
{ url: baseUrl, lastModified: new Date(), changeFrequency: "weekly" },
{ url: `${baseUrl}/blog`, lastModified: new Date(), changeFrequency: "daily" },
...posts.map(p => ({
url: `${baseUrl}/blog/${p.slug}`,
lastModified: p.modified,
changeFrequency: "monthly" as const,
})),
...categories.map(c => ({
url: `${baseUrl}/blog/categoria/${c.slug}`,
changeFrequency: "weekly" as const,
})),
];
}
O sitemap revalida junto com o ISR. Cada 5 minutos (ou o intervalo configurado), o Next.js refaz a chamada à REST API e regenera. Posts novos aparecem no sitemap automaticamente, sem deploy.
Erro mais comum: esquecer de submeter o sitemap no Google Search Console. Mesmo com sitemap.xml acessível, ele só é lido sistematicamente quando submetido. Submissão é uma vez só, depois o Google atualiza por conta própria.
OG image: o detalhe que muda o CTR
Open Graph image é a imagem que aparece quando o link é compartilhado em redes sociais (LinkedIn, X, Facebook, WhatsApp). É também o que aparece em alguns rich results do Google. Uma OG image bem feita aumenta significativamente o CTR (taxa de clique).
O Next.js 16 oferece duas opções:
Imagem estática (mais simples): usar a imagem destacada do post como OG image. É o padrão para a maioria dos sites institucionais. Funciona bem se as imagens destacadas seguem um padrão visual consistente.
OG image dinâmica (mais sofisticada): gerar uma imagem em runtime com título do post, autor e branding. O Next.js permite isso via opengraph-image.tsx com a API ImageResponse. Útil em blogs com volume alto e padrão visual definido.
Em sites do nosso portfólio, começamos sempre com imagem estática (a destacada do post) e migramos para dinâmica quando o volume justifica o investimento de design.
Search Console: o que monitorar
Implementar SEO programático sem monitoramento é otimizar no escuro. O Google Search Console é a ferramenta gratuita que mostra como o Google enxerga o site:
Checklist: o que checar semanalmente
-
Cobertura: quantas páginas estão indexadas vs submetidas no sitemap. Discrepância grande indica problemas de qualidade ou erros de canonical
-
Performance: cliques, impressões, CTR e posição média por página e por keyword. Tendências de queda exigem investigação imediata
-
Core Web Vitals: dados CrUX em tempo quase real, segmentados por dispositivo e por URL
-
Rich Results: validação de schema.org. Se o seu BlogPosting ou FAQPage tem erro, aparece aqui antes de afetar tráfego
-
Sitemaps: verificar que o sitemap.xml foi lido recentemente e quantas URLs foram processadas
Configurar alertas no Search Console (via integração com email) avisa quando há queda brusca de tráfego ou erros novos de cobertura.
robots.ts: o detalhe esquecido
O Next.js 16 também tem um arquivo app/robots.ts que exporta o robots.txt do site. Em projetos institucionais, o conteúdo é simples mas precisa estar correto:
// app/robots.ts
export default function robots(): MetadataRoute.Robots {
return {
rules: [{ userAgent: "*", allow: "/", disallow: ["/api/", "/admin/"] }],
sitemap: "https://virtusdesign.com.br/sitemap.xml",
};
}
O ponto crítico é apontar o sitemap. Sem isso, alguns crawlers não encontram a lista completa de URLs e indexam o site parcialmente.
Próximos passos
SEO programático no Next.js 16 entrega um site institucional totalmente otimizado, sem plugins, sem dependências externas e sem que a equipe editorial precise se preocupar com schema.org. É a última peça de uma série que começou na visão estratégica, passou pela arquitetura técnica, pela renderização de blocos Gutenberg e pela otimização de Core Web Vitals.
Aplicar este padrão no seu site requer atenção a detalhes mas não exige tooling complexo. O Next.js 16 entrega tudo nativamente; o que falta é o método para combinar as peças com consistência. Se você quer implementar este modelo no seu projeto, fale com a Virtus Design.
Conheça também nossa ferramenta gratuita de auditoria SEO, que avalia 9 categorias de SEO técnico em 30 segundos, e o portfólio completo de serviços em desenvolvimento, design e SEO técnico.

