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/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:
<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/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.
| Question | Static metadata object | generateMetadata |
|---|---|---|
| Values known at author time | Yes | Unnecessary |
| Depends on params, search params, or fetched data | Not possible | Yes |
| Runs per request | No, evaluated once per module | Yes, as part of rendering |
| Allowed in a Client Component | No | No |
Exporting both from the same file is a build error, and the message says so directly.
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.
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/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.
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.
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
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.
Frequently Asked Questions
Can I export both metadata and generateMetadata from one file?
Why can a Client Component not export metadata?
Do child segments inherit metadata from a parent layout?
Where does the Metadata API put the tags?
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.
More in this topic
`generateMetadata` Explained with Real Examples
What generateMetadata does, when it runs, and how to use it for real routes: awaited params, deduplicated data fetching, extending parent metadata, and returning a 404 from metadata.
Canonical URLs in Next.js: `metadataBase`, `alternates.canonical`, and Dynamic Pages
How canonical URLs work in the Next.js App Router: setting metadataBase once, writing alternates.canonical per route, handling dynamic segments, and what happens when the base URL is missing.
Open Graph and Twitter Card Metadata in Next.js
How to write Open Graph and Twitter card metadata in the Next.js App Router: the openGraph and twitter fields, automatic card defaults, article tags, and image merge rules.