Guia completo de SEO em Next.js: Metadata API + dados estruturados na prática

No painel de estatísticas do Google Search Console, aquele enorme “0” ocupava a tela. O produto já estava no ar havia 23 dias. Dois meses de desenvolvimento, incontáveis madrugadas, uma UI cuidadosamente polida e uma experiência de usuário fluida pareciam não significar nada diante daquele “0” gelado.
O mais frustrante era compartilhar o link no Twitter e ver apenas uma área de prévia em branco. Nem uma imagem de capa decente aparecia.
“Mas o Next.js não já vem com SSR? Por que o SEO continua tão ruim?” Depois de vasculhar a documentação oficial, percebi uma verdade dura: SSR não significa SEO amigável. Se suas meta tags estão erradas, se os dados estruturados não foram configurados, se você não entende Open Graph, o mecanismo de busca ainda pode tratar sua página como ar.
Talvez você também já tenha sentido essa dor: um produto construído com esforço, mas que ninguém encontra na busca, e que parece pouco profissional quando compartilhado. Aí só resta pagar anúncios. Só que, na prática, dominar a Metadata API do Next.js 15 e algumas configurações-chave já resolve boa parte desses problemas.
Este artigo vai te mostrar, passo a passo, como usar a Metadata API para cada página ter meta tags únicas, como configurar dados estruturados para tornar os resultados de busca mais atraentes e como criar uma prévia perfeita para compartilhamento social. Mais importante: vou apontar 5 erros comuns de SEO para você evitar os mesmos buracos em que eu já caí.
Por que o SEO do seu site Next.js é ruim?
SSR não significa SEO amigável
Para ser sincero, no começo eu também pensava assim. Estou usando Next.js, tenho renderização no servidor, o HTML vai direto para os crawlers. Então o SEO não deveria estar perfeito?
Ingenuidade.
Depois olhei o código-fonte do projeto de um amigo, abri as ferramentas de desenvolvimento do navegador e inspecionei o HTML. Todas as páginas tinham o mesmo <title>: “My App”. O <meta name="description"> não existia ou era idêntico em todas. É como abrir uma loja excelente, mas deixar a placa da entrada escrita apenas “Loja”. O cliente não faz ideia do que você vende.
O Next.js realmente oferece SSR, mas configurar meta tags continua sendo responsabilidade sua. Sem essa configuração, o que chega ao mecanismo de busca é só uma casca de HTML, e ele não entende sobre o que a página fala.
5 erros fatais de SEO
Vi muitos desenvolvedores caírem nesses erros, inclusive eu.
1. Todas as páginas compartilham o mesmo title e description
É o erro mais comum. Ou o projeto deixa um title fixo em _document.tsx, ou simplesmente não escreve nada. O resultado: quando o Google encontra a home, a página “sobre” e a página de produto, todas aparecem com o mesmo título e a mesma descrição.
Imagine entrar em uma livraria e ver todos os livros com o mesmo nome na capa. Você compraria?
2. Esquecer o canonical URL e gerar conteúdo duplicado
Esse erro é especialmente discreto. Seu site pode ter paginação (?page=2), filtros (?category=tech) ou ordenação (?sort=date). Na prática, tudo isso pode ser apenas visões diferentes da mesma página, mas o mecanismo de busca pode interpretar como páginas separadas e penalizar você por “conteúdo duplicado”.
Um blog de um cliente meu perdeu 40% do tráfego por causa disso. Depois de adicionar canonical URL, voltou ao normal em duas semanas.
3. Não usar dados estruturados e perder rich snippets
Você já reparou que, ao buscar “receita de torta de maçã”, alguns resultados mostram avaliação, tempo de preparo e calorias direto na página de busca? O Google não adivinhou isso. O site contou ao Google por meio de dados estruturados, em JSON-LD.
Segundo estudos, sites que implementam dados estruturados podem ter aumento médio de 20% a 30% na taxa de cliques. Na prática, é como ganhar um terço a mais de tráfego adicionando algumas linhas de código.
4. Imagens sem atributo alt, desperdiçando tráfego da busca por imagens
Muitos desenvolvedores acham que o atributo alt serve apenas para pessoas com deficiência visual e não tem relação com SEO. Errado.
A busca de imagens do Google é uma enorme porta de entrada de tráfego. Se suas imagens têm descrições alt claras, elas podem ranquear na busca por imagens. Já vi um site de materiais de design receber 30% do tráfego a partir do Google Imagens porque escrevia o alt de cada imagem com cuidado.
5. Não ter sitemap.xml e robots.txt
Esses dois arquivos dizem aos mecanismos de busca quais páginas do seu site podem ser rastreadas e quais não podem. Sem sitemap, o Google pode levar meses para descobrir um artigo novo. Com sitemap enviado ao Search Console, uma página nova pode ser indexada em poucos dias.
E o Next.js App Router hoje já permite gerar sitemap.ts e robots.ts automaticamente. Nem precisa escrever tudo à mão. Então por que deixar sem configurar?
Dominando a Metadata API no Next.js 15
A Metadata API do Next.js 15 é um dos maiores presentes do framework para SEO. Antes, você precisava escrever <Head> manualmente em cada página. Agora basta exportar um objeto ou uma função, e o Next.js cuida do restante.
Metadata estática: ideal para páginas de conteúdo fixo
O caso mais simples: sua página “Sobre nós” ou sua página de política de privacidade quase nunca muda. Basta exportar um objeto metadata em page.tsx:
// app/about/page.tsx
import { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Sobre nós - TechBlog',
description: 'Somos um grupo de desenvolvedores apaixonados por tecnologia, compartilhando experiências práticas de frontend, backend e DevOps.',
keywords: ['blog de tecnologia', 'desenvolvimento frontend', 'Next.js', 'React'],
authors: [{ name: 'João' }],
openGraph: {
title: 'Sobre nós - TechBlog',
description: 'Blog de tecnologia com experiências práticas de desenvolvimento',
url: 'https://yourdomain.com/about',
siteName: 'TechBlog',
images: [
{
url: 'https://yourdomain.com/og-about.jpg',
width: 1200,
height: 630,
}
],
type: 'website',
},
twitter: {
card: 'summary_large_image',
title: 'Sobre nós - TechBlog',
description: 'Blog de tecnologia com experiências práticas de desenvolvimento',
images: ['https://yourdomain.com/og-about.jpg'],
},
}
export default function AboutPage() {
return <div>Conteúdo sobre nós...</div>
}
Percebe? Segurança de tipos, autocompletar no IDE e menos risco de erro de digitação. Além disso, o Next.js remove duplicações automaticamente. Se mais de um lugar definir a mesma meta tag, ele faz a mesclagem de forma inteligente.
Boas práticas:
- Mantenha
titlecom até 60 caracteres, porque textos maiores podem ser cortados no resultado de busca - Mantenha
descriptioncom 150 a 160 caracteres, o tamanho ideal para o resumo do Google - Use
openGraph.imagescom 1200x630 pixels, dimensão que funciona bem no Twitter, Facebook e LinkedIn
Metadata dinâmica: a salvação de blogs e páginas de produto
O recurso realmente poderoso é a função generateMetadata. Em uma página de post, por exemplo, cada artigo tem título e descrição diferentes. Você não vai escrever tudo manualmente.
// app/blog/[slug]/page.tsx
import { Metadata } from 'next'
import { getPostBySlug } from '@/lib/posts'
export async function generateMetadata(
{ params }: { params: { slug: string } }
): Promise<Metadata> {
// Busca os dados do artigo no banco ou CMS
const post = await getPostBySlug(params.slug)
return {
title: `${post.title} - TechBlog`,
description: post.excerpt,
authors: [{ name: post.author }],
openGraph: {
title: post.title,
description: post.excerpt,
images: [post.coverImage],
type: 'article',
publishedTime: post.publishedAt,
authors: [post.author],
},
twitter: {
card: 'summary_large_image',
title: post.title,
description: post.excerpt,
images: [post.coverImage],
},
}
}
export default async function BlogPostPage({ params }: { params: { slug: string } }) {
const post = await getPostBySlug(params.slug)
return <article>{post.content}</article>
}
Assim, cada artigo passa a ter informações de SEO próprias. Quando o Google rastreia a página, ele enxerga o HTML completo, sem depender de JavaScript no cliente.
Técnica essencial: metadataBase
Reparou que, no código acima, as URLs das imagens são absolutas? Se suas imagens usam caminhos relativos, como /images/cover.jpg, você precisa configurar metadataBase no root layout:
// app/layout.tsx
export const metadata: Metadata = {
metadataBase: new URL('https://yourdomain.com'),
}
Depois disso, todos os caminhos relativos são automaticamente transformados em URLs completas. Sem isso, o Open Graph pode falhar ao buscar a imagem, e o compartilhamento social fica sem capa.
Sistema de template: padronize o título de todas as páginas
Você já notou que muitos sites usam títulos no formato “Nome da página | Nome do site”? Por exemplo, “Home | TechBlog” ou “Sobre nós | TechBlog”.
Escrever esse sufixo manualmente em todas as páginas é cansativo. Com title.template, fica simples:
// app/layout.tsx (root layout)
export const metadata: Metadata = {
title: {
template: '%s | TechBlog',
default: 'TechBlog - Blog de tecnologia',
},
description: 'Blog de tecnologia com experiências práticas de frontend, backend e DevOps',
metadataBase: new URL('https://yourdomain.com'),
}
Então uma subpágina só precisa declarar o nome:
// app/about/page.tsx
export const metadata: Metadata = {
title: 'Sobre nós', // Renderiza como "Sobre nós | TechBlog"
}
Não quer sufixo na home? Use title.absolute:
// app/page.tsx
export const metadata: Metadata = {
title: {
absolute: 'TechBlog - Página inicial do blog de tecnologia', // Não aplica o template
},
}
Esse mecanismo mantém os títulos consistentes e facilita mudanças. Basta alterar o template no root layout, e todas as páginas seguem o novo formato.
Dados estruturados (Schema.org) para se destacar
O que são dados estruturados e por que importam tanto?
Você já pesquisou “como fazer torta de maçã”?
Observe os resultados. Algumas receitas mostram avaliação, como 4,8 estrelas, tempo de preparo, como 45 minutos, calorias, como 320 kcal, e até uma prévia dos passos. Outras mostram apenas título e descrição.
A diferença está nos dados estruturados.
Dados estruturados são um formato padrão, geralmente JSON-LD, usado para dizer aos mecanismos de busca: “isto é um post de blog, o autor é XXX, foi publicado em XXX” ou “isto é um produto, custa XXX e tem avaliação XXX”. Com essas informações, o mecanismo de busca consegue exibir rich snippets, aqueles cartões com avaliações, preços e informações de autor.
Os números são claros: sites que implementam dados estruturados podem ter aumento médio de 20% a 30% na taxa de cliques. É como ganhar um terço de tráfego com custo quase zero.
Tipos comuns de Schema
O Schema.org define centenas de tipos, mas para a maioria dos sites estes são os mais úteis:
- Organization - informações de empresa ou organização, geralmente na home
- BlogPosting - posts de blog, um por artigo
- Product - informações de produto, essencial para e-commerce, incluindo preço, avaliação e estoque
- FAQPage - perguntas frequentes, que podem aparecer expandidas nos resultados
- LocalBusiness - negócios locais, como restaurantes e salões, com endereço, horário e telefone
Vamos focar nos dois primeiros, porque são os mais importantes para blogs e sites corporativos.
Implementando JSON-LD no Next.js
O componente <Script> do Next.js 15 torna a implementação de dados estruturados bem simples. Eu costumo criar um componente genérico:
// components/StructuredData.tsx
import Script from 'next/script'
type StructuredDataProps = {
data: object
}
export default function StructuredData({ data }: StructuredDataProps) {
return (
<Script
id="structured-data"
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
/>
)
}
Depois, é só usar na página:
Exemplo 1: Organization (informações da empresa)
// app/layout.tsx (root layout)
import StructuredData from '@/components/StructuredData'
const organizationData = {
'@context': 'https://schema.org',
'@type': 'Organization',
name: 'TechBlog',
url: 'https://yourdomain.com',
logo: 'https://yourdomain.com/logo.png',
sameAs: [
'https://twitter.com/yourusername',
'https://github.com/yourcompany',
'https://linkedin.com/company/yourcompany',
],
contactPoint: {
'@type': 'ContactPoint',
email: '[email protected]',
contactType: 'Customer Service',
},
}
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
{children}
<StructuredData data={organizationData} />
</body>
</html>
)
}
Exemplo 2: BlogPosting (post de blog)
// app/blog/[slug]/page.tsx
import StructuredData from '@/components/StructuredData'
import { getPostBySlug } from '@/lib/posts'
export default async function BlogPostPage({ params }: { params: { slug: string } }) {
const post = await getPostBySlug(params.slug)
const articleData = {
'@context': 'https://schema.org',
'@type': 'BlogPosting',
headline: post.title,
description: post.excerpt,
image: post.coverImage,
author: {
'@type': 'Person',
name: post.author,
url: `https://yourdomain.com/author/${post.authorSlug}`,
},
publisher: {
'@type': 'Organization',
name: 'TechBlog',
logo: {
'@type': 'ImageObject',
url: 'https://yourdomain.com/logo.png',
},
},
datePublished: post.publishedAt,
dateModified: post.updatedAt,
mainEntityOfPage: {
'@type': 'WebPage',
'@id': `https://yourdomain.com/blog/${post.slug}`,
},
}
return (
<>
<article>{post.content}</article>
<StructuredData data={articleData} />
</>
)
}
Parece bastante código, mas no fundo é só expor as informações do artigo em um formato padronizado para o Google. Configurou uma vez, depois vira reaproveitamento.
Valide seus dados estruturados
Depois de configurar, como saber se está certo? Use estas duas ferramentas:
-
Google Rich Results Test (https://search.google.com/test/rich-results)
- Insira a URL da página, e o Google mostra quais rich results ela pode exibir
- Se houver erro, a ferramenta indica exatamente onde está o problema
-
Schema Markup Validator (https://validator.schema.org/)
- Verifica se o JSON-LD segue o padrão Schema.org
- É mais rigoroso do que a ferramenta do Google; vale testar nas duas
O erro mais comum que já vi é esquecer publisher, obrigatório para BlogPosting, ou usar URL de imagem relativa. A ferramenta aponta o que está faltando; você corrige e testa de novo.
Open Graph e Twitter Cards na prática
Por que o compartilhamento social importa tanto?
Você já compartilhou um artigo no Twitter ou Facebook e viu a prévia aparecer em branco, ou com uma imagem totalmente errada, como o logo do site ou um elemento decorativo aleatório?
Isso passa uma sensação pouco profissional. Já sites bem configurados exibem automaticamente uma imagem de capa bonita, título e descrição. A taxa de cliques pode ser 2 a 3 vezes maior.
Configurar Open Graph e Twitter Cards serve exatamente para controlar como o seu link aparece nas redes sociais.
Entendendo o protocolo Open Graph
Open Graph foi criado originalmente pelo Facebook, mas hoje é aceito por Twitter, LinkedIn, Slack, Discord e quase todas as plataformas principais.
Quando falamos da Metadata API, já configuramos o objeto openGraph. Agora vamos olhar os campos principais com mais detalhe:
export const metadata: Metadata = {
openGraph: {
// Campos obrigatórios
title: 'Título do artigo', // Título exibido nas redes sociais
description: 'Resumo do artigo', // Descrição, cerca de 150 caracteres
url: 'https://yourdomain.com/article', // URL deste conteúdo
siteName: 'TechBlog', // Nome do site
// Imagem, o ponto mais importante
images: [
{
url: 'https://yourdomain.com/og-image.jpg',
width: 1200,
height: 630, // Tamanho recomendado: 1200x630
alt: 'Descrição da imagem, para acessibilidade e SEO',
},
],
// Tipo de conteúdo
type: 'article', // Use article para posts; website para outras páginas
// Campos específicos de artigo, quando type é article
publishedTime: '2025-01-15T08:00:00.000Z',
modifiedTime: '2025-01-16T10:30:00.000Z',
authors: ['João', 'Maria'],
tags: ['Next.js', 'SEO', 'desenvolvimento frontend'],
// Localização, se houver versões em vários idiomas
locale: 'pt_BR',
alternateLocale: ['en_US', 'ja_JP'],
},
}
Ponto-chave: tamanho da imagem
1200x630 pixels é a proporção de ouro, 1.91:1, e funciona bem em todas as plataformas:
- Facebook e LinkedIn: exibem a imagem completa
- Twitter: pode cortar para 2:1, mas continua aceitável
- Slack e Discord: também funciona bem
O arquivo de imagem não deve passar de 8 MB, ou a build pode falhar.
Configurando Twitter Cards
O Twitter tem seu próprio sistema de meta tags. Embora possa usar Open Graph como fallback, é melhor configurar separadamente:
export const metadata: Metadata = {
twitter: {
card: 'summary_large_image', // Modo com imagem grande, recomendado
site: '@yourusername', // Conta do Twitter do site
creator: '@authorusername', // Conta do Twitter do autor
title: 'Título do artigo',
description: 'Resumo do artigo',
images: ['https://yourdomain.com/twitter-image.jpg'],
},
}
O campo card aceita dois valores:
summary: modo com imagem pequena, à esquerda, e texto à direitasummary_large_image: modo com imagem grande, ocupando a parte superior do cartão; recomendo este
Limite de imagem no Twitter: o arquivo não pode passar de 5 MB, mais restrito do que OG.
Gerando imagens sociais dinamicamente (avançado)
Criar manualmente uma capa 1200x630 para cada artigo? Cansa rápido.
Desde o Next.js 13.3, é possível gerar imagens OG dinamicamente com código, uma ótima opção para blogs:
// app/blog/[slug]/opengraph-image.tsx
import { ImageResponse } from 'next/og'
import { getPostBySlug } from '@/lib/posts'
export const runtime = 'edge'
export const alt = 'Blog post cover'
export const size = {
width: 1200,
height: 630,
}
export const contentType = 'image/png'
export default async function Image({ params }: { params: { slug: string } }) {
const post = await getPostBySlug(params.slug)
return new ImageResponse(
(
<div
style={{
fontSize: 60,
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
color: 'white',
padding: '80px',
}}
>
<h1 style={{ fontSize: 72, fontWeight: 'bold', textAlign: 'center' }}>
{post.title}
</h1>
<p style={{ fontSize: 36, marginTop: 20, opacity: 0.9 }}>
by {post.author}
</p>
</div>
),
{
...size,
}
)
}
Assim, a imagem de compartilhamento de cada artigo é gerada dinamicamente com base no título. Não precisa desenhar tudo à mão.
Se você quiser usar fontes personalizadas ou imagens de fundo, dá para customizar mais. Para detalhes, consulte a parte next/og da documentação oficial do Next.js.
Testando e validando o compartilhamento social
Depois de configurar, não publique no escuro. Teste primeiro com estas ferramentas:
-
Facebook Sharing Debugger (https://developers.facebook.com/tools/debug/)
- Insira a URL e veja como o Facebook rastreia e exibe o conteúdo
- Atenção: o Facebook armazena informações OG em cache. Se você mudou o código, clique em “Scrape Again” nessa ferramenta para atualizar
-
Twitter Card Validator (https://cards-dev.twitter.com/validator)
- Insira a URL e visualize o Twitter Card
- Observação: depois de 2023, essa ferramenta passou a exigir conta de desenvolvedor do Twitter, mas você também pode testar publicando um tweet
-
LinkedIn Post Inspector (https://www.linkedin.com/post-inspector/)
- Ferramenta de prévia do LinkedIn, também útil para atualizar cache
Um erro que já cometi: depois de trocar a imagem OG, o Facebook continuava mostrando a imagem antiga. É preciso atualizar o cache no Sharing Debugger; senão você começa a duvidar da própria vida.
Outras configurações indispensáveis de SEO
Metadata API, dados estruturados e Open Graph são o núcleo do SEO, mas há outras configurações igualmente importantes. Não deixe passar.
sitemap.xml: ajude os mecanismos de busca a conhecer suas páginas
Sitemap é um arquivo XML que lista todas as URLs do seu site. Os crawlers do Google e do Bing leem esse arquivo para entender a estrutura do site e indexar mais rápido.
O Next.js App Router torna isso muito simples. Você só precisa criar app/sitemap.ts:
// app/sitemap.ts
import { MetadataRoute } from 'next'
import { getAllPosts } from '@/lib/posts'
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = await getAllPosts()
const baseUrl = 'https://yourdomain.com'
// Páginas estáticas
const staticPages: MetadataRoute.Sitemap = [
{
url: baseUrl,
lastModified: new Date(),
changeFrequency: 'daily',
priority: 1,
},
{
url: `${baseUrl}/about`,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.8,
},
]
// Gera páginas de posts dinamicamente
const blogPages: MetadataRoute.Sitemap = posts.map((post) => ({
url: `${baseUrl}/blog/${post.slug}`,
lastModified: new Date(post.updatedAt),
changeFrequency: 'weekly' as const,
priority: 0.7,
}))
return [...staticPages, ...blogPages]
}
O Next.js gera automaticamente esse arquivo em https://yourdomain.com/sitemap.xml.
Depois de configurar, lembre-se de fazer duas coisas:
- Enviar ao Google Search Console (https://search.google.com/search-console)
- Enviar ao Bing Webmaster Tools (https://www.bing.com/webmasters)
Depois do envio, a velocidade de indexação de novas páginas pode melhorar mais de 50%. Antes, quando eu publicava posts sem enviar sitemap, um artigo novo podia levar duas semanas para ser indexado. Depois do envio, em três dias já aparecia no Google.
robots.txt: controle o acesso dos crawlers
robots.txt diz aos crawlers dos mecanismos de busca quais páginas podem ser rastreadas e quais não podem.
Da mesma forma, o Next.js App Router permite gerar esse arquivo com código:
// app/robots.ts
import { MetadataRoute } from 'next'
export default function robots(): MetadataRoute.Robots {
return {
rules: [
{
userAgent: '*', // Aplica para todos os crawlers
allow: '/', // Permite rastrear todas as páginas
disallow: ['/admin', '/api', '/private'], // Bloqueia estes caminhos
},
],
sitemap: 'https://yourdomain.com/sitemap.xml', // Aponta para o sitemap
}
}
Isso gera https://yourdomain.com/robots.txt, com conteúdo parecido com:
User-agent: *
Allow: /
Disallow: /admin
Disallow: /api
Disallow: /private
Sitemap: https://yourdomain.com/sitemap.xml
Cenários comuns:
- Páginas administrativas, como
/admin, claramente não devem ser rastreadas - Rotas de API, como
/api, também não precisam ser rastreadas - Rascunhos e páginas de prévia podem usar uma meta tag
noindexou uma regraDisallow
canonical URL: evite penalização por conteúdo duplicado
Canonical URL diz ao mecanismo de busca: “esta página pode ter várias URLs, mas esta é a versão canônica”.
Cenários típicos:
- Paginação:
/blog?page=1,/blog?page=2 - Filtros:
/products?category=tech - Ordenação:
/products?sort=price
Na prática, muitas vezes são visões diferentes da mesma página. Sem declarar canonical, o mecanismo de busca pode entender como conteúdo duplicado e dividir autoridade entre URLs.
No Next.js, configure canonical assim:
// app/blog/page.tsx
export const metadata: Metadata = {
alternates: {
canonical: 'https://yourdomain.com/blog',
},
}
Ou gere dinamicamente:
// app/blog/[slug]/page.tsx
export async function generateMetadata({ params }: { params: { slug: string } }): Promise<Metadata> {
return {
alternates: {
canonical: `https://yourdomain.com/blog/${params.slug}`,
},
}
}
Se sua página tem versões em vários idiomas, também use alternates.languages para informar o Google:
export const metadata: Metadata = {
alternates: {
canonical: 'https://yourdomain.com/blog/nextjs-seo',
languages: {
'en-US': 'https://yourdomain.com/en/blog/nextjs-seo',
'ja-JP': 'https://yourdomain.com/ja/blog/nextjs-seo',
},
},
}
Otimização de imagens: next/image + atributo alt
Muitos desenvolvedores ignoram SEO de imagem, mas o Google Imagens é uma fonte enorme de tráfego.
Dois pontos essenciais:
- Use o componente
next/imageem vez da tagimgnativa
import Image from 'next/image'
<Image
src="/cover.jpg"
alt="Capa do guia completo de SEO em Next.js"
width={1200}
height={630}
priority // Use para imagens acima da dobra; elas carregam com prioridade
/>
next/image aplica automaticamente estas otimizações:
- Lazy loading, adiando imagens fora da primeira dobra
- Conversão para WebP, reduzindo o tamanho do arquivo
- Dimensões responsivas, carregando o tamanho adequado para cada dispositivo
- Prevenção de CLS, o deslocamento cumulativo de layout, uma métrica de Core Web Vitals
- Escreva sempre o atributo
alt
alt não é opcional. Ele atende tanto a SEO quanto a acessibilidade, porque leitores de tela leem o texto alternativo.
Bons exemplos de alt:
- ✅ “Exemplo de código para configurar a Metadata API do Next.js”
- ✅ “Exibição de rich snippet de um artigo nos resultados do Google”
Exemplos ruins de alt:
- ❌ “Imagem”
- ❌ “screenshot.png”
- ❌ Não escrever alt
O Google Imagens usa o conteúdo de alt para ranquear imagens. Já vi um site de materiais de design receber 30% do tráfego da busca por imagens justamente porque escrevia o alt de cada imagem com atenção.
Caso prático: configuração completa de SEO para um blog
Depois de tanta teoria e tantos trechos de código, vamos juntar tudo e ver como um projeto completo de blog deveria configurar SEO.
Estrutura do projeto
Imagine que estamos criando um blog técnico com Next.js 15 App Router. A estrutura de diretórios seria parecida com esta:
app/
├── layout.tsx # Root layout - configuração global
├── page.tsx # Home
├── about/page.tsx # Página sobre
├── blog/
│ ├── page.tsx # Lista de posts
│ └── [slug]/
│ ├── page.tsx # Página de detalhe do post
│ └── opengraph-image.tsx # Geração dinâmica de imagem OG, opcional
├── sitemap.ts # Geração de sitemap
└── robots.ts # Geração de robots.txt
Código completo de exemplo
1. Root Layout - configuração global de SEO
// app/layout.tsx
import { Metadata } from 'next'
import StructuredData from '@/components/StructuredData'
export const metadata: Metadata = {
metadataBase: new URL('https://yourdomain.com'), // Obrigatório para montar caminhos relativos
title: {
template: '%s | TechBlog', // Template de título para subpáginas
default: 'TechBlog - Blog técnico de desenvolvimento frontend',
},
description: 'Blog técnico com experiências práticas de Next.js, React e TypeScript',
keywords: ['Next.js', 'React', 'TypeScript', 'desenvolvimento frontend', 'blog de tecnologia'],
authors: [{ name: 'João', url: 'https://yourdomain.com/about' }],
openGraph: {
type: 'website',
siteName: 'TechBlog',
locale: 'pt_BR',
},
twitter: {
card: 'summary_large_image',
site: '@yourusername',
},
}
// Dados estruturados de Organization, globais no root layout
const organizationData = {
'@context': 'https://schema.org',
'@type': 'Organization',
name: 'TechBlog',
url: 'https://yourdomain.com',
logo: 'https://yourdomain.com/logo.png',
sameAs: [
'https://twitter.com/yourusername',
'https://github.com/yourcompany',
],
}
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="pt-BR">
<body>
{children}
<StructuredData data={organizationData} />
</body>
</html>
)
}
2. Home - metadata estática
// app/page.tsx
import { Metadata } from 'next'
export const metadata: Metadata = {
title: {
absolute: 'TechBlog - Blog técnico de desenvolvimento frontend', // Não aplica template
},
description: 'Experiências práticas com Next.js, React, TypeScript e outras tecnologias frontend para ajudar desenvolvedores a evoluir',
openGraph: {
title: 'TechBlog - Blog técnico de desenvolvimento frontend',
description: 'Experiências práticas de tecnologia frontend',
url: 'https://yourdomain.com',
images: [
{
url: 'https://yourdomain.com/og-home.jpg',
width: 1200,
height: 630,
alt: 'Capa da home do TechBlog',
},
],
},
}
export default function HomePage() {
return <div>Conteúdo da home...</div>
}
3. Página de detalhe do post - metadata dinâmica + dados estruturados
// app/blog/[slug]/page.tsx
import { Metadata } from 'next'
import { notFound } from 'next/navigation'
import { getPostBySlug } from '@/lib/posts'
import StructuredData from '@/components/StructuredData'
// Gera metadata dinamicamente
export async function generateMetadata(
{ params }: { params: { slug: string } }
): Promise<Metadata> {
const post = await getPostBySlug(params.slug)
if (!post) return {}
return {
title: post.title, // Aplica o template, virando "Título do artigo | TechBlog"
description: post.excerpt,
keywords: post.tags,
authors: [{ name: post.author }],
openGraph: {
title: post.title,
description: post.excerpt,
url: `https://yourdomain.com/blog/${post.slug}`,
images: [post.coverImage],
type: 'article',
publishedTime: post.publishedAt,
authors: [post.author],
},
twitter: {
card: 'summary_large_image',
title: post.title,
description: post.excerpt,
images: [post.coverImage],
},
alternates: {
canonical: `https://yourdomain.com/blog/${post.slug}`,
},
}
}
export default async function BlogPostPage({ params }: { params: { slug: string } }) {
const post = await getPostBySlug(params.slug)
if (!post) notFound()
// Dados estruturados BlogPosting
const articleData = {
'@context': 'https://schema.org',
'@type': 'BlogPosting',
headline: post.title,
description: post.excerpt,
image: post.coverImage,
datePublished: post.publishedAt,
dateModified: post.updatedAt || post.publishedAt,
author: {
'@type': 'Person',
name: post.author,
},
publisher: {
'@type': 'Organization',
name: 'TechBlog',
logo: {
'@type': 'ImageObject',
url: 'https://yourdomain.com/logo.png',
},
},
mainEntityOfPage: {
'@type': 'WebPage',
'@id': `https://yourdomain.com/blog/${post.slug}`,
},
}
return (
<>
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
<StructuredData data={articleData} />
</>
)
}
4. Sitemap e Robots
// app/sitemap.ts
import { getAllPosts } from '@/lib/posts'
export default async function sitemap() {
const posts = await getAllPosts()
const baseUrl = 'https://yourdomain.com'
const blogUrls = posts.map((post) => ({
url: `${baseUrl}/blog/${post.slug}`,
lastModified: new Date(post.updatedAt),
changeFrequency: 'weekly' as const,
priority: 0.7,
}))
return [
{
url: baseUrl,
lastModified: new Date(),
changeFrequency: 'daily' as const,
priority: 1,
},
{
url: `${baseUrl}/about`,
lastModified: new Date(),
changeFrequency: 'monthly' as const,
priority: 0.8,
},
...blogUrls,
]
}
// app/robots.ts
export default function robots() {
return {
rules: {
userAgent: '*',
allow: '/',
disallow: ['/api', '/admin'],
},
sitemap: 'https://yourdomain.com/sitemap.xml',
}
}
Checklist de validação depois do deploy
Depois de configurar, confira antes de publicar:
-
Ver o código-fonte HTML
- No navegador, pressione F12, abra Elements e confira as tags em
<head> - Verifique se
<title>,<meta name="description">e tags OG renderizam corretamente
- No navegador, pressione F12, abra Elements e confira as tags em
-
Testar sitemap e robots
- Acesse
https://yourdomain.com/sitemap.xmle veja se foi gerado corretamente - Acesse
https://yourdomain.com/robots.txte confira o conteúdo
- Acesse
-
Validar dados estruturados
- Teste algumas páginas com o Google Rich Results Test
- Confirme que não há erros ou alertas
-
Testar compartilhamento social
- Use Facebook Sharing Debugger e Twitter Card Validator
- Confirme que imagem, título e descrição aparecem corretamente
-
Enviar aos mecanismos de busca
- Envie o sitemap no Google Search Console
- Envie o sitemap no Bing Webmaster Tools
Com isso feito, a configuração de SEO do seu site Next.js já está bem completa.
Conclusão
Se você leu até aqui com atenção, já deve ter entendido: SSR no Next.js não significa SEO amigável, mas as ferramentas do Next.js 15 realmente tornam a configuração de SEO muito mais simples.
Vamos recapitular os pontos principais:
- Metadata API permite configurar meta tags com segurança de tipos; páginas estáticas usam o objeto metadata, páginas dinâmicas usam a função generateMetadata
- Dados estruturados (JSON-LD) são uma arma poderosa para aumentar a taxa de cliques em 20% a 30%, e podem ser implementados com facilidade usando o componente
<Script> - Open Graph e Twitter Cards determinam como seu link aparece nas redes sociais, e 1200x630 é a dimensão mais versátil para imagens
- sitemap.xml e robots.txt podem ser gerados automaticamente com arquivos
.ts; não esqueça de enviar ao Search Console - Otimização de imagens com
next/imagee textos alt bem escritos também pode trazer tráfego relevante da busca por imagens
Falando francamente, essas configurações parecem trabalhosas, mas o retorno é altíssimo. Já vi produtos tecnicamente excelentes ficarem sem tráfego porque o SEO estava mal feito, dependendo apenas de anúncios pagos. Sites bem configurados, por outro lado, recebem tráfego orgânico continuamente, com custo quase zero.
SEO não é misticismo; é método. Siga o checklist deste artigo, valide com ferramentas e os resultados aparecem. Talvez não no dia seguinte, mas em três meses você deve ver um crescimento perceptível de tráfego.
Não espere o tráfego desaparecer para lembrar de SEO. Abra seu projeto agora e reserve meio dia para configurar isso. Se encontrar problemas, salve este artigo para consultar quando precisar.
Se este texto foi útil para você, compartilhe com outros desenvolvedores. Ajudar alguém a evitar alguns erros também já vale muito.
Fluxo completo de configuração de SEO no Next.js
Passos completos de otimização de SEO, da Metadata API aos dados estruturados, sitemap e robots.txt.
⏱️ Estimated time: 4 hr
- 1
Step 1: Configurar a metadata básica
Use a Metadata API do Next.js 15:
• Exporte um objeto metadata em layout.js ou page.js
• Configure title, description e keywords
• Defina Open Graph e Twitter Cards
Exemplo:
export const metadata = {
title: 'Título da página',
description: 'Descrição da página',
openGraph: {
title: 'Título OG',
description: 'Descrição OG',
images: ['/og-image.jpg']
}
} - 2
Step 2: Configurar metadata dinâmica
Para rotas dinâmicas:
• Use a função generateMetadata
• Gere metadata com base nos parâmetros da rota
• Use funções async para buscar dados
Exemplo:
export async function generateMetadata({ params }) {
const post = await getPost(params.id)
return {
title: post.title,
description: post.description
}
} - 3
Step 3: Adicionar dados estruturados
Use o formato JSON-LD:
• Use uma tag script com type application/ld+json e conteúdo JSON
• Suporte a tipos como Article, Product e FAQ
• Use o vocabulário Schema.org
Exemplo, corpo JSON; na página, aplique JSON.stringify ao objeto e coloque dentro do script:
{
"@context": "https://schema.org",
"@type": "Article",
"headline": "Título do artigo"
}
No Next.js, você pode emitir o JSON usando next/script ou abordagem equivalente. - 4
Step 4: Configurar sitemap e robots.txt
Crie sitemap.ts:
• Exporte uma função default que retorna um array de sitemap
• Inclua URL, lastModified e changeFrequency de todas as páginas
• Gere páginas dinâmicas quando necessário
Crie robots.ts:
• Configure robôs permitidos e bloqueados
• Defina o caminho do sitemap
• Configure as regras de rastreamento - 5
Step 5: Otimizar imagens para SEO
Pontos essenciais de otimização de imagem:
• Use o componente next/image
• Adicione texto alt significativo
• Configure dimensões de imagem; 1200x630 funciona bem para imagens OG
• Use formatos WebP/AVIF
• Adicione dados estruturados de imagem, como ImageObject - 6
Step 6: Validar e testar
Ferramentas de validação:
• Google Rich Results Test: valida dados estruturados
• Facebook Sharing Debugger: testa tags OG
• Twitter Card Validator: testa Twitter Cards
• Google Search Console: envia sitemap e monitora dados
Checklist:
• Cada página tem title e description únicos
• A imagem OG está no tamanho correto, 1200x630
• Os dados estruturados estão no formato correto
• O sitemap foi enviado ao Search Console
FAQ
Qual é a relação entre SSR e SEO?
Qual é a diferença entre Metadata API e o componente Head?
Como configurar metadata para rotas dinâmicas?
Exemplo:
export async function generateMetadata({ params }) {
const data = await getData(params.id)
return { title: data.title }
}
Qual deve ser o tamanho de uma imagem Open Graph?
Dados estruturados são obrigatórios?
Preciso criar sitemap e robots.txt manualmente?
Quanto tempo leva para ver resultados depois da otimização de SEO?
Recomendações:
1) Envie o sitemap ao Google Search Console
2) Valide com o Google Rich Results Test
3) Monitore continuamente os dados do Search Console
4) Mantenha o conteúdo atualizado
22 min de leitura · Publicado em: 19 dez 2025 · Atualizado em: 14 jul 2026
Guia completo Next.js
Se você chegou pela busca, o caminho mais rápido é ir para o post anterior ou próximo desta série.
Anterior
Otimização do Core Web Vitals no Next.js: guia completo de LCP, FCP e CLS
Guia completo para otimizar LCP, FCP e CLS no Next.js e levar a pontuação do Lighthouse a mais de 90, com mais de 10 exemplos de código, armadilhas comuns e técnicas práticas.
Parte 27 de 51
Próximo
Guia de configuração de Sitemap e robots.txt no Next.js: acelere a indexação do seu site
Guia completo para configurar Sitemap e robots.txt no Next.js, com três opções de geração, erros comuns e integração com o Google Search Console para acelerar a indexação de sites novos.
Parte 29 de 51



Comentários
Entre com GitHub para comentar