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.

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

// 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 nextjs-caching.

// 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 og-images for layout patterns.

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

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

// 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 llms-txt and ai-txt for AI crawler directives, served from the public folder or a route handler (see nextjs-route-handlers).