//
Busque em todas as páginas da documentação
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Implemente as melhores práticas de SEO no Next.js 15+ App Router usando a Metadata API, generateMetadata dinâmico, sitemaps, robots.txt, imagens Open Graph e dados estruturados JSON-LD.
// app/layout.tsx
import type { Metadata } from "next";
export const metadata: Metadata = {
title: {
default: "Meu App",
template: "%s | Meu App",
},
description: "Uma aplicação web moderna construída com Next.js",
metadataBase: new URL("https://myapp.com"),
openGraph: {
type: "website",
locale: "pt_BR",
siteName: "Meu App",
},
twitter: {
card: "summary_large_image",
creator: "@myapp",
},
robots: {
index: true,
follow: true,
},
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="pt-BR">
<body>{children}</body>
</html>
);
}// app/posts/[slug]/page.tsx
import type { Metadata, ResolvingMetadata } from "next";
import { notFound } from "next/navigation";
type Props = {
params: Promise<{ slug: string }>;
};
export async function generateMetadata(
{ params }: Props,
parent: ResolvingMetadata
): Promise<Metadata> {
const { slug } = await params;
const post = await db.post.findUnique({ where: { slug } });
if (!post) {
return {};
}
const parentMetadata = await parent;
const previousImages = parentMetadata.openGraph?.images ?? [];
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
type: "article",
publishedTime: post.createdAt.toISOString(),
authors: [post.author.name],
images: [
{
url: `/api/og?title=${encodeURIComponent(post.title)}`,
width: 1200,
height: 630,
alt: post.title,
},
...previousImages,
],
},
twitter: {
card: "summary_large_image",
title: post.title,
description: post.excerpt,
},
};
}
export default async function PostPage({ params }: Props) {
const { slug } = await params;
const post = await db.post.findUnique({ where: { slug } });
if (!post) notFound();
return <article>{post.content}</article>;
}// app/sitemap.ts
import type { MetadataRoute } from "next";
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = await db.post.findMany({
select: { slug: true, updatedAt: true },
orderBy: { updatedAt: "desc" },
});
const postEntries = posts.map((post) => ({
url: `https://myapp.com/posts/${post.slug}`,
lastModified: post.updatedAt,
changeFrequency: "weekly" as const,
priority: 0.8,
}));
const staticPages = [
{
url: "https://myapp.com",
lastModified: new Date(),
changeFrequency: "daily" as const,
priority: 1.0,
},
{
url: "https://myapp.com/about",
lastModified: new Date(),
changeFrequency: "monthly" as const,
priority: 0.5,
},
];
return [...staticPages, ...postEntries];
}// app/robots.ts
import type { MetadataRoute } from "next";
export default function robots(): MetadataRoute.Robots {
return {
rules: [
{
userAgent: "*",
allow: "/",
disallow: ["/api/", "/admin/", "/dashboard/"],
},
],
sitemap: "https://myapp.com/sitemap.xml",
};
}// app/posts/[slug]/page.tsx
import type { WithContext, Article } from "schema-dts";
function JsonLd({ data }: { data: WithContext<Article> }) {
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
/>
);
}
export default async function PostPage({ params }: Props) {
const { slug } = await params;
const post = await db.post.findUnique({ where: { slug } });
if (!post) notFound();
const jsonLd: WithContext<Article> = {
"@context": "https://schema.org",
"@type": "Article",
headline: post.title,
description: post.excerpt,
datePublished: post.createdAt.toISOString(),
dateModified: post.updatedAt.toISOString(),
author: {
"@type": "Person",
name: post.author.name,
},
};
return (
<>
<JsonLd data={jsonLd} />
<article>{post.content}</article>
</>
);
}// app/api/og/route.tsx
import { ImageResponse } from "next/og";
import { NextRequest } from "next/server";
export const runtime = "edge";
export async function GET(request: NextRequest) {
const title = request.nextUrl.searchParams.get("title") ?? "Meu App";
return new ImageResponse(
(
<div
style={{
fontSize: 60,
color: "white",
background: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
width: "100%",
height: "100%",
display: "flex",
alignItems: "center",
justifyContent: "center",
padding: 60,
textAlign: "center",
}}
>
{title}
</div>
),
{
width: 1200,
height: 630,
}
);
}<head> no App Router. Exporte um objeto metadata ou uma função generateMetadata de page.tsx ou layout.tsx.title.template no layout raiz ("%s | Meu App") é aplicado aos títulos das páginas filhas.generateMetadata recebe metadados pais resolvidos através do segundo argumento. Isso permite estender imagens Open Graph pais ou outros valores herdados.sitemap.ts e robots.ts são convenções de arquivo especiais. O Next.js os serve em /sitemap.xml e /robots.txt automaticamente.metadataBase define a URL base para todas as URLs de metadados relativas (imagens Open Graph, URLs canônicas). Sempre defina-o no layout raiz.ImageResponse de next/og gera imagens Open Graph dinâmicas usando JSX na edge. Ele usa Satori internamente, que suporta um subconjunto de CSS (apenas flexbox, sem grid).Múltiplos Sitemaps (Sites Grandes):
// app/sitemap/[id]/route.ts
import { NextRequest } from "next/server";
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
const page = parseInt(id, 10);
const perPage = 50000;
const posts = await db.post.findMany({
skip: page * perPage,
take: perPage,
select: { slug: true, updatedAt: true },
});
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${posts.map((post) => `
<url>
<loc>https://myapp.com/posts/${post.slug}</loc>
<lastmod>${post.updatedAt.toISOString()}</lastmod>
</url>`).join("")}
</urlset>`;
return new Response(xml, {
headers: { "Content-Type": "application/xml" },
});
}URLs Canônicas:
export const metadata: Metadata = {
alternates: {
canonical: "/posts/my-post",
languages: {
"en-US": "/en/posts/my-post",
"de-DE": "/de/posts/my-post",
},
},
};Metadata e ResolvingMetadata de "next" para inferência de tipo completa.MetadataRoute.Sitemap é um array de objetos com url, lastModified, changeFrequency e priority.schema-dts para dados estruturados JSON-LD tipados.generateMetadata são assíncronos no Next.js 15+ (mesmo padrão Promise<{ slug: string }> das páginas).generateMetadata é executado antes do componente da página. Sua busca de dados é dedupicada com a busca da página se a mesma URL for solicitada, mas a função em si é executada separadamente.metadataBase deve ser uma URL absoluta. URLs relativas em imagens Open Graph quebrarão sem ele.metadata e a função generateMetadata só funcionam em Server Components (page.tsx e layout.tsx).ImageResponse suporta apenas flexbox. CSS Grid, position: absolute (com exceções) e muitas propriedades CSS não são suportadas pelo Satori.title.template se aplica apenas a páginas filhas, não à página onde é definido. A própria página usa title.default.| Abordagem | Prós | Contras |
|---|---|---|
| Metadata API (embutida) | Type-safe, automática, co-localizada | Não pode ser usada em Client Components |
Pacote next-seo | API familiar da era Pages Router | Redundante com a Metadata API embutida |
Tags <head> manuais | Controle total | Sem segurança de tipo, fácil de esquecer tags |
schema-dts para JSON-LD | Dados estruturados tipados | Dependência extra |
Pacote next-sitemap | Geração automática, suporte ISR | Dependência extra, sobrecarga de configuração |
metadata é um objeto estático para páginas com metadados fixos (por exemplo, a página inicial).generateMetadata é uma função assíncrona para páginas onde os metadados dependem de dados dinâmicos (por exemplo, um post de blog).page.tsx ou layout.tsx e são apenas para Server Components.title.template no layout raiz (por exemplo, "%s | Meu App") é aplicado aos títulos das páginas filhas.title.template se aplica apenas a páginas filhas, não à página onde é definido.metadataBase define a URL base para todas as URLs de metadados relativas (imagens OG, URLs canônicas).metadataBase: new URL("https://myapp.com").app/sitemap.ts a serve automaticamente em /sitemap.xml.app/robots.ts a serve automaticamente em /robots.txt.metadata e generateMetadata devem ser exportados de page.tsx ou layout.tsx sem a diretiva "use client".position: absolute tem suporte limitado.<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(jsonLdObject),
}}
/><script type="application/ld+json"> no componente da página.schema-dts para objetos JSON-LD tipados.import type { Metadata, ResolvingMetadata } from "next";
type Props = {
params: Promise<{ slug: string }>;
};
export async function generateMetadata(
{ params }: Props,
parent: ResolvingMetadata
): Promise<Metadata> {
const { slug } = await params;
// ...
}params é uma Promise no Next.js 15+ e deve ser aguardada.generateMetadata, fornecendo metadados pais resolvidos.const prev = (await parent).openGraph?.images ?? [].sitemap.ts embutida não tem suporte de paginação.generateMetadata é executado antes do componente da página.import type { WithContext, Article } from "schema-dts";
const jsonLd: WithContext<Article> = {
"@context": "https://schema.org",
"@type": "Article",
headline: post.title,
datePublished: post.createdAt.toISOString(),
};schema-dts fornece tipos TypeScript para todos os tipos do Schema.org.Revisado por Chris St. John·Última atualização: 7 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