`generateMetadata` Explained with Real Examples

What generateMetadata does, when it runs, and how to use it for real routes: awaited params, deduplicated data fetching, extending parent metadata, and returning a 404 from metadata.

9 min read

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

typescripttypescript
// 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.tsxApp.tsx
// 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.tsxApp.tsx
// 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.

The status code depends on streaming

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

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.
RunePowered by Rune AI

Frequently Asked Questions

Does generateMetadata run on every request?

It runs as part of rendering the route. On a prerendered route that happens at build time, and on a dynamically rendered route it happens per request.

Will my data be fetched twice if the page needs it too?

Not if the call is deduplicated. Identical fetch requests are memoized across generateMetadata and the page, and a non-fetch data source can be wrapped in the React cache function to get the same effect.

Can I use cookies or headers inside generateMetadata?

Yes, and both must be awaited. Reading them makes the metadata depend on request data, which affects prerendering and, with Cache Components enabled, requires you to mark that choice explicitly.

Can generateMetadata return a 404?

Yes. Calling notFound inside the function renders the closest not-found UI and adds a noindex tag. The response carries a real 404 status when nothing has streamed yet, and a 200 once streaming has started, because headers cannot change after they are sent.

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.