Next.js: SEO & API de Metadados
Última atualização: 2026-08-26
SEO é um motor de tráfego gratuito para sites — metadados corretos podem fazer o Google indexar seu site três vezes mais rápido.
1. O Que Você Vai Aprender
generateMetadata()Gerar dinamicamente título da página, description, OpenGraph e metadados do TwittermetadataExportação Estática e Metadados de Rota Estática- Dados Estruturados JSON-LD (BreadcrumbList / Article / FAQPage)
- Sitemap e Geração Dinâmica com
robots.txt - Configuração de SEO Multilíngue com hreflang
@vercel/og+ Satori: Gerar Imagens OG Dinamicamente
2. Uma História Real de um Gerente de Operações de Conteúdo
(1) Ponto de Dor: Três meses após o lançamento, o Google indexou apenas cinco páginas
Bob é responsável pelas operações de conteúdo do TaskFlow. Três meses após o lançamento do blog, ele havia escrito 50 artigos:
"O Google Search Console mostra que apenas 5 páginas foram indexadas. Ao inspecionar, descobri que todas as páginas têm
<title>'TaskFlow' como título, sem meta description e sem imagem Open Graph — então, quando compartilhadas nas redes sociais, aparecem como links simples."
| Problema | Impacto | Quantificação |
|---|---|---|
| Título duplicado | Mecanismos de busca não conseguem distinguir entre páginas | Apenas 5 de 50 indexadas |
| Sem imagem OG | Sem imagem de pré-visualização para compartilhamento social | Taxa de cliques em compartilhamentos: 0,3% |
| Sem JSON-LD | Sem resumo de mídia rica | Sem informações adicionais nos resultados de busca |
| Sem Sitemap | Google não consegue rastrear páginas profundas | Atraso de indexação de 30 dias |
(2) A Solução da API de Metadados do Next.js
Use
generateMetadata()para gerar tags SEO únicas e dados estruturados JSON-LD para cada página.
// app/blog/[slug]/page.tsx — SEO de Notícias
import type { Metadata } from 'next'
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {
const { slug } = await params
const post = await getPost(slug)
return {
title: `${post.title} - Blog do TaskFlow`,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [{ url: post.ogImage, width: 1200, height: 630 }]
}
}
}
(3) Resultado
| Métrica | Antes da Otimização | Depois da Otimização | Melhoria |
|---|---|---|---|
| Taxa de Indexação do Google | 10% | 98% | 9,8x |
| Taxa de Cliques em Compartilhamentos | 0,3% | 2,8% | 9,3x |
| Resultados de Busca com Mídia Rica | ❌ Nenhum | ✅ Breadcrumbs + Cartões de Artigo | — |
| Velocidade de Descoberta de Páginas | 30 dias | < 24h | 30x |
3. Metadados Estáticos e Dinâmicos
(1) Exportação Estática
// app/about/page.tsx — Metadados de Página Estática
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Sobre o TaskFlow - Plataforma de Colaboração em Equipe',
description: 'TaskFlow ajuda mais de 10.000 equipes a colaborar com alta eficiência, oferecendo Gestão de Projetos, Colaboração em Tempo Real e recursos de análise inteligente.',
keywords: ['Trabalho em Equipe', 'Gestão de Projetos', 'TaskFlow', 'SaaS'],
authors: [{ name: 'Equipe TaskFlow', url: 'https://taskflow.io' }]
}
(2) Geração Dinâmica
// app/products/[id]/page.tsx — Metadados de Notícias
import type { Metadata, ResolvingMetadata } from 'next'
type Props = { params: Promise<{ id: string }>; searchParams: Promise<{ [key: string]: string | string[] | undefined }> }
export async function generateMetadata({ params, searchParams }: Props, parent: ResolvingMetadata): Promise<Metadata> {
const { id } = await params
const product = await fetch(`https://api.taskflow.io/products/${id}`).then(r => r.json())
return {
title: `${product.name} - Produtos TaskFlow`,
description: product.description,
openGraph: {
title: product.name,
description: product.description,
url: `https://taskflow.io/products/${id}`,
siteName: 'TaskFlow',
images: [
{
url: product.ogImage,
width: 1200,
height: 630,
alt: product.name
}
],
locale: 'pt_BR',
type: 'website'
},
twitter: {
card: 'summary_large_image',
title: product.name,
description: product.description,
images: [product.ogImage]
},
alternates: {
canonical: `https://taskflow.io/products/${id}`,
languages: {
'en': `https://taskflow.io/en/products/${id}`,
'ja': `https://taskflow.io/ja/products/${id}`,
'ar': `https://taskflow.io/ar/products/${id}`
}
}
}
}
(3) Tabela de Referência Rápida de Campos de Metadados
| Campo | Finalidade | Exemplo |
|---|---|---|
title |
Título da Página / <title> |
'Detalhes do Produto - TaskFlow' |
description |
Resumo para Mecanismos de Busca | 'Ferramenta de Gestão de Projetos TaskFlow...' |
openGraph |
Compartilhamento no Facebook / LinkedIn | { title, description, images } |
twitter |
Cartão X (Twitter) | { card: 'summary_large_image' } |
alternates.canonical |
URL Autoritativa para Prevenir Conteúdo Duplicado | https://taskflow.io/page |
alternates.languages |
hreflang multilíngue | { 'en': '...' } |
robots |
Comando para rastreadores | { index: true, follow: true } |
▶ Exemplo: SEO Completo para um Post de Blog
Output:
Função assíncrona executa e retorna dados buscados.
// app/blog/[slug]/page.tsx
import type { Metadata } from 'next'
interface Post {
title: string
excerpt: string
ogImage: string
publishedAt: string
author: string
tags: string[]
}
async function getPost(slug: string): Promise<Post> {
const res = await fetch(`https://api.taskflow.io/blog/${slug}`, { next: { revalidate: 3600 } })
return res.json()
}
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {
const { slug } = await params
const post = await getPost(slug)
return {
title: `${post.title} | Blog do TaskFlow`,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
type: 'article',
publishedTime: post.publishedAt,
authors: [post.author],
images: [{ url: post.ogImage, width: 1200, height: 630 }]
},
twitter: {
card: 'summary_large_image',
title: post.title,
description: post.excerpt,
images: [post.ogImage]
},
keywords: [...post.tags, 'TaskFlow', 'Gestão de Projetos'],
alternates: { canonical: `https://taskflow.io/blog/${slug}` },
robots: { index: true, follow: true }
}
}
Output:
Define tipo(s) TypeScript: Post.
▶ Exemplo: Usando a meta tag robots para controlar rastreadores web
Output:
Define tipo(s) TypeScript: Post.
// app/private/dashboard/page.tsx — Impedir que mecanismos de busca indexem
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Dashboard - TaskFlow',
robots: {
index: false, // Não indexar
follow: false, // Não rastrear links
noarchive: true, // Não armazenar em cache
nosnippet: true, // Não exibir resumo
nocache: true // Não armazenar em cache
}
}
export default function DashboardPage() {
return <h1>Dashboard Privado</h1>
}
<meta name="robots" content="noindex, nofollow, noarchive, nosnippet" />
Output:
Navegador renderiza: cabeçalho "Dashboard Privado". Mecanismos de busca não indexarão esta página — a tag <meta name="robots" content="noindex, nofollow, noarchive, nosnippet"> impede o rastreamento.
4. Dados Estruturados JSON-LD
(1) Três Schemas Comuns
graph TB
A[Dados Estruturados JSON-LD] --> B[BreadcrumbList<br/>Navegação Breadcrumb]
A --> C[Article<br/>Detalhes do Artigo]
A --> D[FAQPage<br/>Perguntas Frequentes]
B --> E[Exibir o caminho nos resultados de busca]
C --> F[Painel de Conhecimento + Imagem de Capa]
D --> G[Resultados de busca exibem Q&A diretamente]
style A fill:#cce5ff
style B fill:#d4edda
style C fill:#d4edda
style D fill:#d4edda
| Schema | Caso de Uso | Exibição nos Resultados de Busca |
|---|---|---|
| BreadcrumbList | Todas as Páginas | Exibir trilha de breadcrumb (Início > Produtos > Detalhes) |
| Article | Post de Blog | Cartão de Artigo (Título + Resumo + Imagem de Capa + Data de Publicação) |
| FAQ Page | Central de Ajuda / Perguntas Frequentes | Exibir lista de perguntas e respostas (recolhível) |
| Product | Página de Produto | Preço + Estoque + Classificação por Estrelas |
(2) Função de Injeção JSON-LD
// lib/jsonld.ts — Ferramentas de Geração JSON-LD
export function breadcrumbJsonld(items: { name: string; url: string }[]) {
return {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: items.map((item, index) => ({
'@type': 'ListItem',
position: index + 1,
name: item.name,
item: item.url
}))
}
}
export function articleJsonld(post: {
title: string
excerpt: string
url: string
ogImage: string
publishedAt: string
author: string
}) {
return {
'@context': 'https://schema.org',
'@type': 'Article',
headline: post.title,
description: post.excerpt,
image: post.ogImage,
datePublished: post.publishedAt,
author: { '@type': 'Person', name: post.author },
publisher: { '@type': 'Organization', name: 'TaskFlow', logo: 'https://taskflow.io/logo.png' },
mainEntityOfPage: { '@type': 'WebPage', '@id': post.url }
}
}
▶ Exemplo: Página de Artigo JSON-LD + Breadcrumb
Output:
Módulo TypeScript executado com sucesso.
// app/blog/[slug]/page.tsx — SEO Completo + JSON-LD
import { breadcrumbJsonld, articleJsonld } from '@/lib/jsonld'
export default async function BlogPostPage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params
const post = await getPost(slug)
const url = `https://taskflow.io/blog/${slug}`
const breadcrumb = breadcrumbJsonld([
{ name: 'Início', url: 'https://taskflow.io' },
{ name: 'Blog', url: 'https://taskflow.io/blog' },
{ name: post.title, url }
])
const article = articleJsonld({
title: post.title,
excerpt: post.excerpt,
url,
ogImage: post.ogImage,
publishedAt: post.publishedAt,
author: post.author
})
return (
<>
{/* Injeção JSON-LD */}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumb) }}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(article) }}
/>
<article>
<h1>{post.title}</h1>
<p>{post.excerpt}</p>
<div>{post.content}</div>
</article>
</>
)
}
Output:
Renderiza a UI do componente ▶ Exemplo: Página de Artigo JSON-LD + Breadcrumb conforme descrito na seção.
5. Sitemap e robots.txt
(1) Geração Dinâmica de Sitemap
// app/sitemap.ts — Sitemap de Notícias (Inclui automaticamente todas as páginas)
import type { MetadataRoute } from 'next'
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const baseUrl = 'https://taskflow.io'
// Páginas Estáticas
const staticPages = [
{ url: baseUrl, lastModified: new Date(), changeFrequency: 'monthly' as const, priority: 1.0 },
{ url: `${baseUrl}/about`, lastModified: new Date(), changeFrequency: 'monthly' as const, priority: 0.8 },
{ url: `${baseUrl}/blog`, lastModified: new Date(), changeFrequency: 'weekly' as const, priority: 0.9 },
{ url: `${baseUrl}/pricing`, lastModified: new Date(), changeFrequency: 'monthly' as const, priority: 0.8 },
{ url: `${baseUrl}/contact`, lastModified: new Date(), changeFrequency: 'yearly' as const, priority: 0.5 }
]
// Posts de Blog Dinâmicos (da API)
const posts = await fetch('https://api.taskflow.io/blog/posts').then(r => r.json())
const blogPages = posts.map((post: { slug: string; updatedAt: string }) => ({
url: `${baseUrl}/blog/${post.slug}`,
lastModified: new Date(post.updatedAt),
changeFrequency: 'weekly' as const,
priority: 0.7
}))
// Páginas Multilíngues
const locales = ['en', 'ja', 'ar']
const localizedPages = locales.flatMap((locale) =>
staticPages.map((page) => ({
url: `${baseUrl}/${locale}${page.url.replace(baseUrl, '')}`,
lastModified: page.lastModified,
changeFrequency: page.changeFrequency,
priority: page.priority * 0.9,
alternates: {
languages: Object.fromEntries(
locales.map((l) => [l, `${baseUrl}/${l}${page.url.replace(baseUrl, '')}`])
)
}
}))
)
return [...staticPages, ...blogPages, ...localizedPages]
}
(2) robots.txt
// app/robots.ts
import type { MetadataRoute } from 'next'
export default function robots(): MetadataRoute.Robots {
return {
rules: [
{
userAgent: '*',
allow: '/',
disallow: ['/api/', '/admin/', '/_next/', '/dashboard']
},
{
userAgent: 'GPTBot',
disallow: '/'
}
],
sitemap: 'https://taskflow.io/sitemap.xml'
}
}
▶ Exemplo: Validação de Sitemap Multilíngue
# Trecho do sitemap.xml Gerado
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xhtml="http://www.w3.org/1999/xhtml">
<url>
<loc>https://taskflow.io/about</loc>
<lastmod>2026-07-06</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://taskflow.io/en/about"/>
<xhtml:link rel="alternate" hreflang="ja" href="https://taskflow.io/ja/about"/>
<xhtml:link rel="alternate" hreflang="ar" href="https://taskflow.io/ar/about"/>
</url>
</urlset>
6. Gerando Imagens OG Dinâmicas
(1) Arquitetura do @vercel/og
graph LR
A[Compartilhar o link nas redes sociais] --> B[Rastreador web solicita Imagem OG]
B --> C[@vercel/og Edge Function]
C --> D[Satori + React]
D --> E[Renderizar JSX como PNG]
E --> F[Imagem OG 1200×630]
F --> G[Facebook / X / LinkedIn Exibem]
style C fill:#cce5ff
style D fill:#d4edda
style E fill:#fff3cd
| Biblioteca | Função | Descrição |
|---|---|---|
@vercel/og |
Geração de Imagem OG via Edge Function | Zero Sobrecarga no Servidor |
Satori |
Conversão JSX → SVG | Menos de 1 ms |
resvg-wasm |
Renderização SVG → PNG | ~5ms |
(2) Instalação e Exemplos Básicos
npm install @vercel/og
// app/og/route.tsx — API de Geração de Imagem OG
import { ImageResponse } from '@vercel/og'
export const runtime = 'edge'
export async function GET() {
return new ImageResponse(
(
<div style={{
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
background: 'linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%)',
color: 'white',
fontSize: 60,
fontWeight: 700,
padding: 40
}}>
<h1>TaskFlow</h1>
<p style={{ fontSize: 32, opacity: 0.9 }}>Plataforma de Colaboração em Equipe</p>
</div>
),
{ width: 1200, height: 630 }
)
}
▶ Exemplo: Imagem OG Dinâmica para Artigo
Output:
Renderiza a UI do componente GET.
// app/blog/[slug]/og/route.tsx — Imagem OG para Artigos em Destaque
import { ImageResponse } from '@vercel/og'
export const runtime = 'edge'
export async function GET(req: Request, { params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params
// Obter Informações do Artigo
const post = await fetch(`https://api.taskflow.io/blog/${slug}`).then(r => r.json())
return new ImageResponse(
(
<div style={{
width: 1200,
height: 630,
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
padding: 60,
background: 'linear-gradient(135deg, #1e1b4b 0%, #4f46e5 100%)',
color: 'white'
}}>
{/* Tags */}
<div style={{ display: 'flex', gap: 8 }}>
{post.tags?.slice(0, 3).map((tag: string) => (
<span key={tag} style={{
padding: '4px 12px',
borderRadius: 20,
background: 'rgba(255,255,255,0.2)',
fontSize: 18
}}>{tag}</span>
))}
</div>
{/* Título */}
<h1 style={{ fontSize: 52, margin: '20px 0', lineHeight: 1.2 }}>
{post.title}
</h1>
{/* Autor + Data */}
<div style={{ display: 'flex', gap: 16, fontSize: 22, opacity: 0.8 }}>
<span>{post.author}</span>
<span>{new Date(post.publishedAt).toLocaleDateString('pt-BR')}</span>
</div>
{/* Logo */}
<div style={{ position: 'absolute', bottom: 40, right: 60, fontSize: 28, fontWeight: 'bold' }}>
Blog do TaskFlow
</div>
</div>
),
{ width: 1200, height: 630 }
)
}
Output:
Busca dados no servidor e renderiza uma lista de itens de post.
Conteúdo visível: ))} | Blog do TaskFlow
/blog/[slug]/og/route.tsx, e o link final é https://taskflow.io/blog/nextjs-seo-guide/og. Basta referenciar este endereço em generateMetadata.
7. Exemplo Completo: Implementação Abrangente de SEO Multilíngue + Imagens OG
// app/layout.tsx — Layout Raiz (Padrão) SEO + Multilíngue
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: { template: '%s | TaskFlow', default: 'TaskFlow - Plataforma de Colaboração em Equipe' },
description: 'TaskFlow ajudando mais de 10.000 equipes no mundo a colaborar com alta eficiência. Oferece Gestão de Projetos, Colaboração em Tempo Real, Análise Inteligente com IA.',
openGraph: {
siteName: 'TaskFlow',
type: 'website',
locale: 'pt_BR',
images: [{ url: 'https://taskflow.io/og-default.png', width: 1200, height: 630 }]
},
twitter: { card: 'summary_large_image', site: '@taskflow' },
robots: { index: true, follow: true },
alternates: {
canonical: 'https://taskflow.io',
languages: {
'en': 'https://taskflow.io/en',
'ja': 'https://taskflow.io/ja',
'ar': 'https://taskflow.io/ar'
}
}
}
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="pt">
<body>{children}</body>
</html>
)
}
// app/sitemap.ts — Sitemap Completo
import type { MetadataRoute } from 'next'
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const baseUrl = 'https://taskflow.io'
const locales = ['pt', 'en', 'ja', 'ar'] as const
const pages = ['', '/about', '/blog', '/pricing', '/contact']
return pages.flatMap((page) =>
locales.map((locale) => ({
url: `${baseUrl}/${locale}${page}`,
lastModified: new Date(),
changeFrequency: 'monthly' as const,
priority: page === '' ? 1.0 : 0.8,
alternates: {
languages: Object.fromEntries(locales.map((l) => [l, `${baseUrl}/${l}${page}`]))
}
}))
)
}
// app/robots.ts
import type { MetadataRoute } from 'next'
export default function robots(): MetadataRoute.Robots {
return {
rules: { userAgent: '*', allow: '/', disallow: ['/api/', '/admin/', '/dashboard'] },
sitemap: 'https://taskflow.io/sitemap.xml'
}
}
// app/blog/[slug]/page.tsx — A página de artigo consolida todo o SEO
import type { Metadata } from 'next'
type Props = { params: Promise<{ slug: string }> }
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params
const post = await fetch(`https://api.taskflow.io/blog/${slug}`).then(r => r.json())
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
type: 'article',
publishedTime: post.publishedAt,
authors: [post.author],
images: [{ url: `https://taskflow.io/blog/${slug}/og`, width: 1200, height: 630 }]
},
twitter: {
card: 'summary_large_image',
title: post.title,
description: post.excerpt,
images: [`https://taskflow.io/blog/${slug}/og`]
},
alternates: {
canonical: `https://taskflow.io/blog/${slug}`,
languages: {
en: `https://taskflow.io/en/blog/${slug}`,
ja: `https://taskflow.io/ja/blog/${slug}`,
ar: `https://taskflow.io/ar/blog/${slug}`
}
}
}
}
export default async function BlogPostPage({ params }: Props) {
const { slug } = await params
return (
<article>
{/* Dados Estruturados JSON-LD */}
<script type="application/ld+json" dangerouslySetInnerHTML={{
__html: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: [
{ '@type': 'ListItem', position: 1, name: 'Início', item: 'https://taskflow.io' },
{ '@type': 'ListItem', position: 2, name: 'Blog', item: 'https://taskflow.io/blog' },
{ '@type': 'ListItem', position: 3, name: slug }
]
})
}} />
<h1>Título do Artigo</h1>
<div>Conteúdo do Artigo...</div>
</article>
)
}
❓ Perguntas Frequentes
P: Qual é a diferença entre as exportações de
generateMetadata()emetadata? R:metadataé uma exportação estática usada para rotas estáticas (como/about).generateMetadata()é uma função assíncrona que gera metadados dinamicamente com base emparams/searchParamse é usada para rotas dinâmicas (como/blog/[slug]). As duas não podem ser usadas simultaneamente.
P: Qual é a relação entre JSON-LD e meta tags? R: Meta tags (
<title>/<meta name="description">) são tags SEO básicas. JSON-LD (<script type="application/ld+json">) são dados estruturados que permitem ao Google exibir rich snippets (breadcrumbs, classificações por estrelas, FAQs recolhíveis). Os dois são complementares e ambos precisam ser implementados.
P: Gerar imagens OG dinâmicas aumenta a carga do servidor? R:
@vercel/ogé executado no Edge Runtime (computação de borda), e cada imagem leva apenas ~5 ms para ser gerada, resultando em sobrecarga praticamente zero. Em ambiente de produção, recomendamos habilitar cache CDN (Cache-Control: public, max-age=31536000, immutable).
P: Onde a tag hreflang deve ser configurada? R: Configure em
generateMetadata()emalternates.languages. O Next.js injeta automaticamente<link rel="alternate" hreflang="en" href="...">no<head>da página. Você também precisa incluir a tagxhtml:linkcorrespondente no sitemap.
P: Os campos
changeFrequencyepriorityno sitemap afetam os rankings do Google? R: O Google declarou oficialmente que ignora esses dois campos. Eles ainda têm valor de referência para mecanismos de busca como Bing e Yandex. Recomendamos que seu sitemap inclua todas as páginas e que o campolastModifiedseja preciso — isso é importante para que os rastreadores determinem se devem rastrear novamente as páginas.
📖 Resumo
generateMetadata()Implementa SEO dinâmico, suportando todos os campos: title, description, OpenGraph e Twitter- Dados estruturados JSON-LD (BreadcrumbList / Article / FAQPage) permitem que os resultados de busca exibam rich snippets
- Os arquivos sitemap e robots.txt são gerados dinamicamente via
app/sitemap.ts/app/robots.tse suportam links alternativos multilíngues - Configure SEO multilíngue com hreflang em
alternates.languagespara garantir que os mecanismos de busca exibam a versão correta do idioma da página para usuários nas regiões correspondentes @vercel/og+ Satori gera dinamicamente imagens OG de 1200×630 no Edge Runtime, permitindo que cada artigo tenha uma pré-visualização de compartilhamento única- Estratégia Abrangente de SEO: Metadados (Básico) + JSON-LD (Aprimorado) + Sitemap (Descoberta) + Imagem OG (Social)
📝 Exercícios
-
Exercício Básico (⭐): Configure a exportação
metadatana rota estática/about, incluindo title, description, Open Graph e cartão do Twitter. -
Exercício Avançado (⭐⭐): Implemente SEO completo para uma página de detalhes de notícia:
generateMetadata()gere dinamicamente title, description, tags OG e cartão do Twitter, injete dados estruturados Article JSON-LD e liste todas as URLs de artigos no sitemap. -
Desafio (⭐⭐⭐): Construa um sistema completo de geração de imagens OG:
@vercel/oggere uma imagem de compartilhamento de 1200×630 para cada post do blog, incluindo título, autor e tags; referencie a URL da imagem emgenerateMetadata(); e, por fim, use o Teste de Resultados Avançados do Google para verificar a correção do JSON-LD e das tags OG.