Tipar Metadata no Next.js com TypeScript
Domine a tipagem de Metadata no Next.js. Do import do tipo até generateMetadata dinâmico e openGraph tipado, tudo com exemplos que melhoram seu SEO na prática.
Por que isso é importante
Tipar Metadata no Next.js com TypeScript. Domine a tipagem de Metadata no Next.js. Do import do tipo até generateMetadata dinâmico e openGraph tipado, tudo com exemplos que melhoram seu SEO na prática.
Como Funciona o Sistema de Metadata do Next.js
O Next.js App Router tem dois jeitos de definir metadata: estática e dinâmica. A estática é um objeto exportado chamado metadata. A dinâmica é uma função async chamada generateMetadata. As duas aceitam o tipo Metadata do Next.js.
O tipo Metadata vem de next. Quando você importa e usa, ganha autocomplete pra todas as propriedades: title, description, openGraph, twitter, robots, alternates, icons e mais. Se errar o nome de uma propriedade, o TypeScript avisa na hora.
O sistema de metadata do Next.js é hierárquico. O layout.tsx define metadata padrão, e cada page.tsx pode sobrescrever. O Next.js faz merge automático. Tipar tudo garante que o merge funciona sem conflitos e sem campos perdidos.
Galera que não tipa metadata acaba copiando e colando objetos entre páginas sem saber quais campos são válidos. Com o tipo Metadata, o editor mostra tudo que dá pra usar. Simples assim.
Passo a Passo: Tipando Metadata no Next.js
Do básico ao avançado. Cada passo adiciona uma camada de segurança no seu SEO.
Exemplos Práticos de Metadata Tipada
Vamos ver cada padrão de metadata que você vai usar nos seus projetos.
Metadata Estática com Tipo Importado
// app/layout.tsx
import type { Metadata } from "next";
export const metadata: Metadata = {
title: {
template: "%s | CrazyStack",
default: "CrazyStack - Cursos de Programação",
},
description: "Aprenda Node.js, React e TypeScript na prática",
keywords: ["programação", "react", "node.js", "typescript"],
authors: [{ name: "CrazyStack Team" }],
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
"max-video-preview": -1,
"max-image-preview": "large",
"max-snippet": -1,
},
},
openGraph: {
type: "website",
locale: "pt_BR",
siteName: "CrazyStack",
},
};
Metadata Estática em Página Específica
// app/about/page.tsx
import type { Metadata } from "next";
// Herda o template do layout: "Sobre Nós | CrazyStack"
export const metadata: Metadata = {
title: "Sobre Nós",
description: "Conheça a equipe CrazyStack e nossa missão",
openGraph: {
title: "Sobre Nós - CrazyStack",
description: "Conheça a equipe por trás dos cursos",
type: "website",
images: [
{
url: "https://www.crazystack.com.br/og-about.png",
width: 1200,
height: 630,
alt: "CrazyStack Team",
},
],
},
alternates: {
canonical: "https://www.crazystack.com.br/about",
},
};
export default function AboutPage() {
return <div>Sobre nós</div>;
}
generateMetadata Dinâmico com Params Tipados
// app/blog/[slug]/page.tsx
import type { Metadata } from "next";
type Params = {
slug: string;
};
type PageProps = {
params: Promise<Params>;
};
// generateMetadata recebe as mesmas props que a page
export async function generateMetadata(
{ params }: PageProps
): Promise<Metadata> {
const { slug } = await params;
const post = await getPost(slug);
if (!post) {
return {
title: "Post não encontrado",
robots: { index: false },
};
}
return {
title: post.title,
description: post.excerpt,
keywords: post.tags,
openGraph: {
title: post.title,
description: post.excerpt,
type: "article",
publishedTime: post.publishedAt,
authors: [post.author],
tags: post.tags,
images: [
{
url: post.coverImage,
width: 1200,
height: 630,
alt: post.title,
},
],
},
twitter: {
card: "summary_large_image",
title: post.title,
description: post.excerpt,
images: [post.coverImage],
},
alternates: {
canonical: `https://www.crazystack.com.br/blog/${slug}`,
},
};
}
Metadata com ResolvingMetadata (Herança do Parent)
// app/products/[id]/page.tsx
import type { Metadata, ResolvingMetadata } from "next";
type Props = {
params: Promise<{ id: string }>;
};
// ResolvingMetadata dá acesso à metadata do parent (layout)
export async function generateMetadata(
{ params }: Props,
parent: ResolvingMetadata
): Promise<Metadata> {
const { id } = await params;
const product = await getProduct(id);
// Acessa as images do parent pra combinar
const previousImages = (await parent).openGraph?.images || [];
return {
title: product.name,
description: `Compre ${product.name} - ${product.price}`,
openGraph: {
title: product.name,
description: product.description,
images: [
{ url: product.image, width: 800, height: 600 },
...previousImages, // mantém images do layout
],
},
};
}
Helper Reutilizável pra Metadata
// lib/metadata.ts
import type { Metadata } from "next";
type MetadataInput = {
title: string;
description: string;
path: string;
image?: string;
type?: "website" | "article";
publishedAt?: string;
tags?: string[];
};
export function createMetadata(input: MetadataInput): Metadata {
const { title, description, path, image, type = "website" } = input;
const url = `https://www.crazystack.com.br${path}`;
const ogImage = image ?? "https://www.crazystack.com.br/og-default.png";
return {
title,
description,
alternates: { canonical: url },
openGraph: {
title,
description,
type,
url,
images: [{ url: ogImage, width: 1200, height: 630 }],
...(input.publishedAt && { publishedTime: input.publishedAt }),
...(input.tags && { tags: input.tags }),
},
twitter: {
card: "summary_large_image",
title,
description,
images: [ogImage],
},
};
}
// Uso em qualquer page.tsx:
// export const metadata = createMetadata({
// title: "Blog",
// description: "Artigos sobre programação",
// path: "/blog",
// });
O helper createMetadata é o padrão mais produtivo. Você define os campos obrigatórios uma vez e reutiliza em todas as páginas. O TypeScript garante que não falta nada e que os valores são do tipo certo.
Erros Comuns com Metadata Tipada
Problemas que sabotam seu SEO silenciosamente
Exportar metadata e generateMetadata na mesma página: O Next.js só aceita um dos dois por arquivo. Se exportar ambos, o build quebra. Escolha estático ou dinâmico, nunca os dois juntos.
Esquecer o tipo de retorno no generateMetadata: Sem Promise<Metadata> explícito, o TypeScript não valida as propriedades. Você pode retornar um objeto com campos errados e ninguém reclama.
Usar title como string no layout raiz: Se o layout usa title: 'MeuSite' (string), as páginas filhas sobrescrevem completamente. Use o objeto { template, default } pra manter o padrão 'Página | MeuSite'.
Não tipar as images do openGraph: images aceita string ou array de objetos. Se você passa um objeto sem width/height, as redes sociais podem renderizar a preview errado. Tipe com { url, width, height, alt }.
Ignorar o ResolvingMetadata: Quando a metadata do layout tem informações importantes (como imagens padrão), use o segundo argumento do generateMetadata pra acessar e combinar com a metadata da página.
Checklist de Metadata Tipada
SEO Profissional com TypeScript na Prática
Metadata tipada é só o início do SEO profissional. No CrazyStack, você configura metadata dinâmica, sitemap automático, robots.txt, structured data e openGraph pra cada página do seu SaaS. Tudo com TypeScript garantindo que nada tá faltando ou errado.
Se você quer que suas páginas apareçam no Google com título, descrição e preview perfeitos, esse é o caminho pra sair do amadorismo.
Continue lendo
Como Tipar Page Params no Next.js App Router com TypeScript
Tipagem de params, searchParams e rotas dinâmicas no Next.js App Router.
Como Usar TypeScript com Next.js App Router
Setup completo de TypeScript no Next.js App Router: page, layout, loading, error e config.
Como Criar API no Next.js com TypeScript
Route Handlers tipados no Next.js App Router com validação e retornos seguros.
Interface no TypeScript
Quando usar interface