Pular para o conteúdo
TypeScript

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.

  1. Passo 1 - Importe o tipo Metadata: Use import type { Metadata } from 'next'. O import type garante que o tipo é removido no build e não gera código JavaScript extra.
  2. Passo 2 - Exporte metadata estática tipada: Declare export const metadata: Metadata = { ... }. O TypeScript valida cada propriedade do objeto na hora que você digita.
  3. Passo 3 - Use generateMetadata pra páginas dinâmicas: Exporte uma função async que recebe props (com params) e retorna Promise. Perfeito pra SEO baseado em dados do banco.
  4. Passo 4 - Tipe o openGraph completo: O openGraph tem sub-propriedades como images, type, locale. Use o autocomplete do tipo Metadata pra preencher tudo sem consultar a documentação.
  5. Passo 5 - Configure robots tipado: Defina index, follow, googleBot com propriedades tipadas. O TypeScript previne valores inválidos como 'maybe' em vez de true/false.
  6. Passo 6 - Defina template de título: Use title: { template: '%s | SeuSite', default: 'SeuSite' } no layout raiz. Cada página filha só precisa definir o título específico.

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 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

  • Importou type { Metadata } from 'next' com import type
  • Layout raiz usa title com template: '%s | Site' e default
  • Metadata estática tipada com export const metadata: Metadata
  • generateMetadata retorna Promise explicitamente
  • openGraph com images tipadas (url, width, height, alt)
  • Alternates com canonical apontando pra URL correta
  • Robots configurado com index, follow e googleBot
  • Helper reutilizável pra padronizar metadata entre páginas

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.