Type something to search...
Next.js  generateMetadata function

Next.js generateMetadata function

Every page you ship eventually gets shared somewhere — pasted into Slack, tweeted, indexed by Google, unfurled in an iMessage thread. What determines whether that share looks like a polished card with a title, description, and image, or a bare URL with no context, is metadata: the <title>, <meta>, and <link> tags sitting in your document's <head>. Next.js gives you two ways to produce all of that from plain JavaScript objects instead of hand-writing HTML — the static metadata export and the dynamic generateMetadata function — and between them they cover a genuinely enormous surface area: Open Graph, Twitter Cards, robots directives, icons, verification tags, app deep links, and a dozen more specialized fields most teams don't touch until they need exactly one of them.

This is the exhaustive reference for that API. If you've only seen a metadata object with a title and description in a starter template, this covers everything else it can do — every field, every merge rule, and the newer behavior around streaming metadata and Cache Components that changes how you think about metadata that depends on runtime data.

The Static metadata Object

When your metadata doesn't depend on anything computed at request time, export a plain metadata object from a layout.tsx or page.tsx file:

import type { Metadata } from "next";

export const metadata: Metadata = {
  title: "Acme Dashboard",
  description: "Manage your Acme account and billing.",
};

export default function Page() {
  return <main>...</main>;
}

This is resolved once, at build time if the route is static, and baked directly into the prerendered HTML. There's no function call, no async work, nothing to await — it's just a typed object, and typing it with Metadata from next gets you autocomplete across every field this article covers.

The generateMetadata Function

The moment your metadata needs the current route's params, an external data fetch, or the resolved metadata of a parent segment, you switch to generateMetadata:

import type { Metadata, ResolvingMetadata } from "next";

type Props = {
  params: Promise<{ id: string }>;
  searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
};

export async function generateMetadata(
  { params, searchParams }: Props,
  parent: ResolvingMetadata,
): Promise<Metadata> {
  const { id } = await params;
  const product = await fetch(`https://.../${id}`).then((res) => res.json());

  // Extend rather than replace the parent's images
  const previousImages = (await parent).openGraph?.images || [];

  return {
    title: product.title,
    openGraph: {
      images: ["/some-specific-page-image.jpg", ...previousImages],
    },
  };
}

export default function Page({ params, searchParams }: Props) {}

Note that params and searchParams are Promises, not plain objects — this trips up more people than any other part of the Metadata API, because it's easy to copy an old code sample that destructures them synchronously. For type completion without hand-rolling this shape, use the PageProps<'/route'> or LayoutProps<'/route'> helper types Next.js generates for your specific route.

A few rules govern both approaches:

  • Metadata can only be exported from layout.js and page.js files — not from arbitrary components nested inside them.
  • You cannot export both metadata and generateMetadata from the same file. Pick one per segment.
  • fetch calls inside generateMetadata are automatically deduplicated against identical calls in generateStaticParams, layouts, pages, and Server Components rendering the same request — so fetching the same product twice, once for metadata and once for the page body, costs one network round trip, not two.
  • File-based metadata (an icon.png, opengraph-image.tsx, etc. sitting in the route folder) always wins over anything you return from generateMetadata or the static object, if both target the same field.

Why generateMetadata Is Server-Component-Only

Both the metadata export and generateMetadata only work in Server Components, and this isn't an arbitrary restriction — metadata has to be fully resolved on the server before Next.js can construct the initial HTML response's <head>. There's no client-side equivalent because there's no "client-side head" to update before the first byte goes out.

In practice this means a page.tsx that needs 'use client' for interactivity has to split that logic into a separate file rather than marking the whole page client-side:

import type { Metadata } from "next";
import { InteractiveComponent } from "./interactive-component";

export const metadata: Metadata = {
  title: "My Page",
};

export default function Page() {
  return <InteractiveComponent />;
}
"use client";

export function InteractiveComponent() {
  // hooks, event handlers, browser APIs — all fine here
}

page.tsx stays a Server Component so its metadata export is valid, and all the client-only behavior lives one file down. This pattern shows up constantly once you internalize it — it's the same reason a root layout that needs a theme toggle wraps its children in a small client-side provider component rather than becoming a client component itself.

generateMetadata Parameters and Return Value

