generateMetadata is an async function you export from a layout or page to build that route's metadata from data. It receives the route params and search params, returns a metadata object, and runs on the server before the page component renders. Use it whenever the title, description, or social tags depend on something you have to look up.
The examples here were verified with Next.js 16.3 in an App Router project using the default caching model, with the Cache Components flag off. A note at the end covers what changes when that flag is on.
The smallest working example
A product route needs the product name in the title, so the function reads the dynamic segment and returns the fields that depend on it. Params arrive as a promise, which is why the first line awaits them.
// app/products/[id]/page.tsx
import type { Metadata } from 'next'
import { getProduct } from '@/app/lib/products'
export async function generateMetadata({
params,
}: PageProps<'/products/[id]'>): Promise<Metadata> {
const { id } = await params
const product = await getProduct(id)
return { title: product.name, description: product.summary }
}Requesting /products/42 resolves this before any HTML is sent, so the rendered head carries the product name. If the root layout defines a title template, the returned title is composed with it, giving something like <title>Cordless Drill | Acme Store</title>.
Typing the argument with the generated PageProps helper is the least effort way to get the param names right. Writing the type by hand works too, as long as params stays a promise.
Do not fetch the same record twice
The page almost always needs the record that the metadata needed. Calling the data function in both places is correct but wasteful unless the call is deduplicated, and the React cache function is the general solution for data sources that are not fetch.
// app/lib/products.ts
import { cache } from 'react'
import { db } from '@/app/lib/db'
export const getProduct = cache(async (id: string) => {
return db.product.findUnique({ where: { id } })
})Both callers now share one result for the duration of the request, so the database is queried once. Requests made with fetch are memoized already for identical calls across the metadata function, the layout, and the page, so wrapping them adds nothing.
This matters more than it looks on a product or article page, where the metadata function, the page, and often an image route all want the same record. The distinction between this and the other caching layers is covered in use cache versus React cache.
Extending parent metadata instead of replacing it
The second argument resolves to the metadata already produced by parent segments. It exists because nested objects are replaced during merging, so a page that sets its own social image would otherwise drop the one from the root layout.
// app/blog/[slug]/page.tsx
import type { Metadata, ResolvingMetadata } from 'next'
export async function generateMetadata(
{ params }: PageProps<'/blog/[slug]'>,
parent: ResolvingMetadata
): Promise<Metadata> {
const { slug } = await params
const inherited = (await parent).openGraph?.images || []
return { openGraph: { images: [`/og/${slug}.png`, ...inherited] } }
}The rendered page now has two Open Graph image tags, the post-specific one first and the inherited default second. Crawlers use the first usable image, so ordering is the whole point of the spread.
Awaiting the parent has a cost worth knowing. It waits for parent segments to resolve their metadata, so only reach for it when you actually intend to extend an inherited value.
Returning a 404 from metadata
When the record behind a dynamic route does not exist, the route should say so rather than render an empty page with a default title. The navigation helpers work inside the metadata function for exactly this reason.
// app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation'
import { getPost } from '@/app/lib/posts'
export async function generateMetadata({ params }: PageProps<'/blog/[slug]'>) {
const { slug } = await params
const post = await getPost(slug)
if (!post) notFound()
return { title: post.title, alternates: { canonical: `/blog/${slug}` } }
}Requesting a slug that does not exist now renders the closest not-found UI, and Next.js injects a noindex tag so the URL stays out of search results. The redirect helper works in the same position when a record has moved.
A real 404 status is only possible while the response headers are still unsent, which covers prerendered routes and requests from bots that receive blocking metadata. Once streaming has started, the not-found UI arrives in a 200 response and the noindex tag is what keeps the URL unindexed.
Because the page usually repeats this check, keep both branches reading the same deduplicated function so they can never disagree about whether the record exists.
Search params, cookies, and request data
Search params are available only in a page segment, never in a layout, because a layout does not re-render for a query string change. Cookies and headers are available in both, and both are async APIs that must be awaited.
// app/search/page.tsx
import type { Metadata } from 'next'
export async function generateMetadata({
searchParams,
}: PageProps<'/search'>): Promise<Metadata> {
const { q } = await searchParams
return { title: q ? `Results for ${q}` : 'Search', robots: { index: false } }
}A request to /search?q=drill renders a title built from the query, and the robots field keeps the result page out of the index, which is the usual choice for search listings. Reading search params also means the route renders at request time, since the value is unknown during a build.
The behavior of these request-time APIs across rendering models is covered in dynamic APIs such as cookies, headers, and searchParams.
What changes with Cache Components
With the Cache Components flag enabled, the metadata function follows the same rules as any other component. If it reads request data or performs uncached fetching while the rest of the route is fully prerenderable, Next.js raises an error instead of silently making the route dynamic.
Two documented resolutions exist. Add the cache directive inside the function when the data is external but not request-specific, or mark the page as intentionally dynamic when the metadata genuinely needs request data.
// app/page.tsx
export async function generateMetadata() {
'use cache'
const site = await getSiteSettings()
return { title: site.title, description: site.description }
}Cached metadata is now part of the prerendered shell, and the value must be serializable, which is why a base URL belongs there as a string rather than as a URL instance. The tradeoffs of enabling the flag are covered in Cache Components explained.
Common mistakes
Each of these produces working-looking code that fails in a specific, quiet way.
- Using params without awaiting them, which yields a promise rather than the segment value.
- Fetching the record in both the metadata function and the page through different helpers, doubling the database work.
- Returning only a nested Open Graph object and unintentionally dropping the site name and default image from the parent layout.
- Rendering a friendly "not found" page with a 200 status instead of calling the not-found helper.
- Exporting the static metadata object alongside this function, which stops the build with a message saying to keep only one.
Rune AI
Key Insights
- generateMetadata is a Server Component export that resolves before the page renders.
- Route params and search params are promises, so await them.
- Share one deduplicated data function between metadata and the page instead of fetching twice.
- The second argument resolves to parent metadata, which lets you extend inherited fields.
- Calling notFound inside the function renders the not-found UI and keeps the URL out of the index.
Frequently Asked Questions
Does generateMetadata run on every request?
Will my data be fetched twice if the page needs it too?
Can I use cookies or headers inside generateMetadata?
Can generateMetadata return a 404?
Conclusion
generateMetadata is an async server function that returns the metadata object for one route segment. Await params before using them, reuse the same deduplicated data function the page uses, extend parent metadata through the second argument when you want to keep inherited images, and let it call notFound when the record is missing.
More in this topic
Canonical URLs in Next.js: `metadataBase`, `alternates.canonical`, and Dynamic Pages
How canonical URLs work in the Next.js App Router: setting metadataBase once, writing alternates.canonical per route, handling dynamic segments, and what happens when the base URL is missing.
Open Graph and Twitter Card Metadata in Next.js
How to write Open Graph and Twitter card metadata in the Next.js App Router: the openGraph and twitter fields, automatic card defaults, article tags, and image merge rules.
Next.js SEO Guide for the App Router: Complete Checklist
A practical Next.js SEO checklist for the App Router: metadata baseline, canonical URLs, sitemap and robots files, social images, indexing control, and how to verify the rendered HTML.