Relevant source files
The following files were used as context for generating this wiki page:
- packages/next/src/lib/metadata/metadata-elements.tsx
- packages/next/src/build/webpack/loaders/metadata/resolve-route-data.ts
- packages/next/src/lib/metadata/resolve-metadata.ts
- packages/next/src/lib/metadata/metadata-resolution-primitives.ts
- packages/next/src/lib/metadata/metadata.tsx
- packages/next/src/pages/_document.tsx
- packages/next/src/build/templates/app-page-runtime.ts
- packages/next/src/export/routes/app-page.ts
- packages/next/src/build/adapter/build-complete.ts
- packages/next/src/lib/metadata/resolve-metadata-parallel.ts
- packages/next/src/server/base-server.ts
- packages/next/src/server/render.tsx
- packages/next/src/build/webpack/loaders/next-metadata-route-loader.ts
- packages/next/src/lib/metadata/metadata-parallel.tsx
- packages/next/src/lib/metadata/types/metadata-interface.ts
- packages/next/src/lib/metadata/get-metadata-route.ts
- packages/next/src/build/webpack/loaders/metadata/discover.ts
- packages/next/src/build/webpack/loaders/next-metadata-image-loader.ts
- packages/next/src/lib/metadata/resolvers/resolve-basics.ts
- packages/next/src/lib/metadata/index.ts
- packages/next/src/server/og/cache-image-response.ts
- packages/next/src/build/swc/index.ts
Overview
Metadata Generation handles the discovery, accumulation, resolution, and HTML rendering of static and dynamic metadata across route segment hierarchies in the App Rendering engine. It bridges user-defined static convention files, metadata and viewport objects, and asynchronous generateMetadata functions into structured Head Element Rendering tags, specialized web app manifests, robots configurations, XML sitemaps, and cached dynamic OpenGraph image responses.
Sources: packages/next/src/lib/metadata/metadata-elements.tsx:11-85, packages/next/src/build/webpack/loaders/metadata/resolve-route-data.ts:5-160, packages/next/src/lib/metadata/resolve-metadata.ts:480-609, packages/next/src/lib/metadata/metadata.tsx:33-172, packages/next/src/server/og/cache-image-response.ts:48-284, packages/next/src/build/webpack/loaders/metadata/discover.ts:45-140
Metadata Type Interfaces
Resolved Metadata and URL Transformation
The fully processed metadata state is defined by ResolvedMetadataWithURLs and mapped via WithStringifiedURLs<T> to recursively transform URL instances into strings since structural cloning helpers cannot handle live URL objects directly.
interface ResolvedMetadataWithURLs {
// origin and base path for absolute urls for various metadata links such as
// opengraph-image
metadataBase: string | null | URL
// The Document title and template if defined
title: null | AbsoluteTemplateString
// The Document description, and optionally the opengraph and twitter descriptions
description: null | string
// Standard metadata names
// https://developer.mozilla.org/docs/Web/HTML/Element/meta/name
applicationName: null | string
authors: null | Array<Author>
generator: null | string
// if you provide an array it will be flattened into a single tag with comma separation
keywords: null | Array<string>
referrer: null | ReferrerEnum
/**
* @deprecated
*/
themeColor: null | ThemeColorDescriptor[]
/**
* @deprecated
*/
colorScheme: null | ColorSchemeEnum
/**
* @deprecated
*/
viewport: null | string
creator: null | string
publisher: null | string
// https://developer.mozilla.org/docs/Web/HTML/Element/meta/name#other_metadata_names
robots: null | ResolvedRobots
// The canonical and alternate URLs for this location
alternates: null | ResolvedAlternateURLs
// Defaults to rel="icon" but the Icons type can be used
// to get more specific about rel types
icons: null | ResolvedIcons
openGraph: null | ResolvedOpenGraph
manifest: null | string | URL
twitter: null | ResolvedTwitterMetadata
facebook: null | ResolvedFacebook
pinterest: null | ResolvedPinterest
// common verification tokens
verification: null | ResolvedVerification
// Apple web app metadata
// https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariHTMLRef/Articles/MetaTags.html
appleWebApp: null | ResolvedAppleWebApp
// Should devices try to interpret various formats and make actionable links
// out of them? The canonical example is telephone numbers on mobile that can
// be clicked to dial
formatDetection: null | FormatDetection
// meta name="apple-itunes-app"
itunes: null | ItunesApp
// meta name="abstract"
// A brief description of what this web-page is about.
// Not recommended, superceded by description.
// https://www.metatags.org/all-meta-tags-overview/meta-name-abstract/
abstract: null | string
// Facebook AppLinks
appLinks: null | ResolvedAppLinks
// link rel properties
archives: null | Array<string>Sources: packages/next/src/lib/metadata/types/metadata-interface.ts:570-676
Note
ResolvedMetadataWithURLs stores absolute link bases such as metadataBase as string | null | URL, whereas ResolvedMetadata ensures all nested URL parameters are stringified for safe serialization across server and client boundaries.
Route Segment Verification and Robots Files
Route segment configuration interfaces also include MetadataRoute namespaces mapping file conventions like robots directives and sitemaps, alongside verification keys used for search console integrations.
type RobotsFile = {
// Apply rules for all
rules:
| (RobotsRuleBase & {
userAgent?: string | string[] | undefined
})
// Apply rules for specific user agents
| Array<
RobotsRuleBase & {
userAgent: string | string[]
}
>
sitemap?: string | string[] | undefined
host?: string | undefined
}
type SitemapFile = Array<{
url: string
lastModified?: string | Date | undefined
changeFrequency?:
| 'always'
| 'hourly'
| 'daily'
| 'weekly'
| 'monthly'
| 'yearly'
| 'never'
| undefined
priority?: number | undefined
alternates?:
| {
languages?: Languages<string> | undefined
}
| undefined
images?: string[] | undefined
videos?: Videos[] | undefined
}>
type ResolvingMetadata = Promise<ResolvedMetadata>
declare namespace MetadataRoute {
// eslint-disable-next-line @typescript-eslint/no-shadow
export type Robots = RobotsFile
export type Sitemap = SitemapFile
export type Manifest = ManifestFile
}Sources: packages/next/src/lib/metadata/types/metadata-interface.ts:699-743
| Verification Key | Source Field Type | Target Resolved Structure |
|---|---|---|
google | string | number | Array<string | number> | Array<string | number> |
yahoo | string | number | Array<string | number> | Array<string | number> |
yandex | string | number | Array<string | number> | Array<string | number> |
me | string | number | Array<string | number> | Array<string | number> |
other | Record<string, string | number | Array<string | number>> | Record<string, Array<string | number>> |
Sources: packages/next/src/lib/metadata/resolvers/resolve-basics.ts:220-242
Build Route Discovery and Loading
Static Convention File Discovery and Enumeration
Static convention files across route directories are discovered by createStaticMetadataFromRoute in App Rendering, which scans for filenames matching configured extensions and numerical suffixes via enumMetadataFiles. The function iterates sequentially through conventions including icons, apple touch icons, OpenGraph images, Twitter images, favicons, and web manifests to leverage directory access caching App Rendering.
| Convention Name | Filename Convention | Allowed Extensions | Numerical Suffixes |
|---|---|---|---|
icon | icon | image extensions, page extensions | Enabled (icon0 to icon9) |
apple | apple-icon | image extensions, page extensions | Enabled (apple-icon0 to apple-icon9) |
openGraph | opengraph-image | image extensions, page extensions | Enabled (opengraph-image0 to opengraph-image9) |
twitter | twitter-image | image extensions, page extensions | Enabled (twitter-image0 to twitter-image9) |
favicon | favicon | ico | Disabled |
manifest | manifest | webmanifest, json, page extensions | Disabled |
Sources: packages/next/src/build/webpack/loaders/metadata/discover.ts:16-140
Warning
While general icon and image conventions permit up to 10 indexed numerical suffixes (such as icon0 through icon9), favicon and manifest restrict discovery strictly to their exact base filenames without numeric iterations.
Sources: packages/next/src/build/webpack/loaders/metadata/discover.ts:32-36, packages/next/src/build/webpack/loaders/metadata/discover.ts:94-137
Webpack Loaders and Code Generation
Discovered files are wrapped through specialized webpack loaders that compile user code into route handlers. next-metadata-route-loader builds static asset route handlers with appropriate Content-Type mapping and cache controls, enforcing file size limits of 5MB for Twitter images and 8MB for OpenGraph images.
function getContentType(resourcePath: string) {
let { name, ext } = getFilenameAndExtension(resourcePath)
if (ext === 'jpg') ext = 'jpeg'
if (name === 'favicon' && ext === 'ico') return 'image/x-icon'
if (name === 'sitemap') return 'application/xml'
if (name === 'robots') return 'text/plain'
if (name === 'manifest') return 'application/manifest+json'
if (ext === 'png' || ext === 'jpeg' || ext === 'ico' || ext === 'svg') {
return imageExtMimeTypeMap[ext]
}
return 'text/plain'
}
async function getStaticAssetRouteCode(
resourcePath: string,
fileBaseName: string
) {
const cache =
process.env.NODE_ENV !== 'production'
? CACHE_HEADERS.NO_CACHE
: CACHE_HEADERS.REVALIDATE
const isTwitter = fileBaseName === 'twitter-image'
const isOpenGraph = fileBaseName === 'opengraph-image'
// Twitter image file size limit is 5MB.
// General Open Graph image file size limit is 8MB.
// x-ref: https://developer.x.com/en/docs/x-for-websites/cards/overview/summary
// x-ref(facebook): https://developers.facebook.com/docs/sharing/webmasters/images
const fileSizeLimit = isTwitter ? 5 : 8
const imgName = isTwitter ? 'Twitter' : 'Open Graph'
const code = `\
/* static asset route */
import { NextResponse } from 'next/server'
const contentType = ${JSON.stringify(getContentType(resourcePath))}
const buffer = Buffer.from(${JSON.stringify(
(await fs.promises.readFile(resourcePath)).toString('base64')
)}, 'base64'
)
if (${isTwitter || isOpenGraph}) {
const fileSizeInMB = buffer.byteLength / 1024 / 1024
if (fileSizeInMB > ${fileSizeLimit}) {
throw new Error('File size for ${imgName} image ${JSON.stringify(resourcePath)} exceeds ${fileSizeLimit}MB. ' +
\`(Current: \${fileSizeInMB.toFixed(2)}MB)\n\` +
'Read more: https://nextjs.org/docs/app/api-reference/file-conventions/metadata/opengraph-image#image-files-jpg-png-gif'
)
}
}
export function GET() {
return new NextResponse(buffer, {
headers: {
'Content-Type': contentType,
'Cache-Control': ${JSON.stringify(cache)},
},
})
}
export const dynamic = 'force-static'
`
return code
}Sources: packages/next/src/build/webpack/loaders/next-metadata-route-loader.ts:60-125
Concurrently, next-metadata-image-loader processes image assets to extract image dimensions via getImageSize, compute content hashes, and generate metadata descriptors containing dimensions, fallback sizes (any), alternative text from .alt.txt sidecar files, and URL query strings.
let imageError
const imageSize: { width?: number; height?: number } = await getImageSize(
content
).catch((error) => {
const message = `Process image "${path.posix.join(segment || '/', interpolatedName)}" failed: ${error}`
imageError = new Error(message)
return {}
})
if (imageError) {
throw imageError
}
const imageData: Omit<MetadataImageModule, 'url'> = {
...(extension in imageExtMimeTypeMap && {
type: imageExtMimeTypeMap[extension as keyof typeof imageExtMimeTypeMap],
}),
...(useNumericSizes && imageSize.width != null && imageSize.height != null
? imageSize
: {
sizes:
// For SVGs, skip sizes and use "any" to let it scale automatically based on viewport,
// For the images doesn't provide the size properly, use "any" as well.
// If the size is presented, use the actual size for the image.
extension !== 'svg' &&
imageSize.width != null &&
imageSize.height != null
? `${imageSize.width}x${imageSize.height}`
: 'any',
}),
}
if (type === 'openGraph' || type === 'twitter') {
const altPath = path.join(
path.dirname(resourcePath),
fileNameBase + '.alt.txt'
)
if (existsSync(altPath)) {
imageData.alt = await fs.readFile(altPath, 'utf8')
}
}
const imageUrl = fillStaticMetadataSegment(pathnamePrefix, pageSegment)
return `\
export default function () {
const imageData = ${JSON.stringify(imageData)}
return [{
...imageData,
url: ${JSON.stringify(imageUrl + hashQuery)},
}]
}`
}Sources: packages/next/src/build/webpack/loaders/next-metadata-image-loader.ts:130-183
Note
When processing dynamic image modules that export generateImageMetadata, next-metadata-image-loader constructs route handlers that resolve segment parameters asynchronously and map dynamic item IDs to their corresponding route outputs.
Sources: packages/next/src/build/webpack/loaders/next-metadata-image-loader.ts:65-128
Sequential Metadata Resolution
Hierarchical Accumulation
Metadata and viewport configurations are resolved hierarchically from the root layout down to the leaf page through accumulateMetadata and accumulateViewport. The resolution engine iterates through nested metadata items, supplying parent-resolved metadata to dynamic generateMetadata or generateViewport functions while merging static definitions into a single accumulated ResolvedMetadata or ResolvedViewport object.
export async function accumulateMetadata(
route: string,
metadataItems: MetadataItems,
pathname: Promise<string>,
metadataContext: MetadataContext
): Promise<ResolvedMetadata> {
let resolvedMetadata = createDefaultMetadata()
let titleTemplates: TitleTemplates = {
title: null,
twitter: null,
openGraph: null,
}
const buildState = {
warnings: new Set<string>(),
}
let favicon
// Collect the static icons in the most leaf node,
// since we don't collect all the static metadata icons in the parent segments.
const leafSegmentStaticIcons = {
icon: [],
apple: [],
}
const resolversAndResults = prerenderMetadata(metadataItems)
let resultIndex = 0
for (let i = 0; i < metadataItems.length; i++) {
const staticFilesMetadata = metadataItems[i][1]
// Treat favicon as special case, it should be the first icon in the list
// i <= 1 represents root layout, and if current page is also at root
if (i <= 1 && isFavicon(staticFilesMetadata?.icon?.[0])) {
const iconMod = staticFilesMetadata?.icon?.shift()
if (i === 0) favicon = iconMod
}
let pendingMetadata = resolversAndResults[resultIndex++]
if (typeof pendingMetadata === 'function') {
// This metadata item had a `generateMetadata` and
// we need to provide the currently resolved metadata
// to it before we continue;
const resolveParentMetadata = pendingMetadata
// we know that the next item is a result if this item
// was a resolver
pendingMetadata = resolversAndResults[resultIndex++] as Result<Metadata>
resolveParentMetadata(freezeInDev(resolvedMetadata))
}
// Otherwise the item was either null or a static export
let metadata: Metadata | null
if (isPromiseLike(pendingMetadata)) {
metadata = await pendingMetadata
} else {
metadata = pendingMetadata
}
resolvedMetadata = await mergeMetadata(route, pathname, {
resolvedMetadata,
metadata,
metadataContext,
staticFilesMetadata,
titleTemplates,
buildState,
leafSegmentStaticIcons,
})
// If the layout is the same layer with page, skip the leaf layout and leaf page
// The leaf layout and page are the last two items
if (i < metadataItems.length - 2) {
titleTemplates = {
title: resolvedMetadata.title?.template || null,
openGraph: resolvedMetadata.openGraph?.title.template || null,
twitter: resolvedMetadata.twitter?.title.template || null,
}
}
}Sources: packages/next/src/lib/metadata/resolve-metadata.ts:480-559
Caution
If a metadata export contains themeColor, colorScheme, or viewport fields, the build state logs a warning instructing developers to move these configuration properties to a separate viewport export instead.
Sources: packages/next/src/lib/metadata/metadata-resolution-primitives.ts:427-435
Lazy Evaluation and Caching
When metadata or viewport resolvers are wrapped with the 'use cache' directive and accept parent data as their second argument, execution is deferred via createLazyResult to ensure parent data resolves correctly without hanging input abort signal handling.
function getResult<TData extends object, TResolved>(
resolversAndResults: Array<((value: TResolved) => void) | Result<TData>>,
exportForResult: null | TData | InstrumentedResolver<TData, TResolved>
) {
if (typeof exportForResult === 'function') {
// If the function is a 'use cache' function that uses the parent data as
// the second argument, we don't want to eagerly execute it during
// metadata/viewport pre-rendering, as the parent data might also be
// computed from another 'use cache' function. To ensure that the hanging
// input abort signal handling works in this case (i.e. the depending
// function waits for the cached input to resolve while encoding its args),
// they must be called sequentially. This can be accomplished by wrapping
// the call in a lazy promise, so that the original function is only called
// when the result is actually awaited.
const useCacheFunctionInfo = getUseCacheFunctionInfo(
exportForResult.$$original
)
if (useCacheFunctionInfo && useCacheFunctionInfo.usedArgs[1]) {
const promise = new Promise<TResolved>((resolve) =>
resolversAndResults.push(resolve)
)
resolversAndResults.push(
createLazyResult(async () => exportForResult(promise))
)
} else {
let result: TData | Promise<TData>
if (useCacheFunctionInfo) {
resolversAndResults.push(noop)
// @ts-expect-error We intentionally omit the parent argument, because
// we know from the check above that the 'use cache' function does not
// use it.
result = exportForResult()
} else {
result = exportForResult(
new Promise<TResolved>((resolve) => resolversAndResults.push(resolve))
)
}
resolversAndResults.push(result)
if (result instanceof Promise) {
// since we eager execute generateMetadata and
// they can reject at anytime we need to ensure
// we attach the catch handler right away to
// prevent unhandled rejections crashing the process
result.catch((err) => {
return {
__nextError: err,
}
})
}
}
} else if (typeof exportForResult === 'object') {
resolversAndResults.push(exportForResult)
} else {
resolversAndResults.push(null)
}
}Sources: packages/next/src/lib/metadata/resolve-metadata.ts:413-468
Parallel Metadata Tree Resolution
Concurrent Branch Traversal and Lazy Evaluation
Metadata tree resolution evaluates route segments concurrently across parallel route trees using getMetadataResolution and resolveMetadataResolutionImpl. When getResult processes resolvers, it inspects whether an export is a function and checks any associated cache information via getUseCacheFunctionInfo. If a function is designated with 'use cache' but does not utilize parent data as its second argument, it is invoked immediately via callResolver. Otherwise, execution is wrapped in a lazy promise or supplied with a promise-based parent resolver to coordinate asynchronous resolution across nested route hierarchies.
function getResult<TData extends object, TResolved>(
exportForResult: null | TData | InstrumentedResolver<TData, TResolved>
): PrerenderedResult<TData, TResolved> {
if (typeof exportForResult === 'function') {
// If the function is a 'use cache' function that uses the parent data as
// the second argument, we don't want to eagerly execute it during
// metadata/viewport pre-rendering, as the parent data might also be
// computed from another 'use cache' function. To ensure that the hanging
// input abort signal handling works in this case (i.e. the depending
// function waits for the cached input to resolve while encoding its args),
// they must be called sequentially. This can be accomplished by wrapping
// the call in a lazy promise, so that the original function is only called
// when the result is actually awaited.
const useCacheFunctionInfo = getUseCacheFunctionInfo(
exportForResult.$$original
)
if (useCacheFunctionInfo && !useCacheFunctionInfo.usedArgs[1]) {
return {
resolveParent: null,
result: callResolver(() => {
// @ts-expect-error We intentionally omit the parent argument, because
// we know this 'use cache' function does not use it.
return exportForResult()
}),
}
}
let resolveParent: (value: TResolved) => void
const parent = new Promise<TResolved>((resolve) => {
resolveParent = resolve
})
return {
resolveParent: resolveParent!,
result: useCacheFunctionInfo
? createLazyResult(() => exportForResult(parent))
: callResolver(() => exportForResult(parent)),
}
}
return {
resolveParent: null,
result: typeof exportForResult === 'object' ? exportForResult : null,
}
}Sources: packages/next/src/lib/metadata/resolve-metadata-parallel.ts:223-266
Note
Generators are eagerly executed during traversal, so callResolver explicitly attaches a rejection handler (.catch(() => null)) to promises before any intermediate layer can suspend or fail.
Sources: packages/next/src/lib/metadata/resolve-metadata-parallel.ts:207-221
Error Propagation and Outlet Management
Component-level metadata and viewport extraction coordinate through createMetadataComponents, which wraps resolutions inside boundaries such as ViewportBoundary, MetadataBoundary, and OutletBoundary. If selected metadata resolution returns status conditions such as not-found, forbidden, or unauthorized, fallback branch resolution via getMetadataForBranch or getViewportForBranch executes automatically unless an explicit errorType is already present.
async function Viewport() {
const tags = await getMetadataResolution(
tree,
pathnameForMetadata,
searchParams,
interpolatedParams,
metadataContext,
errorType
)
.then(async (resolution) => {
const selected = await resolution.selectedViewport
if (selected.status === 'resolved') {
return <>{createViewportElements(selected.value)}</>
}
if (
!errorType &&
(selected.status === 'not-found' ||
selected.status === 'forbidden' ||
selected.status === 'unauthorized')
) {
const convention = await getViewportForBranch(
tree,
pathnameForMetadata,
searchParams,
selected.status,
interpolatedParams,
metadataContext,
resolution.selectedKeyPath
)
if (convention.status === 'resolved') {
return <>{createViewportElements(convention.value)}</>
}
}
return null
})
.catch(() => {
// We're going to throw the error from the metadata outlet so we just render null here instead
return null
})
return tags
}Sources: packages/next/src/lib/metadata/metadata-parallel.tsx:49-90, packages/next/src/lib/metadata/metadata-parallel.tsx:180-220
Caution
If a metadata resolution throws an error during parallel tree traversal, errors are captured by .catch(() => null) wrappers in Viewport and getSelectedMetadata so that failure propagation is deferred to the metadata outlet component.
Sources: packages/next/src/lib/metadata/metadata-parallel.tsx:84-87, packages/next/src/lib/metadata/metadata-parallel.tsx:110-114
Head Element Rendering
Viewport Tag Transformation
The createViewportElements function converts resolved viewport descriptors into an array of React elements representing <meta> tags. It initializes an element list with a UTF-8 character set tag, then iteratively constructs the content string for scaling, dimensions, and scalability attributes.
export function createViewportElements(
viewport: ResolvedViewport
): React.ReactElement[] {
const tags: React.ReactElement[] = []
let i = 0
tags.push(<meta key={i++} charSet="utf-8" />)
// Build viewport content string from layout properties
const viewportParts: string[] = []
if (viewport.width != null) {
viewportParts.push(`width=${viewport.width}`)
}
if (viewport.height != null) {
viewportParts.push(`height=${viewport.height}`)
}
if (viewport.initialScale != null) {
viewportParts.push(`initial-scale=${viewport.initialScale}`)
}
if (viewport.minimumScale != null) {
viewportParts.push(`minimum-scale=${viewport.minimumScale}`)
}
if (viewport.maximumScale != null) {
viewportParts.push(`maximum-scale=${viewport.maximumScale}`)
}
if (viewport.userScalable != null) {
viewportParts.push(`user-scalable=${viewport.userScalable ? 'yes' : 'no'}`)
}Sources: packages/next/src/lib/metadata/metadata-elements.tsx:11-38
Note
Properties like userScalable are explicitly mapped from boolean values to yes or no strings within the viewport content attribute, while color schemes populate a separate named meta tag.
Sources: packages/next/src/lib/metadata/metadata-elements.tsx:36-38, packages/next/src/lib/metadata/metadata-elements.tsx:70-74
Metadata Element Generation
The createMetadataElements function maps selected metadata fields into corresponding HTML elements such as <title>, <meta>, and <link>. It processes basic properties, author arrays, paginated navigation links, alternate language and media descriptors, and custom name-value pairs from the other configuration property.
export function createMetadataElements(
metadata: SelectedMetadata
): React.ReactElement[] {
const tags: React.ReactElement[] = []
let i = 0
// --- Title ---
if (metadata.title) {
tags.push(<title key={i++}>{metadata.title}</title>)
}
// --- Basic meta tags ---
if (metadata.description) {
tags.push(
<meta key={i++} name="description" content={metadata.description} />
)
}
if (metadata.applicationName) {
tags.push(
<meta
key={i++}
name="application-name"
content={metadata.applicationName}
/>
)
}
// --- Authors ---
if (metadata.authors) {
for (const author of metadata.authors) {
if (author.url) {
tags.push(<link key={i++} rel="author" href={author.url.toString()} />)
}
if (author.name) {
tags.push(<meta key={i++} name="author" content={author.name} />)
}
}
}
// --- Manifest ---
if (metadata.manifest) {
const manifestOrigin = getOrigin(metadata.manifest)
tags.push(
<link
key={i++}
rel="manifest"
href={metadata.manifest.toString()}
crossOrigin={
!manifestOrigin && process.env.VERCEL_ENV === 'preview'
? 'use-credentials'
: undefined
}
/>
)
}
if (metadata.generator) {
tags.push(<meta key={i++} name="generator" content={metadata.generator} />)
}
if (metadata.keywords && metadata.keywords.length) {
tags.push(
<meta key={i++} name="keywords" content={metadata.keywords.join(',')} />
)
}
if (metadata.referrer) {
tags.push(<meta key={i++} name="referrer" content={metadata.referrer} />)
}Sources: packages/next/src/lib/metadata/metadata-elements.tsx:83-149
| Metadata Field | HTML Element | Attribute Mapping / Logic |
|---|---|---|
title | <title> | Direct text child content |
description | <meta> | name="description" content={value} |
applicationName | <meta> | name="application-name" content={value} |
authors | <link> / <meta> | rel="author" for URLs, name="author" for names |
manifest | <link> | rel="manifest", conditional crossOrigin="use-credentials" |
generator | <meta> | name="generator" content={value} |
keywords | <meta> | name="keywords" content={array.join(',')} |
referrer | <meta> | name="referrer" content={value} |
robots.basic | <meta> | name="robots" content={value} |
robots.googleBot | <meta> | name="googlebot" content={value} |
Sources: packages/next/src/lib/metadata/metadata-elements.tsx:90-163
Streaming and Component Factory
Depending on whether process.env.__NEXT_PARALLEL_ROUTE_METADATA is active, createMetadataComponents is loaded from either App Rendering or parallel routing modules to construct the Viewport, Metadata, and MetadataOutlet React components. The components integrate with MetadataBoundary, ViewportBoundary, and OutletBoundary to manage suspense states and streaming boundaries.
export function createMetadataComponents({
tree,
pathname,
parsedQuery,
metadataContext,
interpolatedParams,
errorType,
serveStreamingMetadata,
}: {
tree: LoaderTree
pathname: string
parsedQuery: SearchParams
metadataContext: MetadataContext
interpolatedParams: Params
errorType?: MetadataErrorType | 'redirect'
serveStreamingMetadata: boolean
}): {
Viewport: React.ComponentType
Metadata: React.ComponentType
MetadataOutlet: React.ComponentType<{ tree: LoaderTree }>
} {Sources: packages/next/src/lib/metadata/metadata.tsx:33-53, packages/next/src/lib/metadata/index.ts:1-15
Warning
When serveStreamingMetadata is disabled, MetadataBlocker resolves the metadata promise before releasing the render tree, whereas streaming mode wraps the resolution inside a suspended element to allow incremental flushing.
Sources: packages/next/src/lib/metadata/metadata.tsx:117-140, packages/next/src/lib/metadata/metadata.tsx:155-164
Route Data Generation
Structured metadata route returns from files such as robots.ts, sitemap.ts, and manifest.ts are transformed into raw string responses via resolveRouteData. The resolveRobots function processes crawler rules by iterating over user agents, allow and disallow directives, crawl delays, and custom fields, appending host and sitemap attributes when defined.
export function resolveRobots(data: MetadataRoute.Robots): string {
let content = ''
const rules = Array.isArray(data.rules) ? data.rules : [data.rules]
for (const rule of rules) {
const userAgent = resolveArray(rule.userAgent || ['*'])
for (const agent of userAgent) {
content += `User-Agent: ${agent}\n`
}
if (rule.allow) {
const allow = resolveArray(rule.allow)
for (const item of allow) {
content += `Allow: ${item}\n`
}
}
if (rule.disallow) {
const disallow = resolveArray(rule.disallow)
for (const item of disallow) {
content += `Disallow: ${item}\n`
}
}
if (rule.crawlDelay) {
content += `Crawl-delay: ${rule.crawlDelay}\n`
}
if (rule.other) {
for (const key of Object.keys(rule.other)) {
const value = rule.other[key]
if (value == null) continue
const values = Array.isArray(value) ? value : [value]
for (const v of values) {
content += `${key}: ${v}\n`
}
}
}
content += '\n'
}
if (data.host) {
content += `Host: ${data.host}\n`
}
if (data.sitemap) {
const sitemap = resolveArray(data.sitemap)
// TODO-METADATA: support injecting sitemap url into robots.txt
sitemap.forEach((item) => {
content += `Sitemap: ${item}\n`
})
}
return content
}Sources: packages/next/src/build/webpack/loaders/metadata/resolve-route-data.ts:5-52
Sitemap generation via resolveSitemap inspects items for alternates, images, and videos, dynamically injecting XML namespaces for xmlns:image, xmlns:video, and xmlns:xhtml when present. It serializes dates using toISOString and emits <loc>, <lastmod>, <changefreq>, and <priority> elements for every URL entry. Web app manifests are serialized using JSON.stringify.
export function resolveSitemap(data: MetadataRoute.Sitemap): string {
const hasAlternates = data.some(
(item) => Object.keys(item.alternates ?? {}).length > 0
)
const hasImages = data.some((item) => Boolean(item.images?.length))
const hasVideos = data.some((item) => Boolean(item.videos?.length))
let content = ''
content += '<?xml version="1.0" encoding="UTF-8"?>\n'
content += '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"'
if (hasImages) {
content += ' xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"'
}
if (hasVideos) {
content += ' xmlns:video="http://www.google.com/schemas/sitemap-video/1.1"'
}
if (hasAlternates) {
content += ' xmlns:xhtml="http://www.w3.org/1999/xhtml">\n'
} else {
content += '>\n'
}
for (const item of data) {
content += '<url>\n'
content += `<loc>${item.url}</loc>\n`
const languages = item.alternates?.languages
if (languages && Object.keys(languages).length) {
// Since sitemap is separated from the page rendering, there's not metadataBase accessible yet.
// we give the default setting that won't effect the languages resolving.
for (const language in languages) {
content += `<xhtml:link rel="alternate" hreflang="${language}" href="${
languages[language as keyof typeof languages]
}" />\n`
}
}
if (item.images?.length) {
for (const image of item.images) {
content += `<image:image>\n<image:loc>${image}</image:loc>\n</image:image>\n`
}
}
if (item.videos?.length) {
for (const video of item.videos) {
let videoFields = [
`<video:video>`,
`<video:title>${video.title}</video:title>`,
`<video:thumbnail_loc>${video.thumbnail_loc}</video:thumbnail_loc>`,
`<video:description>${video.description}</video:description>`,
video.content_loc &&
`<video:content_loc>${video.content_loc}</video:content_loc>`,
video.player_loc &&
`<video:player_loc>${video.player_loc}</video:player_loc>`,
video.duration &&
`<video:duration>${video.duration}</video:duration>`,
video.view_count &&
`<video:view_count>${video.view_count}</video:view_count>`,
video.tag && `<video:tag>${video.tag}</video:tag>`,
video.rating && `<video:rating>${video.rating}</video:rating>`,
video.expiration_date &&
`<video:expiration_date>${video.expiration_date}</video:expiration_date>`,
video.publication_date &&
`<video:publication_date>${video.publication_date}</video:publication_date>`,
video.family_friendly &&
`<video:family_friendly>${video.family_friendly}</video:family_friendly>`,
video.requires_subscription &&
`<video:requires_subscription>${video.requires_subscription}</video:requires_subscription>`,
video.live && `<video:live>${video.live}</video:live>`,
video.restriction &&
`<video:restriction relationship="${video.restriction.relationship}">${video.restriction.content}</video:restriction>`,
video.platform &&
`<video:platform relationship="${video.platform.relationship}">${video.platform.content}</video:platform>`,
video.uploader &&
`<video:uploader${video.uploader.info && ` info="${video.uploader.info}"`}>${video.uploader.content}</video:uploader>`,
`</video:video>\n`,
].filter(Boolean)
content += videoFields.join('\n')
}
}
if (item.lastModified) {
const serializedDate =
item.lastModified instanceof DateSources: packages/next/src/build/webpack/loaders/metadata/resolve-route-data.ts:56-160
| File Type | Route Extension | Content-Type Header | Serialization Handler |
|---|---|---|---|
robots | .txt | text/plain | resolveRobots |
sitemap | .xml | application/xml | resolveSitemap |
manifest | .webmanifest | application/manifest+json | resolveManifest |
Sources: packages/next/src/build/webpack/loaders/metadata/resolve-route-data.ts:5-160, packages/next/src/build/webpack/loaders/next-metadata-route-loader.ts:64-67, packages/next/src/lib/metadata/get-metadata-route.ts:177-180
Note
Unlike other metadata file conventions, sitemaps never receive parent-path hash suffixes during filename normalization because each sitemap aggregates all sub-route URLs and must maintain predictable paths.
Sources: packages/next/src/lib/metadata/get-metadata-route.ts:24-29, packages/next/src/lib/metadata/get-metadata-route.ts:37-39
Dynamic Image Response Caching
OpenGraph and metadata image routes rendered via ImageResponse rely on getCachedImageResponseBody to cache deterministic rasterizations inside the Resume Data Cache during prerenders. The Node build of @vercel/og is loaded dynamically via importOgModule to keep heavy WASM and satori modules off the standard module-load path until an image render is triggered.
function importOgModule(): Promise<OgModule> {
// Cache Components is Node-only (rejected for the edge runtime at compile
// time), so we always load the Node build. Loading it dynamically keeps the
// heavy `@vercel/og` renderer (satori + WASM) off the module-load path, so
// it's pulled in only when an image is actually rendered.
return import('next/dist/compiled/@vercel/og/index.node.js')
}Sources: packages/next/src/server/og/cache-image-response.ts:22-28
During a prerender work unit, the element tree is serialized with prerenderToNodeStream under hangingInputAbortSignal to separate static elements from dynamic inputs like un-cached fetches or cookies. When serialization completes without triggering dynamic inputs, the raw prelude chunks are concatenated into a Buffer, and a stable SHA-256 cache key is derived by hashing the serialized element buffer alongside a structured encoding of the options object.
const chunks: Buffer[] = []
for await (const chunk of prelude) {
chunks.push(chunk)
}
const elementBuffer = Buffer.concat(chunks)
// Derive a stable cache key from the serialized element plus the options.
// We hash rather than reuse the raw serialized bytes so the key stays
// compact even for large inputs (e.g. embedded fonts), and we fold the
// options in by content so two images that differ only in their options
// (size, fonts, ...) don't collide. The options are hashed directly here,
// never serialized through Flight, which would both bloat the key and apply
// `Buffer.prototype .toJSON` to font data.
const hash = createHash('sha256')
hash.update(elementBuffer)
updateHashWithOptions(hash, options)
const cacheKey = hash.digest('base64')
Sources: packages/next/src/server/og/cache-image-response.ts:212-230
Warning
updateHashWithOptions skips Flight serialization for image options to prevent Node Buffer instances from converting into JSON objects with toJSON methods, which would otherwise break satori's font parser.
Sources: packages/next/src/server/og/cache-image-response.ts:219-225, packages/next/src/server/og/cache-image-response.ts:259-264
| Value Type | Hash Tag | Encoding Behavior |
|---|---|---|
undefined | 'u' | Emits type tag directly |
null | 'n' | Emits type tag directly |
| Primitive | 'p' | Encoded as ${type}:${String(value)} via updateHashWithBytes |
ArrayBuffer | 'a' | Encoded as raw bytes via updateHashWithBytes |
| ArrayBufferView | 'v' | Encoded from underlying buffer slice via updateHashWithBytes |
Array | [...] | Count-prefixed, recursively walks items via updateHashWithOptions |
| Plain Object | {...} | Count-prefixed, sorts keys and recursively walks entries via updateHashWithOptions |
Sources: packages/next/src/server/og/cache-image-response.ts:303-371, packages/next/src/server/og/cache-image-response.ts:378-381
Note
If a deserialized element tree contains React.lazy markers emitted by Flight for async components, resolveFlightLazies recursively unwraps them so satori can walk the resolved element structure.
Sources: packages/next/src/server/og/cache-image-response.ts:399-432
Related
On This Page
OverviewMetadata Type InterfacesResolved Metadata and URL TransformationRoute Segment Verification and Robots FilesBuild Route Discovery and LoadingStatic Convention File Discovery and EnumerationWebpack Loaders and Code GenerationSequential Metadata ResolutionHierarchical AccumulationLazy Evaluation and CachingParallel Metadata Tree ResolutionConcurrent Branch Traversal and Lazy EvaluationError Propagation and Outlet ManagementHead Element RenderingViewport Tag TransformationMetadata Element GenerationStreaming and Component FactoryRoute Data GenerationDynamic Image Response CachingRelated