Skip to content

Metadata and SEO

You describe a page’s <head> as data — a static metadata object or a dynamic generateMetadata function — and Next.js renders the correct tags on the server, so you never hand-manage <head> again.

Export a metadata object from any layout or page and Next.js turns it into <title>, <meta>, and link tags. It runs on the server, so the tags are present in the initial HTML that crawlers read.

app/about/page.tsx
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'About',
description: 'Who we are and what we build.',
openGraph: { title: 'About Acme', images: ['/og.png'] },
}
export default function AboutPage() {
return <h1>About</h1>
}

When the tags depend on data — a blog post, a product — export generateMetadata instead. It receives the route params, runs on the server, and returns the same Metadata shape. Fetches here are deduplicated against the ones in your page, so you do not pay twice.

app/blog/[slug]/page.tsx
import type { Metadata } from 'next'
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>
}): Promise<Metadata> {
const { slug } = await params
const post = await fetch(`https://api.example.com/posts/${slug}`).then((r) => r.json())
return { title: post.title, description: post.excerpt }
}

A layout can define a title.template so every child page gets a consistent suffix. A default is required alongside the template.

// app/layout.tsx — every child title becomes "<page> | Acme"
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: {
template: '%s | Acme',
default: 'Acme',
},
}

Some metadata is a file, not a field. Drop a specially named file into a route segment and Next.js wires it up: opengraph-image.tsx generates a social preview, icon.png becomes the favicon, and sitemap.ts and robots.ts become their respective routes. These special routes are cached by default.

// app/opengraph-image.tsx — generated, cached social image
import { ImageResponse } from 'next/og'
export const size = { width: 1200, height: 630 }
export const contentType = 'image/png'
export default function Image() {
return new ImageResponse(<div style={{ fontSize: 96 }}>Acme</div>, size)
}
// app/sitemap.ts — becomes /sitemap.xml
import type { MetadataRoute } from 'next'
export default function sitemap(): MetadataRoute.Sitemap {
return [{ url: 'https://acme.com', lastModified: new Date() }]
}

Because everything is data or a file, there is no <head> to keep in sync by hand — the template merges parent and child, and duplicate tags cannot drift apart.

flowchart TD
  A["metadata object"] --> M[Merge parent + child]
  B["generateMetadata(params)"] --> M
  C["opengraph-image / sitemap / robots files"] --> M
  M --> Head["Server-rendered head tags"]
How the Metadata API builds the head
When should you use generateMetadata instead of a static metadata object?
What does a title.template require alongside it?
How do you add a sitemap in the App Router?
Why does the Metadata API beat hand-managing the head?