generateMetadata

generateMetadata for Dynamic Pages

When the tags depend on data, export an async generateMetadata. It takes the page's props — params and searchParams, promises since Next.js 16 10,514 — plus parent, resolving to what the parent segments produced.

Resolving metadata from the record the page rendersShell
export async function generateMetadata(
  { params }: Props, parent: Promise<Metadata>,
): Promise<Metadata> {
  const book = await getBook((await params).slug);
  if (!book) return { title: "Book not found" };
  return {
    title: book.title,
    alternates: { canonical: `/books/${book.slug}` },
    openGraph: { type: "article", title: book.title, authors: [book.author],
      publishedTime: book.published, images: (await parent).openGraph?.images ?? [] },
  };
}
Output
<title>The Pragmatic Programmer | Bookshelf</title>
<link rel="canonical" href="https://example.com/books/the-pragmatic-programmer"/>
<meta property="article:published_time" content="2019-09-13"/>
<meta property="article:author" content="Andrew Hunt"/>

The root layout's template wrapped the record's own title. Loading the record twice costs nothing: fetch is memoized across generateMetadata, generateStaticParams, layouts and the page within one request; for a database query, wrap the loader in React 7,897 's cache.

Awaiting data here would normally hold back the response, so Next.js streams metadata: the shell goes out at once and the resolved tags are appended to <body>, where React moves them into the head. Bots running JavaScript see the finished DOM; bots reading only raw HTML would not, so Next.js inspects the User-Agent and blocks for a known list. On a page whose generateMetadata waits 200 ms and whose body waits 1.5 s, an ordinary request put the Suspense fallback at byte 971 and <title> only at 7632; fetched as Twitterbot/1.0, <title> came at byte 750, inside <head>. Override that list with htmlLimitedBots; /.*/ disables streaming for all.