Dynamic Metadata with the Next.js Metadata API

generateMetadata is the App Router’s answer to data-driven SEO: an async function, colocated with a route segment, that fetches the route’s data and returns the metadata Next renders into the head — server-side, in the response. Because it runs on the server, crawlers and social scrapers see the resolved tags without executing any client JavaScript. This is the implementation detail under Next.js App Router SEO.

generateMetadata and the page share one deduped fetch during server render On the server, generateMetadata and the page component both call getPost, deduplicated within the request, producing the resolved head and body of a single HTML response. One request, resolved on the server before the response ships Server render getPost() fetch • deduped generateMetadata() title, canonical, OG Page (server) body + JSON-LD HTML response <head> resolved tags <body> content + JSON-LD
Because generateMetadata resolves on the server, crawlers and scrapers receive the tags in the response without running any client JavaScript.

Step-by-step fix

  1. Export an async generateMetadata from the segment.

    // app/blog/[slug]/page.jsx
    export async function generateMetadata({ params }) {
      const post = await getPost(params.slug);
      return {
        title: post.title,
        description: post.summary,
        alternates: { canonical: `https://example.com/blog/${params.slug}` },
        openGraph: { title: post.title, images: [post.ogImage] },
        twitter: { card: 'summary_large_image' },
      };
    }
  2. Let Next dedupe the fetch. Identical fetch calls in generateMetadata and the component are deduplicated within a request, so fetching the post in both is not a double round-trip.

    // ❌ Separate clients/uncached fetches → duplicate work, slower TTFB
    // ✅ Use fetch() so Next dedupes between generateMetadata and the page
    async function getPost(slug) {
      const res = await fetch(`https://api.example.com/posts/${slug}`, { next: { revalidate: 3600 } });
      return res.json();
    }
  3. Generate per-route OG images. Add an opengraph-image to the segment so each route has a unique image.

    // app/blog/[slug]/opengraph-image.jsx
    import { ImageResponse } from 'next/og';
    export default async function Image({ params }) {
      const post = await getPost(params.slug);
      return new ImageResponse(<div style={{ fontSize: 64 }}>{post.title}</div>);
    }
  4. Render JSON-LD in the server component body so structured data ships in the response.

    export default async function Page({ params }) {
      const post = await getPost(params.slug);
      const ld = { '@context': 'https://schema.org', '@type': 'Article', headline: post.title };
      return (<>
        <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(ld) }} />
        <Article post={post} />
      </>);
    }
One opengraph-image file renders a unique share card per route A single opengraph-image file in the route segment runs ImageResponse per route, producing a distinct Open Graph card with its own image and title for each blog post. opengraph-image renders a distinct card for every route opengraph-image.jsx ImageResponse, per route Post A Post A — Example /blog/a Post B Post B — Example /blog/b Post C Post C — Example /blog/c
Colocating an opengraph-image with the segment gives each route a unique social card at request or build time, referenced straight from generateMetadata.

Validation

  • curl of the route shows the resolved title, canonical, and OG tags in the response.
  • GSC URL Inspection rendered HTML matches the live head.
  • OG image renders correctly in social debuggers and is unique per route.
  • Rich Results Test validates the Article JSON-LD.
Four independent checks that dynamic metadata reached the response A verification checklist: curl shows resolved tags, Search Console rendered HTML matches, the social debugger shows a unique Open Graph image, and the Rich Results Test validates the JSON-LD. Verify the server rendered the tags — four tools, one pass each curl of the route resolved title, canonical, and OG tags are in the raw HTML GSC URL Inspection rendered HTML matches the live head Social debugger Open Graph image renders and is unique per route Rich Results Test Article JSON-LD validates against the schema
Each row is an independent confirmation that generateMetadata resolved server-side and its output reached the response a crawler reads.

Reference

// app/products/[id]/page.jsx — metadata + structured data, fully server-rendered
async function getProduct(id) {
  const res = await fetch(`https://api.example.com/products/${id}`, { next: { revalidate: 600 } });
  return res.json();
}
export async function generateMetadata({ params }) {
  const p = await getProduct(params.id); // deduped with the page fetch
  return {
    title: `${p.name} — Example`,
    description: p.summary,
    alternates: { canonical: `https://example.com/products/${p.id}` },
    openGraph: { title: p.name, images: [p.image] },
  };
}
How the generateMetadata return maps to head tags Each field the product route's generateMetadata returns — title, description, canonical, and Open Graph — becomes a corresponding tag in the server-rendered head of the response. Each returned field becomes a server-rendered head tag generateMetadata() returns title description alternates.canonical openGraph.title, images Rendered <head> <title> <meta name="description"> <link rel="canonical"> <meta property="og:*">
Every key the function returns resolves server-side into its head tag, so the crawler reads them straight from the HTML response.

Frequently Asked Questions

Does generateMetadata run on the server? Yes. generateMetadata runs on the server during rendering, so the tags it returns are present in the HTML response. That is why it is reliable for crawlers and social scrapers that do not execute client JavaScript.

How do I generate per-route Open Graph images in Next.js? Use an opengraph-image file in the route segment, or the ImageResponse API in a route handler, to render an image per route at request or build time. Reference it from generateMetadata so each page ships a unique OG image.

← Back to Next.js App Router SEO