Metadata and SEO
The idea in one sentence
Section titled “The idea in one sentence”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.
Static metadata
Section titled “Static metadata”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.
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>}Dynamic metadata and title templates
Section titled “Dynamic metadata and title templates”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.
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', },}File-based metadata
Section titled “File-based metadata”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 imageimport { 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.xmlimport 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"]