generateMetadata
Compute a route's title, description and social tags from params, search params or fetched data.
export async function generateMetadata(
{ params, searchParams }: { params: Promise<Record<string, string | string[]>>; searchParams: Promise<{ [key: string]: string | string[] | undefined }> },
parent: ResolvingMetadata
): Promise<Metadata>- Added in Next.js v13.2.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$props.params | Promise<Record<string, string | string[]>> | Dynamic route parameters from the root segment down to the segment this function is exported from. A catch-all segment resolves to an array of strings. |
$props.searchParamsoptional | Promise<{ [key: string]: string | string[] | undefined }> | The current URL's query string. A repeated key resolves to an array. Only available in page.js segments, never in layout.js. |
$parentoptional | ResolvingMetadata | A promise of the metadata already resolved by parent segments, so a child can extend a parent value rather than replace it. |
Return value
Promise<Metadata> | MetadataA Metadata object whose fields become <head> tags. Fields are shallowly merged with the metadata resolved from parent segments; duplicate keys are replaced by the segment nearest the page.
Errors
Exporting both the metadata object and generateMetadata from the same route segment is an error, as is using a relative URL in a URL-based field without a metadataBase.
Changelog
- PHP v15.2.0Metadata resolved by generateMetadata can stream: the initial UI is sent first and the tags are appended to <body> when it resolves. HTML-limited bots detected by User-Agent still get blocking, in-<head> metadata; override the list with htmlLimitedBots.
- PHP v15.0.0params and searchParams are Promises and must be awaited.
- PHP v14.0.0viewport, themeColor and colorScheme were deprecated here in favour of the separate viewport export and generateViewport.
Reach for generateMetadata only when the tags depend on something you cannot know at build authoring time: the route params, the query string, or a record you have to fetch. If the values are literals, use the static metadata export instead — it is cheaper and it never blocks.
What trips people up
- Merging is shallow. If a page sets
openGraph.title, every otheropenGraphfield from the layout is dropped, not merged in. Pull shared nested objects into a variable and spread them. - Server Components only. A file with
'use client'cannot export it. Keep the page a Server Component and push interactivity into a child. - File-based metadata wins. An
opengraph-image.tsxnext to the page overrides anopenGraph.imagesvalue you return here. - One or the other. A segment may export
metadataorgenerateMetadata, never both.
fetch calls are memoized across generateMetadata, generateStaticParams, the layout and the page, so requesting the same record in both the metadata function and the component costs one round trip. Use React's cache when the data source is not fetch. redirect() and notFound() are both legal inside it.
Example
import type { Metadata, ResolvingMetadata } from 'next'
type Props = {
params: Promise<{ slug: string }>
searchParams: Promise<{ [key: string]: string | string[] | undefined }>
}
export async function generateMetadata(
{ params }: Props,
parent: ResolvingMetadata
): Promise<Metadata> {
const { slug } = await params
const post = await fetch(`https://api.example.com/posts/${slug}`).then((res) =>
res.json()
)
// Extend the parent images instead of replacing them
const previousImages = (await parent).openGraph?.images || []
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
images: [`/og/${slug}.png`, ...previousImages],
},
}
}
export default async function Page({ params }: Props) {
const { slug } = await params
const post = await fetch(`https://api.example.com/posts/${slug}`).then((res) =>
res.json()
)
return <article>{post.body}</article>
}When to use it
- Set a product page's title and OG image from the product record fetched by slug.
- Append a page-specific OG image to the images the root layout already declared.
- Mark a preview or filtered listing noindex based on a search param.
- Emit a canonical URL per locale for a route rendered under several language prefixes.
Frequently asked questions
Why is my generateMetadata not running?
The segment is almost certainly a Client Component: the metadata exports are only supported in Server Components, and a 'use client' directive at the top of page.tsx makes the export inert with no error. Move the interactive part into a child component and keep page.tsx as a Server Component. The other common cause is exporting both metadata and generateMetadata from the same file, or having a file-based convention such as opengraph-image.tsx that takes priority.
Can I use generateMetadata in layout.tsx?
Yes, but the layout version receives only params — searchParams is undefined outside page.js segments, because a layout is shared across the queries beneath it. Layouts are the right place for metadataBase, title.template and site-wide openGraph defaults, and pages then override individual keys.
Why did my openGraph description disappear on one page?
Metadata merges shallowly, so a page that sets any openGraph field replaces the whole openGraph object inherited from the layout. Re-declare the fields you still want, or export the shared object from a module and spread it into each segment's openGraph.
More examples
Title template in the layout, title in the page
Visiting /blog/hello renders <title>hello | Acme</title>, because the page's string title is substituted into the layout's %s. Visiting /blog/changelog renders <title>Changelog</title> — title.absolute opts out of every parent template. A route that sets no title at all falls back to title.default, giving <title>Acme</title>. Note that the template never applies to the segment that declares it.
// app/layout.tsx
import type { Metadata } from 'next'
export const metadata: Metadata = {
metadataBase: new URL('https://acme.com'),
title: {
template: '%s | Acme',
default: 'Acme', // required whenever a template is set
},
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}
// app/blog/[slug]/page.tsx
import type { Metadata } from 'next'
type Props = { params: Promise<{ slug: string }> }
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params
if (slug === 'changelog') {
// Ignore the parent template entirely
return { title: { absolute: 'Changelog' } }
}
return { title: slug }
}Noindex a filtered listing, canonicalise to the clean URL
For /search?q=boots the head contains <title>Results for “boots” | Acme</title>, <meta name="robots" content="noindex, follow" /> and <link rel="canonical" href="https://acme.com/search" /> — the canonical is absolute because the root layout set metadataBase. For bare /search the robots tag is absent, so the page indexes normally. searchParams is only readable here because this is a page.js segment; the same code in layout.tsx would not receive it.
// app/search/page.tsx
import type { Metadata } from 'next'
type Props = {
searchParams: Promise<{ q?: string; page?: string }>
}
export async function generateMetadata({
searchParams,
}: Props): Promise<Metadata> {
const { q } = await searchParams
if (!q) {
return {
title: 'Search',
alternates: { canonical: '/search' },
}
}
return {
title: `Results for "${q}"`,
robots: {
index: false,
follow: true,
},
alternates: { canonical: '/search' },
}
}
export default async function Page({ searchParams }: Props) {
const { q } = await searchParams
return <h1>{q ? `Results for ${q}` : 'Search'}</h1>
}Sharing nested OG fields across segments
Because this page defines an openGraph key at all, the layout's entire openGraph object is discarded — not merged field by field. Spreading openGraphImage puts the shared siteName, type and default image back, so the output carries og:site_name, og:type, og:image, og:title and og:description together. Omitting the spread would leave the page with og:title and og:description only.
// app/shared-metadata.ts
export const openGraphImage = {
images: ['/og/default.png'],
siteName: 'Acme',
type: 'website' as const,
}
// app/about/page.tsx
import type { Metadata } from 'next'
import { openGraphImage } from '../shared-metadata'
export async function generateMetadata(): Promise<Metadata> {
const team = await fetch('https://api.example.com/team').then((res) =>
res.json()
)
return {
title: 'About',
openGraph: {
...openGraphImage,
title: 'About',
description: `${team.length} people building Acme`,
},
}
}
export default function Page() {
return <h1>About</h1>
}
Discussion