generateMetadata

Compute a route's title, description and social tags from params, search params or fetched data.

Signature
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

ParameterTypeDescription
$props.paramsPromise<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.searchParamsoptionalPromise<{ [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.
$parentoptionalResolvingMetadataA 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 other openGraph field 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.tsx next to the page overrides an openGraph.images value you return here.
  • One or the other. A segment may export metadata or generateMetadata, 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

Example · typescript
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.

Example · typescript
// 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.

Example · typescript
// 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.

Example · typescript
// 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

  • Be the first to comment on this lesson.