Skip to content

Dynamic Metadata

Static metadata works for fixed pages, but a blog post's title or a product's description needs metadata generated from real, fetched data. This lesson covers generateMetadata, the async counterpart to the static metadata export.

generateMetadata: Metadata from Fetched Data

generateMetadata is an async function you export from a page.tsx or layout.tsx, receiving the same params (and searchParams, for pages) that the route component itself receives. Whatever object it returns is used exactly like a static metadata export.

Because it's async, you can fetch the exact content the page is about to render — a blog post, a product, a user profile — and build metadata directly from that real data, ensuring titles and descriptions always match the actual page content.

// app/blog/[slug]/page.tsx
export async function generateMetadata({
  params,
}: {
  params: { slug: string };
}) {
  const post = await getPostBySlug(params.slug);

  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      images: [post.coverImage],
    },
  };
}

export default async function BlogPostPage({
  params,
}: {
  params: { slug: string };
}) {
  const post = await getPostBySlug(params.slug);
  return <article><h1>{post.title}</h1></article>;
}

Next.js automatically deduplicates the getPostBySlug call here, so fetching the same data in both functions doesn't cause two network requests.

generateMetadata Function Signature

export async function generateMetadata({
  params,
  searchParams,
}: {
  params: Record<string, string>;
  searchParams: Record<string, string | string[] | undefined>;
}) {
  // fetch data, then return a metadata object
  return { title: "...", description: "..." };
}
  • generateMetadata runs before the page component renders, and can be async.
  • It receives the same params (and searchParams on pages) as the route component.
  • Identical fetch() calls made in both generateMetadata and the page are automatically deduplicated.
  • A route can export either a static metadata object or generateMetadata, but not both.

Dynamic Metadata Cheat Sheet

When to use generateMetadata over the static export.

Situation Approach
Fixed page content (About, Contact) Static metadata export
Blog post, product, or profile page generateMetadata
Title depends on fetched data generateMetadata
Handling a missing resource (404) Call notFound() inside generateMetadata or the page

Handling Missing Content Gracefully

If the underlying data doesn't exist — an invalid blog slug, for example — generateMetadata should return sensible fallback metadata (or the page itself should call notFound()), rather than throwing an unhandled error or returning undefined values that produce blank tags.

export async function generateMetadata({
  params,
}: {
  params: { slug: string };
}) {
  const post = await getPostBySlug(params.slug);
  if (!post) {
    return { title: "Post Not Found" };
  }
  return { title: post.title, description: post.excerpt };
}

Reading Parent Metadata

generateMetadata can accept a second argument, parent, giving access to the resolved metadata from parent layouts — useful when you want to extend rather than fully replace an inherited value.

Common Mistakes

  • Fetching the same data in generateMetadata and the page component with slightly different arguments, defeating automatic deduplication.
  • Not handling missing/invalid data inside generateMetadata, leaving blank or broken metadata tags.
  • Trying to export both metadata and generateMetadata from the same file.
  • Forgetting generateMetadata runs on the server and cannot use client-only data.

Key Takeaways

  • generateMetadata builds metadata dynamically from fetched data, unlike the static metadata export.
  • It receives the same params/searchParams as the route component.
  • Identical fetch calls between generateMetadata and the page are deduplicated automatically.
  • Always handle missing/invalid content gracefully within generateMetadata.
  • A route uses either metadata or generateMetadata, never both.

Pro Tip

Extract your data-fetching function (like getPostBySlug) into a shared helper used by both generateMetadata and the page component — writing the exact same fetch call in both places, with identical arguments, is what enables Next.js's automatic request deduplication to actually kick in.