Architecture Overview
Server Runtime
Rendering Pipeline
Client Navigation
Caching and Export
Development Tools
Build and Configuration
Ecosystem Packages
Testing Infrastructure
How It Works
The following files were used as context for generating this wiki page:
Metadata generation in Next.js provides a comprehensive, declarative system for defining, inheriting, and resolving document metadata and viewport configurations across the App Router hierarchy. By supporting both static metadata objects and dynamic asynchronous generateMetadata functions in Server Components, the system eliminates manual head-tag management while enforcing correct TypeScript contracts. It handles complex layout accumulation, default fallbacks, specialized social card processing, and URL resolution against metadataBase. Furthermore, it integrates tightly with the routing engine to identify special metadata files and compile route regular expressions, ultimately serializing processed metadata into React server head elements and static export artifacts. Sources: packages/next/src/lib/metadata/resolve-metadata.ts:1-87, packages/next/src/lib/metadata/metadata.tsx:35-63, packages/next/src/lib/metadata/types/metadata-interface.ts:1-14, packages/next/src/lib/metadata/get-metadata-route.ts:107-160
The Metadata API establishes public contracts and TypeScript interfaces for configuring document headers and viewport properties through static exports or dynamic asynchronous generation functions in Server Components. Next.js enforces strict separation: static metadata objects and generateMetadata functions are supported exclusively in Server Components, and routes must not export both a metadata object and a generateMetadata function from the same segment.
Dynamic metadata uses generateMetadata functions that receive segment properties containing asynchronous promises for route parameters and search parameters. Because dynamic parameters are asynchronous in the App Router architecture, functions must await props.params or props.searchParams before accessing dynamic route segments.
type MetadataProps = {
params: Promise<{ slug: string }>
}
export async function generateMetadata(props: MetadataProps) {
return {
title: (await props.params).slug,
}
}Warning
Do not export both a static metadata object and a dynamic generateMetadata function from the same route segment, as this creates an ambiguous resolution conflict.
Sources: packages/next/src/lib/metadata/types/metadata-interface.ts:8-9
The metadata subsystem defines structured types for standard HTML meta attributes, OpenGraph data, Twitter cards, alternative URLs, verification tokens, and viewport configurations.
Sources: packages/next/src/lib/metadata/types/metadata-interface.ts:540-564, packages/next/src/lib/metadata/types/metadata-interface.ts:765-797, packages/next/src/lib/metadata/types/metadata-types.ts:38-110
The hierarchical metadata resolution pipeline processes layout-to-page tree accumulation, default fallback merging, and staged evaluation to generate final page-level configurations. The pipeline starts by initializing default baseline structures through dedicated factory helpers before traversing the component tree.
export function createDefaultViewport(): ResolvedViewport {
return {
width: 'device-width',
initialScale: 1,
themeColor: null,
colorScheme: null,
}
}The base metadata structure initializes properties such as title, description, application name, and various structured metadata objects to null, establishing a consistent fallback base for accumulation across nested layouts.
export function createDefaultMetadata() {
return {
viewport: null,
themeColor: null,
colorScheme: null,
metadataBase: null,
title: null,
description: null,
applicationName: null,
authors: null,
generator: null,
keywords: null,
referrer: null,
creator: null,
publisher: null,
robots: null,
manifest: null,
alternates: { canonical: null, languages: null, media: null, types: null },
icons: null,
openGraph: null,
twitter: null,
verification: {},
appleWebApp: null,
formatDetection: null,
itunes: null,
facebook: null,
pinterest: null,
abstract: null,
appLinks: null,
archives: null,
assets: null,
bookmarks: null,
category: null,
classification: null,
pagination: { previous: null, next: null },
other: {},
}
}Static file metadata resolution merges file-based fallback icons and manifest assets into target resolution objects. When static file metadata contains open graph or twitter images and current level metadata does not specify them, the pipeline resolves URLs and converts instances to string values.
async function mergeStaticMetadata(
metadataBase: MetadataBaseURL,
source: Metadata | null,
target: any,
staticFilesMetadata: StaticMetadata,
metadataContext: MetadataContext,
titleTemplates: TitleTemplates,
leafSegmentStaticIcons: StaticIcons,
pathname: Promise<string>
) {
if (!staticFilesMetadata) return target
const { icon, apple, openGraph, twitter, manifest } = staticFilesMetadata
if (icon) {
leafSegmentStaticIcons.icon = icon
}
if (apple) {
leafSegmentStaticIcons.apple = apple
}
if (twitter && !source?.twitter?.hasOwnProperty('images')) {
const resolvedTwitter = resolveTwitter(
{ ...target.twitter, images: twitter } as Twitter,
metadataBase,
{ ...metadataContext, isStaticMetadataRouteFile: true },
titleTemplates.twitter
)
target.twitter = convertUrlsToStrings(resolvedTwitter)
}
if (openGraph && !source?.openGraph?.hasOwnProperty('images')) {
const resolvedOpenGraph = await resolveOpenGraph(
{ ...target.openGraph, images: openGraph } as OpenGraph,
metadataBase,
pathname,
{ ...metadataContext, isStaticMetadataRouteFile: true },
titleTemplates.openGraph
)
target.openGraph = convertUrlsToStrings(resolvedOpenGraph)
}
if (manifest) {
target.manifest = manifest
}
return target
}Note
mergeStaticMetadata updates the leaf segment static icons directly on the reference object while conditionally resolving missing image arrays for Twitter and OpenGraph contexts using static route file flags.
Sources: packages/next/src/lib/metadata/resolve-metadata.ts:172-202
Functions like getResolvedMetadataImpl and getNotFoundMetadataImpl channel requests into async render passes that generate markup nodes.
async function getResolvedMetadataImpl(
tree: LoaderTree,
pathname: Promise<string>,
searchParams: Promise<ParsedUrlQuery>,
interpolatedParams: Params,
metadataContext: MetadataContext,
isRuntimePrefetchable: boolean,
errorType?: MetadataErrorType | 'redirect'
): Promise<React.ReactNode> {
const errorConvention = errorType === 'redirect' ? undefined : errorType
return renderMetadata(
tree,
pathname,
searchParams,
interpolatedParams,
metadataContext,
isRuntimePrefetchable,
errorConvention
)
}Warning
Viewport elements render a character set declaration followed by computed viewport attributes, color schemes, and media-query-bound theme colors, enforcing rigid ordering for mobile scaling parameters. Sources: packages/next/src/lib/metadata/metadata.tsx:354-418
The metadata resolution subsystem transforms raw user-supplied metadata configurations into fully normalized, absolute URL-resolved output structures. This pipeline handles platform-specific social tags, title hierarchies, icons, and base URL resolution rules across different deployment environments. Sources: packages/next/src/lib/metadata/resolvers/resolve-opengraph.ts:161-207, packages/next/src/lib/metadata/resolvers/resolve-url.ts:35-55
Social metadata properties are processed by resolveOpenGraph and resolveTwitter, which inspect explicit object fields, validate images, and apply type-specific property constraints. OpenGraph property extraction relies on type definitions mapping types like article, book, music.song, and video.movie to their valid metadata fields.
Sources: packages/next/src/lib/metadata/resolvers/resolve-opengraph.ts:145-189
Twitter card resolution inspects the card type and configures subordinate structures such as players for the player card or app descriptors for the app card, defaulting to summary_large_image or summary depending on whether images are present.
Sources: packages/next/src/lib/metadata/resolvers/resolve-opengraph.ts:237-256
Titles are resolved using resolveTitle, which evaluates string inputs against stashed template strings or processes object structures containing default, absolute, and template properties. When a template is active, %s placeholders are replaced with the target title value.
Sources: packages/next/src/lib/metadata/resolvers/resolve-title.ts:4-40
URL resolution handles relative paths, absolute URLs, and environment-based fallbacks. getSocialImageMetadataBaseFallback inspects execution environments to determine appropriate base URLs:
Sources: packages/next/src/lib/metadata/resolvers/resolve-url.ts:35-55
Warning
When no explicit metadataBase is set for relative social images in production, Next.js falls back to localhost or Vercel environment variables and emits a warning if Vercel system environment variables are not exposed.
Sources: packages/next/src/lib/metadata/resolvers/resolve-opengraph.ts:66-97
Icons are resolved through resolveIcons, which processes input arrays, string URLs, or structured icon descriptor objects containing keys defined in IconKeys, ensuring icon and apple attachment fields are properly structured arrays.
Sources: packages/next/src/lib/metadata/resolvers/resolve-icons.ts:14-34
export const resolveIcons: FieldResolver<'icons'> = (icons) => {
if (!icons) {
return null
}
const resolved: any = {
icon: [],
apple: [],
}
if (Array.isArray(icons)) {
resolved.icon = icons.map(resolveIcon).filter(Boolean)
} else if (isStringOrURL(icons)) {
resolved.icon = [resolveIcon(icons)]
} else {
for (const key of IconKeys) {
const values = resolveAsArrayOrUndefined(icons[key])
if (values) resolved[key] = values.map(resolveIcon)
}
}
return resolved
}Special metadata files such as robots.txt, sitemap.xml, manifest.json, manifest.webmanifest, favicons, and social images require specialized detection, extension mapping, and routing classification. Next.js identifies these files using pre-compiled regular expressions and fast-path heuristics, normalizing them into application routes or static metadata routes.
Sources: packages/next/src/lib/metadata/is-metadata-route.ts:6-162, packages/next/src/lib/metadata/get-metadata-route.ts:164-196
The framework maintains explicit extension boundaries for static metadata image categories (icon, apple, favicon, openGraph, twitter) and metadata route extensions.
Sources: packages/next/src/lib/metadata/is-metadata-route.ts:6-31
Path checking optimizes file routing via fastPathCheck, which evaluates exact path matches for favicon.ico, robots.txt, manifest.json, manifest.webmanifest, and sitemap.xml before falling back to full compiled regular expressions.
Sources: packages/next/src/lib/metadata/is-metadata-route.ts:60-95
const FAVICON_REGEX = /^[\\/]favicon\.ico$/
const ROBOTS_TXT_REGEX = /^[\\/]robots\.txt$/
const MANIFEST_JSON_REGEX = /^[\\/]manifest\.json$/
const MANIFEST_WEBMANIFEST_REGEX = /^[\\/]manifest\.webmanifest$/
const SITEMAP_XML_REGEX = /[\\/]sitemap\.xml$/
function fastPathCheck(normalizedPath: string): boolean | null {
if (FAVICON_REGEX.test(normalizedPath)) return true
if (ROBOTS_TXT_REGEX.test(normalizedPath)) return true
if (MANIFEST_JSON_REGEX.test(normalizedPath)) return true
if (MANIFEST_WEBMANIFEST_REGEX.test(normalizedPath)) return true
if (SITEMAP_XML_REGEX.test(normalizedPath)) return true
if (
!normalizedPath.includes('robots') &&
!normalizedPath.includes('manifest') &&
!normalizedPath.includes('sitemap') &&
!normalizedPath.includes('icon') &&
!normalizedPath.includes('apple-icon') &&
!normalizedPath.includes('opengraph-image') &&
!normalizedPath.includes('twitter-image') &&
!normalizedPath.includes('favicon')
) {
return false
}
return null
}Note
If a path fails exact fast-path matching and contains no metadata keywords, fastPathCheck immediately returns false to bypass regex compilation and testing.
Sources: packages/next/src/lib/metadata/is-metadata-route.ts:80-95
Metadata routes are normalized via normalizeMetadataRoute, mapping static file pages and dynamic route pages into their corresponding /route filesystem structures.
Sources: packages/next/src/lib/metadata/get-metadata-route.ts:171-196
export function normalizeMetadataRoute(page: string) {
if (!isMetadataPage(page)) {
return page
}
let route = page
let suffix = ''
if (page === '/robots') {
route += '.txt'
} else if (page === '/manifest') {
route += '.webmanifest'
} else {
suffix = getMetadataRouteSuffix(page)
}
if (!route.endsWith('/route')) {
const { dir, name: baseName, ext } = path.parse(route)
route = path.posix.join(
dir,
`${baseName}${suffix ? `-${suffix}` : ''}${ext}`,
'route'
)
}
return route
}During runtime filesystem routing in development (opts.dev), getItem intercepts metadata route files via isMetadataRouteFile and resolves them through staticMetadataFiles.
Sources: packages/next/src/server/lib/router-utils/filesystem.ts:515-525
if (opts.dev && isMetadataRouteFile(itemPath, [], false)) {
const fsPath = staticMetadataFiles.get(itemPath)
if (fsPath) {
return {
type: 'nextStaticFolder',
fsPath,
itemPath: fsPath,
}
}
}Warning
Sitemaps are explicitly excluded from suffix generation (getMetadataRouteSuffix) because each sitemap aggregates URLs across sub-routes, ensuring userland contains exactly one sitemap per pathname.
Sources: packages/next/src/lib/metadata/get-metadata-route.ts:24-39
Metadata route segments undergo normalization to handle group routes, parallel route folders, and dynamic parameters when generating physical route filenames and matching regular expressions. When a metadata file resides within a nested directory structure containing route groups (group) or parallel route slots @slot, a unique hash suffix is appended to prevent filename collisions. Sitemaps are excluded from this hashing behavior because they aggregate sub-route URLs and require a single canonical pathname. Route segment normalization also translates dynamic parameters into placeholder patterns for static prerendering and compiles named route regular expressions for runtime matching.
Sources: packages/next/src/lib/metadata/get-metadata-route.ts:31-52, packages/next/src/shared/lib/router/utils/route-regex.ts:378-403
Execution flows through explicit functional chains when handling metadata segment interpolation, route suffix resolution, and named regex compilation.
fillMetadataSegment → fillStaticMetadataSegment → getStaticMetadataRoute → normalizeStaticMetadataRouteSegment): fillMetadataSegment evaluates whether the segment is static or dynamic, delegating to fillStaticMetadataSegment packages/next/src/lib/metadata/get-metadata-route.ts:141-160, which resolves the base path using getStaticMetadataRoute packages/next/src/lib/metadata/get-metadata-route.ts:90-101 and cleans each segment through normalizeStaticMetadataRouteSegment packages/next/src/lib/metadata/get-metadata-route.ts:62-73.export function fillStaticMetadataSegment(
segment: string,
lastSegment: string
) {
return normalizePathSep(
path.join(
getStaticMetadataRoute(segment),
getMetadataRouteFilename(segment, lastSegment)
)
)
}fillMetadataSegment → fillStaticMetadataSegment → getMetadataRouteFilename → getMetadataRouteSuffix → isParallelRouteSegment): fillMetadataSegment calls fillStaticMetadataSegment packages/next/src/lib/metadata/get-metadata-route.ts:141-160, invoking getMetadataRouteFilename packages/next/src/lib/metadata/get-metadata-route.ts:53-61, which calculates the route suffix via getMetadataRouteSuffix packages/next/src/lib/metadata/get-metadata-route.ts:31-52 and checks segment properties using isParallelRouteSegment packages/next/src/shared/lib/segment.ts:11-14.function getMetadataRouteSuffix(page: string) {
const parentPathname = path.dirname(page)
if (page.endsWith('/sitemap') || page.endsWith('/sitemap.xml')) {
return ''
}
let suffix = ''
const segments = parentPathname.split('/')
if (
segments.some((seg) => isGroupSegment(seg) || isParallelRouteSegment(seg))
) {
suffix = djb2Hash(parentPathname).toString(36).slice(0, 6)
}
return suffix
}fillMetadataSegment generates named regular expressions by calling getNamedRouteRegex, which delegates to getNamedParametrizedRoute and constructs minimal route keys via buildGetSafeRouteKey.
Sources: packages/next/src/lib/metadata/get-metadata-route.ts:141-160, packages/next/src/shared/lib/router/utils/route-regex.ts:177-190, packages/next/src/shared/lib/router/utils/route-regex.ts:378-403function buildGetSafeRouteKey() {
let i = 0
return () => {
let routeKey = ''
let j = ++i
while (j > 0) {
routeKey += String.fromCharCode(97 + ((j - 1) % 26))
j = Math.floor((j - 1) / 26)
}
return routeKey
}
}Sources: packages/next/src/lib/metadata/get-metadata-route.ts:31-160, packages/next/src/shared/lib/router/utils/route-regex.ts:177-403, packages/next/src/shared/lib/segment.ts:11-14
The normalization and regex compilation utilities rely on specific helper functions and helper patterns to parse routes.
Sources: packages/next/src/lib/metadata/get-metadata-route.ts:31-73, packages/next/src/shared/lib/router/utils/route-regex.ts:177-190, packages/next/src/shared/lib/segment.ts:7-14
Note
When getMetadataRouteSuffix evaluates a path, it checks if any segment in the parent path pathname satisfies isGroupSegment or isParallelRouteSegment. If true, it computes djb2Hash(parentPathname).toString(36).slice(0, 6).
Sources: packages/next/src/lib/metadata/get-metadata-route.ts:43-50
Sources: packages/next/src/lib/metadata/get-metadata-route.ts:24-52, packages/next/src/shared/lib/router/utils/route-regex.ts:177-190
Warning
Invalid parameter keys (such as keys exceeding 30 characters or starting with a number) automatically fallback to getSafeRouteKey() during getSafeKeyFromSegment execution to preserve regular expression validity.
Sources: packages/next/src/shared/lib/router/utils/route-regex.ts:220-229
The following example demonstrates how fillMetadataSegment processes a dynamic route path versus a static metadata prerender path using the underlying signature functions.
import { fillMetadataSegment, getStaticMetadataPrerenderPathname } from './get-metadata-route'
import { getNamedRouteRegex } from '../../shared/lib/router/utils/route-regex'
// 1. Dynamic metadata segment filling with provided parameters
const dynamicFilled = fillMetadataSegment(
'/a/[slug]',
{ slug: 'b' },
'open-graph',
false
)
// Result: '/a/b/open-graph'
// 2. Static metadata prerender path conversion (replaces dynamic segments with '-')
const staticPrerender = getStaticMetadataPrerenderPathname('/a/[slug]/opengraph-image.tsx')
// Result: '/a/-/opengraph-image-[hash]' or similar normalized path
// 3. Compiling a named route regex with route keys
const routeRegexResult = getNamedRouteRegex('/a/[slug]', {
prefixRouteKeys: false,
includeSuffix: false,
includePrefix: false,
})
// Yields namedRegex '^/a/(?<slug>[^/]+?)(?:/)?$' and routeKeys { slug: 'slug' }Sources: packages/next/src/lib/metadata/get-metadata-route.ts:107-160, packages/next/src/shared/lib/router/utils/route-regex.ts:378-403
The conversion of resolved metadata into React server head elements and its integration with static export serialization relies on structured component creation and file writing utilities. The createMetadataComponents function generates three primary React components: Viewport, Metadata, and MetadataOutlet. These components orchestrate the rendering sequence for head tags, utilizing internal resolution pipelines like resolveMetadata and resolveViewport.
Sources: packages/next/src/lib/metadata/metadata.tsx:41-63, packages/next/src/lib/metadata/metadata.tsx:234-293
The conversion process transitions resolved interface objects into native React element nodes through createViewportElements and createMetadataElements. For viewports, tags such as meta[charset="utf-8"], viewport string interpolations, theme-color, and color-scheme are systematically appended to a tag array. For metadata, properties like title and description generate corresponding HTML elements. During static export operations handled by app-page.ts, the resulting page metadata, status, headers, and segment paths are compiled into a RouteMetadata structure and written out via fileWriter.append() with NEXT_META_SUFFIX.
Sources: packages/next/src/lib/metadata/metadata.tsx:354-448, packages/next/src/export/routes/app-page.ts:216-228
Warning
If a client-side rendering bailout or dynamic usage error occurs during static export generation, rendering fails unless trapped by specific error conventions like isDynamicUsageError or isBailoutToCSRError.
Sources: packages/next/src/export/routes/app-page.ts:250-260
The metadata rendering pipeline executes in a structured sequence from asynchronous resolution to React node serialization:
getResolvedMetadataImpl() or getNotFoundMetadataImpl() receives the loader tree, pathname, search params, and context.renderMetadata().renderMetadata() calls resolveMetadata().createMetadataElements(), returning a fragment of React elements.
Sources: packages/next/src/lib/metadata/metadata.tsx:235-331Sources: packages/next/src/lib/metadata/metadata.tsx:41-63, packages/next/src/lib/metadata/metadata.tsx:354-356, packages/next/src/lib/metadata/metadata.tsx:426-428, packages/next/src/lib/metadata/metadata-context.tsx:4-11