The function receives two arguments:

  • props — an object with params (the dynamic route segments as a Promise, from the root down to wherever generateMetadata is defined) and searchParams (a Promise of the current URL's query string, only available in page.js segments, not layouts).
  • parent — a Promise resolving to the metadata object accumulated by parent segments so far, letting you read and extend it rather than blindly overwrite it.
RouteURLparams
app/shop/[slug]/page.js/shop/1{ slug: '1' }
app/shop/[tag]/[item]/page.js/shop/1/2{ tag: '1', item: '2' }
app/shop/[...slug]/page.js/shop/1/2{ slug: ['1', '2'] }
URLsearchParams
/shop?a=1{ a: '1' }
/shop?a=1&b=2{ a: '1', b: '2' }
/shop?a=1&a=2{ a: ['1', '2'] }

The function should return a Metadata object with any subset of the fields below. And here's a rule worth internalizing early: if your metadata doesn't actually depend on request information, use the static metadata object instead of generateMetadata. A generateMetadata function that just returns a hardcoded object gains you nothing over the static export, and it forces Next.js to treat that segment's metadata resolution as a function call rather than a build-time constant. You can also call redirect() or notFound() from inside generateMetadata if the data lookup fails.

The Full Metadata Fields Reference

This is where the API earns the word "exhaustive." Most projects use ten of these fields; knowing the other thirty exist saves you from reaching for a raw <meta> tag or a third-party package when Next.js already has a typed option for it.

title

Accepts either a plain string, or an object with default, template, and absolute:

export const metadata = {
  title: {
    template: "%s | Acme",
    default: "Acme", // required whenever you set a template
  },
};
export const metadata = {
  title: "About",
};
// Renders: <title>About | Acme</title>
  • default supplies a fallback for any child segment that doesn't set its own title.
  • template wraps whatever title a child segment defines — it never applies to the segment that declares it. A template in page.js is a no-op, since a page has no children to template.
  • absolute opts a segment out of any inherited template, useful for a one-off page (a legal page, an embed) that shouldn't get the site-wide suffix.

The gotcha that catches almost everyone once: define title.template without also defining title.default, and TypeScript will complain, because a template with nothing to fall back to for un-titled children is meaningless.

description and the small stuff

export const metadata = {
  description: "The React Framework for the Web",
  generator: "Next.js",
  applicationName: "Next.js",
  referrer: "origin-when-cross-origin",
  keywords: ["Next.js", "React", "JavaScript"],
  authors: [{ name: "Seb" }, { name: "Josh", url: "https://nextjs.org" }],
  creator: "Jiachi Liu",
  publisher: "Sebastian Markbåge",
  formatDetection: { email: false, address: false, telephone: false },
};

Most of these map one-to-one onto a <meta name="..."> tag. formatDetection is worth knowing about specifically — mobile browsers auto-link things that look like phone numbers or addresses in your page text, which is rarely what you want for a SaaS dashboard, and this field turns that off explicitly.

metadataBase and URL composition

Any metadata field that needs a fully-qualified URL — openGraph.images, alternates.canonical, and others — normally requires you to write the absolute URL out every time. metadataBase removes that requirement for every URL-based field in the current segment and below:

export const metadata = {
  metadataBase: new URL("https://acme.com"),
  alternates: { canonical: "/" },
  openGraph: { images: "/og-image.png" },
};

With metadataBase set, /og-image.png resolves to https://acme.com/og-image.png automatically. Skip it, use a relative path anyway, and Next.js throws a build error — this is one of the few metadata mistakes that fails loudly rather than silently producing a broken tag, which is a mercy. Set it once, in the root app/layout.js, and forget about it everywhere else.

URL composition here favors intent over strict path semantics: an "absolute" path like /payments in a metadata field is treated as relative to the end of metadataBase, not as a directory-traversal root reset. Given metadataBase: new URL('https://acme.com'), both payments and /payments resolve identically to https://acme.com/payments — there's no meaningful distinction between the leading-slash and no-leading-slash forms the way there would be in a <a href>.

openGraph

The field most people actually came here for — controls how links look when shared on Facebook, LinkedIn, Slack, iMessage, and most chat apps that unfurl links:

export const metadata = {
  openGraph: {
    title: "Next.js",
    description: "The React Framework for the Web",
    url: "https://nextjs.org",
    siteName: "Next.js",
    images: [
      { url: "https://nextjs.org/og.png", width: 800, height: 600 },
      {
        url: "https://nextjs.org/og-alt.png",
        width: 1800,
        height: 1600,
        alt: "My custom alt",
      },
    ],
    locale: "en_US",
    type: "website",
  },
};

type: 'article' unlocks article-specific sub-fields like publishedTime and authors, rendered as article:published_time and article:author tags. One practical note the docs undersell: for the image itself, you're often better off using the file-based convention — an opengraph-image.tsx or opengraph-image.png sitting right in the route folder — rather than this config object, because the file convention generates the correct dimensions and meta tags for you without you manually syncing a URL string to whatever you actually deployed.

robots

export const metadata: Metadata = {
  robots: {
    index: true,
    follow: true,
    googleBot: {
      index: true,
      follow: true,
      "max-image-preview": "large",
      "max-snippet": -1,
    },
  },
};

This is the typed, structured way to write what used to be a raw <meta name="robots" content="..."> string, and it's worth using even on pages where you're not trying to block indexing — explicit index: true, follow: true is cheap insurance against a future default change.

icons

export const metadata = {
  icons: {
    icon: [
      { url: "/icon.png" },
      { url: "/icon-dark.png", media: "(prefers-color-scheme: dark)" },
    ],
    apple: [{ url: "/apple-icon-x3.png", sizes: "180x180", type: "image/png" }],
  },
};

Same advice as Open Graph images: for anything beyond a single static favicon, the file-based conventions (icon.png, apple-icon.png in app/) generate correct metadata automatically and are less fragile than keeping this config object in sync with files on disk.

twitter

export const metadata = {
  twitter: {
    card: "summary_large_image",
    site: "@nextjs",
    creator: "@nextjs",
    images: ["https://nextjs.org/og.png"],
  },
};

Despite the name, this "Twitter" spec is used by more platforms than just X — a twitter:card tag is honored by several unfurl-preview systems that never had their own equivalent standard. card: 'app' supports a deep-link variant pointing at iOS/Android/web app URLs specifically, useful if the page itself is a marketing surface for a native app.

themeColor, colorScheme, viewport — all deprecated here

If you're on an older tutorial that sets themeColor or viewport inside the metadata object, know that as of Next.js 14 these three fields moved to a separate generateViewport export. Setting them inside metadata will silently do nothing on a current install — this is a common "why isn't my theme color showing up" bug for anyone following an outdated guide.

manifest, alternates, verification, appleWebApp, appLinks, archives/assets/bookmarks/pagination, category, facebook, pinterest

The long tail, briefly:

  • manifest links a PWA web app manifest file.
  • alternates covers canonical URLs, languages for hreflang, media, and types (e.g. pointing at an RSS feed).
  • verification emits the meta tags Google Search Console, Yandex, and Yahoo ask you to paste in to prove domain ownership — plus an other escape hatch for anything else.
  • appleWebApp and appLinks target "add to home screen" behavior on iOS and app-to-app deep linking respectively.
  • archives, assets, bookmarks, and pagination map to niche <link rel> values (prev/next for paginated content is the one you'll actually use, for blog archives or search results).
  • facebook and pinterest connect Facebook's app/admin IDs or toggle Pinterest Rich Pins — narrow, but exactly what you need if a client asks for either specifically.
export const metadata = {
  facebook: { appId: "12345678" },
  pinterest: { richPin: true },
  category: "technology",
};

other

The genuine escape hatch. If a brand-new platform ships a meta tag Next.js hasn't added typed support for yet, other renders it verbatim:

export const metadata = {
  other: {
    custom: ["meta1", "meta2"], // renders two <meta name="custom"> tags
  },
};

What's explicitly unsupported — and what to do instead

A handful of tags have no config-object equivalent because they don't belong in a metadata schema at all:

TagUse this instead
<meta http-equiv>Real HTTP headers, via redirect(), next.config.js headers, or Proxy
<base>, <noscript>Render directly in the layout/page JSX
<style>, <script>Normal CSS imports / the next/script component
<link rel="preload/preconnect/dns-prefetch">ReactDOM.preload(), ReactDOM.preconnect(), ReactDOM.prefetchDNS() from a Client Component

That last group is easy to miss — resource hints look like they should be metadata, but Next.js deliberately routes them through React DOM APIs instead, callable only from Client Components (which are still server-rendered on first load, so the hint still lands in the initial response).

Streaming Metadata

Historically, generateMetadata blocked the entire response — if your metadata needed a slow database call, your whole page waited on it. As of Next.js 15.2, metadata streams: Next.js sends the initial UI immediately and appends the resolved <title>/<meta> tags to the end of the <body> once generateMetadata finishes. Verified crawlers that execute JavaScript (Googlebot included) parse this correctly.

The catch is HTML-limited bots — crawlers like facebookexternalhit that fetch raw HTML without executing anything. For those, Next.js detects the user agent and falls back to blocking on metadata so the tags land in <head> where a non-JS parser expects them. You can override the detection list, or disable streaming outright, via htmlLimitedBots in next.config.js — but the default list is deliberately conservative, and widening it usually just slows down your real users' TTFB for no benefit.

Metadata Under Cache Components

If your project has Cache Components enabled, generateMetadata is subject to the same static/dynamic rules as everything else. Read cookies(), headers(), params, or searchParams inside it, or run an uncached fetch, and that segment's metadata defers to request time — which is fine if the rest of the page is also deferring. If the rest of the page is otherwise fully prerenderable, Next.js raises a build error rather than silently mixing a static shell with request-time metadata, because that combination is unusual enough that it's more likely a mistake than an intentional design.

Two fixes, depending on which situation you're in:

export async function generateMetadata() {
  "use cache";
  const { title, description } = await db.query("site-metadata");
  return { title, description };
}

If the metadata depends on external, cacheable data (not the current request), wrap it in 'use cache' and let Cache Components treat it like any other cacheable function.

If it genuinely needs runtime data — a personalized title from a session cookie — add an explicit dynamic marker elsewhere on the page so Next.js knows the deferral is intentional:

import { Suspense } from "react";
import { connection } from "next/server";

const Connection = async () => {
  await connection();
  return null;
};

async function DynamicMarker() {
  return (
    <Suspense>
      <Connection />
    </Suspense>
  );
}

export default function Page() {
  // Do NOT await connection() here directly — that would
  // prevent everything below from being part of the static shell.
  return (
    <>
      <article>Static content</article>
      <DynamicMarker />
    </>
  );
}

DynamicMarker renders nothing visible; its only job is telling the prerenderer "yes, this page really does have dynamic content, don't error." Wrapping it in Suspense keeps the rest of the page — the static <article> — eligible for the static shell regardless.

Ordering and Merging

Metadata resolves from the root segment down to the page: app/layout.tsxapp/blog/layout.tsxapp/blog/[slug]/page.tsx. Objects from each segment are shallowly merged, and duplicate top-level keys are replaced, not deep-merged:

export const metadata = {
  title: "Acme",
  openGraph: { title: "Acme", description: "Acme is a..." },
};
export const metadata = {
  title: "Blog",
  openGraph: { title: "Blog" },
};
// Renders <title>Blog</title> and <meta property="og:title" content="Blog">
// — note openGraph.description is gone, not inherited

This shallow-merge behavior is the single most common source of "why did my Open Graph description disappear" bugs. Setting openGraph at all in a child segment replaces the entire openGraph object from the parent — it doesn't merge field-by-field. If you want to share some Open Graph fields across pages while overriding others, pull the shared piece into its own variable and spread it explicitly:

export const openGraphImage = { images: ["http://.../shared.png"] };
import { openGraphImage } from "../shared-metadata";

export const metadata = {
  openGraph: { ...openGraphImage, title: "About" },
};

Contrast that with a segment that doesn't touch openGraph at all — in that case, the parent's openGraph object is inherited wholesale, unchanged. The rule to hold onto: omit a field to inherit it; set a field (even partially) to replace the whole thing.

Common Mistakes

Exporting both metadata and generateMetadata from the same file. Pick exactly one per segment — this isn't a lint warning, it's invalid.

Forgetting metadataBase. Any relative URL in an OG-image or canonical field without a metadataBase set somewhere up the tree fails the build outright. Set it once in the root layout and every child segment inherits it.

Assuming openGraph merges field-by-field across segments. It doesn't — see the ordering section above. This one is subtle enough that it survives in production for months before someone notices a share card is missing its description.

Putting 'use client' on a page that also exports metadata. Metadata exports are Server-Component-only; split the interactive piece into its own file instead of converting the whole page.

Treating generateMetadata as free. Any expensive, uncached call inside it runs on every request unless you cache it — with streaming metadata this no longer blocks your page, but it still costs real server time and, under Cache Components, can force an entire otherwise-static page into deferred rendering if you're not careful about where the dynamic read happens.

Key Takeaways

SituationWhat to reach for
Metadata is the same for every visitorStatic metadata object
Metadata depends on route params, external data, or parent metadatagenerateMetadata function
Need an absolute URL without hardcoding the domain everywheremetadataBase in the root layout
Sharing OG images across routes, or fully staticFile-based conventions (icon.png, opengraph-image.tsx) over config fields
themeColor / viewport / colorSchemeUse generateViewport, not metadata — these are deprecated here since v14
Metadata needs cookies/headers/params under Cache ComponentsWrap in 'use cache' if cacheable, or add a Suspense-wrapped dynamic marker if genuinely request-specific
A child segment needs to override only part of a nested field like openGraphPull the shared piece into its own variable and spread it in both segments

generateMetadata is one of those APIs that rewards reading the full field list once, rather than reaching for other or a manual <meta> tag the first time you need something slightly unusual — there's a very good chance Next.js already has a typed, well-tested field for exactly what you're trying to render.

Tags :
Share :

Related Posts

Can Next.js Be Used with GraphQL?

Can Next.js Be Used with GraphQL?

Next.js and GraphQL are two powerful technologies that have gained significant traction in the web development community. Next.js, a React-based fram

Dive Deeper
How does Next.js differ from Create React App?

How does Next.js differ from Create React App?

In the world of modern web development, React.js has emerged as a dominant force due to its flexibility, performance, and extensive ecosystem. Two po

Dive Deeper
How does Next.js handle image optimization?

How does Next.js handle image optimization?

In modern web development, image optimization plays a critical role in enhancing user experience and improving site performance. Large, unoptimized i

Dive Deeper