Capítulo 15 de 456
As Metadata APIs definem metadados de aplicação para SEO e compartilhamento web: objeto estático metadata, função dinâmica generateMetadata, e convenções de arquivo para favicons e imagens OG. Exports de metadata só funcionam em Server Components.
charset e viewport são sempre adicionadas mesmo sem metadata definida.metadata (objeto estático): exportado de layout.js/page.js estático, para metadados que não dependem de dados.generateMetadata: função async que busca dados (ex.: fetch) para gerar metadata dinâmica; recebe params, searchParams e parent (ResolvingMetadata).generateMetadata resolve, sem bloquear a UI; desabilitado automaticamente para bots que esperam metadata no <head> (Twitterbot, Slackbot, Bingbot), detectados via User-Agent. Controlável via htmlLimitedBots. Páginas prerenderizadas não usam streaming (metadata já resolvida no build).cache do React: evita fetch duplicado quando o mesmo dado é necessário tanto em generateMetadata quanto na página.favicon.ico na raiz de app/ (ou geração programática).opengraph-image.jpg na raiz ou em subpastas de rota; a imagem mais específica na árvore de pastas tem precedência.ImageResponse (next/og): gera imagens OG dinamicamente via JSX/CSS (flexbox e subset de CSS; não suporta display: grid).import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'My Blog',
description: '...',
}
export default function Layout() {}
export async function generateMetadata(
{ params, searchParams }: Props,
parent: ResolvingMetadata
): Promise<Metadata> {
const slug = (await params).slug
const post = await fetch(`https://api.vercel.app/blog/${slug}`).then((res) => res.json())
return { title: post.title, description: post.description }
}
slug.import { cache } from 'react'
import { db } from '@/app/lib/db'
// getPost será usada duas vezes, mas executa só uma
export const getPost = cache(async (slug: string) => {
return await db.query.posts.findFirst({ where: eq(posts.slug, slug) })
})
generateMetadata quanto no corpo da página, evitando fetch duplicado.import { ImageResponse } from 'next/og'
import { getPost } from '@/app/lib/data'
export const size = { width: 1200, height: 630 }
export const contentType = 'image/png'
export default async function Image({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params
const post = await getPost(slug)
return new ImageResponse(
<div style={{ fontSize: 128, background: 'white', width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
{post.title}
</div>
)
}
app/blog/[slug]/opengraph-image.tsx) a partir de dado da própria rota.| Arquivo de convenção | Propósito |
|---|---|
favicon.ico, apple-icon.jpg, icon.jpg | Ícones de site |
opengraph-image.jpg, twitter-image.jpg | Imagens de compartilhamento social |
robots.txt | Diretivas para crawlers |
sitemap.xml | Mapa do site |
generateMetadata e na página: duplica a requisição; usar cache do React para memoizar.display: grid em ImageResponse: não suportado (satori só cobre flexbox e subset de CSS); reestruturar o layout com flexbox.<head> para bots de preview social: streaming é desabilitado automaticamente para eles, mas é importante saber que o comportamento difere de usuários reais.metadata estático é para o que não depende de dado; generateMetadata é para o que depende (ex.: dado de post buscado por slug).<head> completo (configurável via htmlLimitedBots).generateMetadata e a página com cache do React evita requisição duplicada.ImageResponse roda sobre @vercel/og/satori/resvg: suporta só flexbox e subset de CSS, não grid.generateMetadata e generateViewport, fetches não cacheados ou acesso a dado de runtime disparam os mesmos insights de Cache Components.