The Next.js Metadata API: Static and Dynamic Metadata

How the Next.js Metadata API works in the App Router: the static metadata object, the dynamic generateMetadata function, how segments merge, and what actually lands in the rendered head.

8 min read

The Next.js Metadata API is the App Router way to describe a document: titles, descriptions, social tags, canonical links, and robots rules. You declare metadata by exporting a static object for constant values, or an async function for values that depend on route params or fetched data. Next.js resolves both on the server and writes the tags into the response.

Both exports work in a layout or a page file, and both are supported only in Server Components. The examples below were verified with Next.js 16.3 in a project using the default caching model, with the Cache Components flag off.

Static metadata with the metadata object

Use the static object when the values never change between requests. It is a plain export, evaluated when the module is loaded, and the Metadata type gives you completion for every supported field.

App.tsxApp.tsx
// app/about/page.tsx
import type { Metadata } from 'next'
 
export const metadata: Metadata = {
  title: 'About',
  description: 'Who builds Acme and why.',
}
 
export default function Page() {
  return <h1>About</h1>
}

Requesting /about now returns a document whose head contains the title and description tags. Next.js also always emits the charset and viewport tags, even for a route that declares no metadata at all.

Two default fields you get for free are worth knowing about, because people often try to add them by hand:

htmlhtml
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />

The viewport tag is configurable through a separate export rather than through the metadata object, which the article on generateViewport and theme color covers.

Dynamic metadata with generateMetadata

When the title or description depends on the route, export an async function instead. It receives the same params and searchParams that the page receives, and both are promises in current versions of Next.js. A layout gets only params, because search params are passed to page segments alone.

App.tsxApp.tsx
// app/blog/[slug]/page.tsx
import type { Metadata } from 'next'
import { getPost } from '@/app/lib/posts'
 
export async function generateMetadata({
  params,
}: PageProps<'/blog/[slug]'>): Promise<Metadata> {
  const { slug } = await params
  const post = await getPost(slug)
  return { title: post.title, description: post.summary }
}

The imported helper is your own data function, whether it queries a database or calls an API. Visiting /blog/hello-world runs this function on the server before the page renders, so the resolved post title reaches the head. The generated PageProps helper types params for the exact route, so a typo in the segment name is a compile error.

The function is where data fetching for metadata belongs. Because the page usually needs the same record, generateMetadata deserves its own detailed walkthrough, including how to avoid fetching the post twice.

Choosing between the two

The decision is about whether the value depends on request or route information, not about how modern the API feels.

QuestionStatic metadata objectgenerateMetadata
Values known at author timeYesUnnecessary
Depends on params, search params, or fetched dataNot possibleYes
Runs per requestNo, evaluated once per moduleYes, as part of rendering
Allowed in a Client ComponentNoNo

Exporting both from the same file is a build error, and the message says so directly.

texttext
Error: "metadata" and "generateMetadata" cannot be exported at the same time, please keep one of them.

If you hit this while adding dynamic behavior to an existing page, delete the static object and move its fields into the return value of the function.

How segments merge

Metadata is evaluated from the root layout down to the page, and the result is merged shallowly. A field the child never mentions is inherited, and a field the child sets replaces the parent value.

Metadata resolution order for one route

The diagram shows the evaluation order for a request to /blog. Each segment contributes to one object, and the segment closest to the page wins on any key it defines.

Shallow merging has a consequence that surprises people. Suppose the root layout sets a site name and a default social image inside its Open Graph object, and a page sets only an Open Graph title.

App.tsxApp.tsx
// app/about/page.tsx
export const metadata = {
  title: 'About',
  openGraph: { title: 'About Acme' },
}

The rendered head for /about now contains the Open Graph title, and the site name and default image from the layout are gone, because the whole nested object was replaced. Sharing pieces between segments means pulling them into a variable and spreading it, or setting them again in the child, a tradeoff covered in the article on Open Graph and Twitter card metadata.

Both exports run on the server only

Metadata must resolve before the page component renders, so neither export is allowed in a file that carries the client directive. Adding one fails the build with a message that names the fix.

texttext
Error: You are attempting to export "metadata" from a component marked with "use client", which is disallowed.

The pattern that works is to keep the page as a Server Component and move the interactive part into a separate Client Component file. The page keeps its metadata export and simply renders the client child.

That boundary also protects secrets. Values you read while building metadata, such as a private API key used to fetch a post, stay on the server and never enter the client bundle.

Where the tags end up

For a prerendered route, metadata is resolved during the build and the tags are part of the initial HTML head. For a dynamically rendered route, Next.js streams the UI first and appends the metadata tags to the body once the function resolves.

That sounds alarming until you see the exception. Next.js inspects the user agent, and bots that expect tags in the head, including the crawlers used for social cards, receive blocking metadata instead.

bashbash
curl -s -A "Twitterbot/1.0" http://localhost:3000/dashboard | grep -o '<title>[^<]*</title>'

Running that against a production build of a dynamic route shows the title inside the head, while the same request without the bot user agent shows it after the page content. Both are correct, and no configuration is needed to get this behavior.

Common mistakes

Most Metadata API problems come from checking the wrong thing rather than from writing the wrong field.

  • Reading your metadata object instead of the rendered HTML, which hides every merge and inheritance effect.
  • Defining a nested Open Graph object in a child segment and losing the parent values.
  • Adding a title template in a page file, where it has no effect because a page has no child segments.
  • Trying to compute metadata from browser state, which is impossible because the API resolves before the page renders.
  • Writing social image paths as relative URLs without a base URL, which the article on canonical URLs and metadataBase explains in detail.

One habit prevents most of these. After changing metadata anywhere in a route, build the project, request the affected route, and read the tags in the response. The merged output is the only version that reaches a crawler, and it takes one command to see it.

Rune AI

Rune AI

Key Insights

  • Export the metadata object for constant values and generateMetadata for data-driven ones, never both from the same file.
  • Both exports are Server Component only and run before the page renders.
  • Metadata resolves from the root layout down to the page, and duplicate keys are replaced.
  • Nested objects such as openGraph are replaced rather than deep merged.
  • Prerendered routes get tags in the head, while dynamic routes stream them into the body for normal browsers.
RunePowered by Rune AI

Frequently Asked Questions

Can I export both metadata and generateMetadata from one file?

No. Next.js fails the build with the message that metadata and generateMetadata cannot be exported at the same time. Pick the static object when the values are constant, and the function when they depend on data.

Why can a Client Component not export metadata?

Metadata has to resolve on the server before the page renders, so the export is disallowed in a file marked with the use client directive. Keep the page as a Server Component and move the interactive part into its own file.

Do child segments inherit metadata from a parent layout?

Yes, but the merge is shallow. A field the child does not define is inherited, and a nested object such as openGraph is replaced in full when the child defines it.

Where does the Metadata API put the tags?

In the head for prerendered routes and for bots that cannot execute JavaScript. For dynamically rendered routes served to normal user agents, Next.js streams the tags and appends them to the body once they resolve.

Conclusion

The Metadata API gives you one static export for constant values and one async function for values that depend on data. Both run on the server, both merge from the root layout downward, and both are shallow merged, so nested objects such as openGraph belong wherever you want them to win. Read the rendered HTML to confirm the result.