---
title: "Next.js: Metadata API"
slug: "nextjs-metadata-api"
category: "frontend"
tags: ["frontend", "nextjs", "metadata", "seo", "open-graph"]
status: "stable"
last_updated: 2026-08-14
summary: "The Next.js 16 Metadata API: static and dynamic exports, OG and Twitter cards, favicon conventions, sitemap.ts, robots.ts."
related: ["[[frontend/nextjs]]", "[[frontend/nextjs-app-router]]", "[[seo/technical]]", "[[seo/og-images]]", "[[seo/page-speed]]", "[[seo/structured-data]]", "[[seo/image-seo]]"]
---

## Overview

The App Router has a first-class Metadata API. Pages and layouts export a `metadata` object or a `generateMetadata` function, and the framework renders the right `<head>` tags. There is no `<Head>` component and no hand-written `<title>`. The same model covers Open Graph cards, Twitter cards, favicons, `robots.txt`, sitemaps, and JSON-LD.

## Export `metadata` from a page or layout

A static `metadata` export is the simplest path. The framework reads it at build time and renders the tags.

```tsx
// app/about/page.tsx
import type { Metadata } from "next";

export const metadata: Metadata = {
  title: "About",
  description: "Who we are and what we ship.",
  alternates: { canonical: "/about" },
};

export default function Page() {
  return <main>...</main>;
}
```

Layouts contribute defaults. A child segment overrides; missing fields fall through. The root `app/layout.tsx` is the right home for site-wide defaults (template title, default OG image, theme color).

## Compose titles with a layout template

A layout's `title.template` wraps every child's `title`. Set it once in the root layout so every page reads `"Page Title | Site Name"` without repeating the suffix.

```tsx
// app/layout.tsx
export const metadata: Metadata = {
  metadataBase: new URL("https://example.com"),
  title: {
    template: "%s | Example",
    default: "Example",
  },
};
```

`metadataBase` is required for any relative URL the framework resolves (canonical, OG image). Set it once at the root.

## Generate metadata dynamically when data drives the title

A `generateMetadata` function runs server-side and can `await` the same loaders the page uses. The Data Cache deduplicates the calls; see [[frontend/nextjs-caching]].

```tsx
// app/posts/[slug]/page.tsx
export async function generateMetadata({
  params,
}: {
  params: Promise<{ slug: string }>;
}): Promise<Metadata> {
  const { slug } = await params;
  const post = await getPost(slug);
  return {
    title: post.title,
    description: post.summary,
    alternates: { canonical: `/posts/${post.slug}` },
    openGraph: {
      title: post.title,
      description: post.summary,
      images: [post.coverImage],
      type: "article",
      publishedTime: post.publishedAt,
    },
    twitter: { card: "summary_large_image" },
  };
}
```

Set `alternates.canonical` on every page. Search engines need a single canonical URL per resource, and the App Router does not infer one for you.

## Render OG and Twitter cards from the same data

The `openGraph` and `twitter` keys map directly to the meta tags. Reuse the page's existing title, description, and cover image; do not write a second copy for social.

For dynamic OG images, generate them at the edge with `next/og`. Place an `opengraph-image.tsx` (or `twitter-image.tsx`) in the route folder and export a default function that returns JSX. See [[seo/og-images]] for layout patterns.

```tsx
// app/posts/[slug]/opengraph-image.tsx
import { ImageResponse } from "next/og";
export const alt = "Post";
export const size = { width: 1200, height: 630 };
export const contentType = "image/png";

export default async function Image({ params }) {
  const post = await getPost(params.slug);
  return new ImageResponse(<div style={{ fontSize: 64 }}>{post.title}</div>, size);
}
```

## Drop favicons in `app/` by convention

Place a file named `icon.png`, `icon.svg`, `apple-icon.png`, or `favicon.ico` in `app/` and the framework wires up the right `<link>` tags. For dynamic icons, export an `icon.tsx` that returns an `ImageResponse`.

The convention is positional; the filename is the contract. No `manifest` writes required for basic PWA icons (the framework picks them up from the file paths).

## Ship a sitemap with `sitemap.ts`

A `sitemap.ts` in `app/` exports a function that returns an array of URL entries. Generate it from the same data your pages use.

```ts
// 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 } });
  return [
    { url: "https://example.com", lastModified: new Date(), priority: 1 },
    ...posts.map((p) => ({
      url: `https://example.com/posts/${p.slug}`,
      lastModified: p.updatedAt,
    })),
  ];
}
```

For more than 50,000 URLs, return one shard per file and reference them from a sitemap index. See [[seo/technical]] for indexing rules.

## Ship `robots.ts` for crawl directives

A `robots.ts` in `app/` returns the `robots.txt` body. The framework serves it at `/robots.txt`.

```ts
// app/robots.ts
import type { MetadataRoute } from "next";

export default function robots(): MetadataRoute.Robots {
  return {
    rules: [{ userAgent: "*", allow: "/", disallow: ["/admin", "/api/internal"] }],
    sitemap: "https://example.com/sitemap.xml",
  };
}
```

Pair this with [[seo/llms-txt]] and [[seo/ai-txt]] for AI crawler directives, served from the public folder or a route handler (see [[frontend/nextjs-route-handlers]]).

## Related

- [[frontend/nextjs]]
- [[frontend/nextjs-app-router]]
- [[seo/technical]]
- [[seo/og-images]]
- [[seo/page-speed]]
- [[seo/structured-data]]
- [[seo/image-seo]]
