---
title: "Metadata Generation"
description: "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-..."
last_updated: "2026-10-12T06:38:56.845597+00:00"
canonical_url: "https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/built-in-features/metadata-generation"
---

<details>
<summary>Relevant source files</summary>

The following files were used as context for generating this wiki page:

- [packages/next/src/lib/metadata/metadata-elements.tsx](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/lib/metadata/metadata-elements.tsx)
- [packages/next/src/build/webpack/loaders/metadata/resolve-route-data.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/webpack/loaders/metadata/resolve-route-data.ts)
- [packages/next/src/lib/metadata/resolve-metadata.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/lib/metadata/resolve-metadata.ts)
- [packages/next/src/lib/metadata/metadata-resolution-primitives.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/lib/metadata/metadata-resolution-primitives.ts)
- [packages/next/src/lib/metadata/metadata.tsx](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/lib/metadata/metadata.tsx)
- [packages/next/src/pages/_document.tsx](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/pages/_document.tsx)
- [packages/next/src/build/templates/app-page-runtime.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/templates/app-page-runtime.ts)
- [packages/next/src/export/routes/app-page.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/export/routes/app-page.ts)
- [packages/next/src/build/adapter/build-complete.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/adapter/build-complete.ts)
- [packages/next/src/lib/metadata/resolve-metadata-parallel.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/lib/metadata/resolve-metadata-parallel.ts)
- [packages/next/src/server/base-server.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/base-server.ts)
- [packages/next/src/server/render.tsx](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/render.tsx)
- [packages/next/src/build/webpack/loaders/next-metadata-route-loader.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/webpack/loaders/next-metadata-route-loader.ts)
- [packages/next/src/lib/metadata/metadata-parallel.tsx](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/lib/metadata/metadata-parallel.tsx)
- [packages/next/src/lib/metadata/types/metadata-interface.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/lib/metadata/types/metadata-interface.ts)
- [packages/next/src/lib/metadata/get-metadata-route.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/lib/metadata/get-metadata-route.ts)
- [packages/next/src/build/webpack/loaders/metadata/discover.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/webpack/loaders/metadata/discover.ts)
- [packages/next/src/build/webpack/loaders/next-metadata-image-loader.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/webpack/loaders/next-metadata-image-loader.ts)
- [packages/next/src/lib/metadata/resolvers/resolve-basics.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/lib/metadata/resolvers/resolve-basics.ts)
- [packages/next/src/lib/metadata/index.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/lib/metadata/index.ts)
- [packages/next/src/server/og/cache-image-response.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/og/cache-image-response.ts)
- [packages/next/src/build/swc/index.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/swc/index.ts)
</details>

## Overview

Metadata Generation handles the discovery, accumulation, resolution, and HTML rendering of static and dynamic metadata across route segment hierarchies in the [App Rendering](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/app-rendering/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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/lib/metadata/metadata-elements.tsx#L11-L85), [packages/next/src/build/webpack/loaders/metadata/resolve-route-data.ts:5-160](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/webpack/loaders/metadata/resolve-route-data.ts#L5-L160), [packages/next/src/lib/metadata/resolve-metadata.ts:480-609](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/lib/metadata/resolve-metadata.ts#L480-L609), [packages/next/src/lib/metadata/metadata.tsx:33-172](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/lib/metadata/metadata.tsx#L33-L172), [packages/next/src/server/og/cache-image-response.ts:48-284](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/og/cache-image-response.ts#L48-L284), [packages/next/src/build/webpack/loaders/metadata/discover.ts:45-140](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/webpack/loaders/metadata/discover.ts#L45-L140)

## 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.

```typescript
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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/lib/metadata/types/metadata-interface.ts#L570-L676)

> [!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.

```typescript
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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/lib/metadata/types/metadata-interface.ts#L699-L743)

| 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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/lib/metadata/resolvers/resolve-basics.ts#L220-L242)

## Build Route Discovery and Loading

### Static Convention File Discovery and Enumeration

Static convention files across route directories are discovered by `createStaticMetadataFromRoute` in [App Rendering](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/app-rendering/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](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/app-rendering/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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/webpack/loaders/metadata/discover.ts#L16-L140)

> [!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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/webpack/loaders/metadata/discover.ts#L32-L36), [packages/next/src/build/webpack/loaders/metadata/discover.ts:94-137](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/webpack/loaders/metadata/discover.ts#L94-L137)

### Webpack Loaders and Code Generation

Discovered files are wrapped through specialized [webpack loaders](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/build-system/webpack-loaders) that compile user code into [route handlers](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/server-runtime/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.

```typescript
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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/webpack/loaders/next-metadata-route-loader.ts#L60-L125)

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.

```typescript
  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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/webpack/loaders/next-metadata-image-loader.ts#L130-L183)

> [!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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/webpack/loaders/next-metadata-image-loader.ts#L65-L128)

## 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.

```typescript
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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/lib/metadata/resolve-metadata.ts#L480-L559)

> [!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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/lib/metadata/metadata-resolution-primitives.ts#L427-L435)

### 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.

```typescript
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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/lib/metadata/resolve-metadata.ts#L413-L468)

## 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.

```typescript
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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/lib/metadata/resolve-metadata-parallel.ts#L223-L266)

> [!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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/lib/metadata/resolve-metadata-parallel.ts#L207-L221)

### 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.

```tsx
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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/lib/metadata/metadata-parallel.tsx#L49-L90), [packages/next/src/lib/metadata/metadata-parallel.tsx:180-220](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/lib/metadata/metadata-parallel.tsx#L180-L220)

> [!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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/lib/metadata/metadata-parallel.tsx#L84-L87), [packages/next/src/lib/metadata/metadata-parallel.tsx:110-114](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/lib/metadata/metadata-parallel.tsx#L110-L114)

## 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.

```tsx
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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/lib/metadata/metadata-elements.tsx#L11-L38)

> [!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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/lib/metadata/metadata-elements.tsx#L36-L38), [packages/next/src/lib/metadata/metadata-elements.tsx:70-74](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/lib/metadata/metadata-elements.tsx#L70-L74)

### 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.

```tsx
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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/lib/metadata/metadata-elements.tsx#L83-L149)

| 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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/lib/metadata/metadata-elements.tsx#L90-L163)

### Streaming and Component Factory

Depending on whether `process.env.__NEXT_PARALLEL_ROUTE_METADATA` is active, `createMetadataComponents` is loaded from either [App Rendering](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/app-rendering/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.

```tsx
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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/lib/metadata/metadata.tsx#L33-L53), [packages/next/src/lib/metadata/index.ts:1-15](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/lib/metadata/index.ts#L1-L15)

> [!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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/lib/metadata/metadata.tsx#L117-L140), [packages/next/src/lib/metadata/metadata.tsx:155-164](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/lib/metadata/metadata.tsx#L155-L164)

## 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.

```typescript
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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/webpack/loaders/metadata/resolve-route-data.ts#L5-L52)

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`.

```typescript
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 Date
```
Sources: [packages/next/src/build/webpack/loaders/metadata/resolve-route-data.ts:56-160](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/webpack/loaders/metadata/resolve-route-data.ts#L56-L160)

| 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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/webpack/loaders/metadata/resolve-route-data.ts#L5-L160), [packages/next/src/build/webpack/loaders/next-metadata-route-loader.ts:64-67](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/webpack/loaders/next-metadata-route-loader.ts#L64-L67), [packages/next/src/lib/metadata/get-metadata-route.ts:177-180](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/lib/metadata/get-metadata-route.ts#L177-L180)

> [!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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/lib/metadata/get-metadata-route.ts#L24-L29), [packages/next/src/lib/metadata/get-metadata-route.ts:37-39](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/lib/metadata/get-metadata-route.ts#L37-L39)

## 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.

```typescript
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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/og/cache-image-response.ts#L22-L28)

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.

```typescript
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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/og/cache-image-response.ts#L212-L230)

> [!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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/og/cache-image-response.ts#L219-L225), [packages/next/src/server/og/cache-image-response.ts:259-264](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/og/cache-image-response.ts#L259-L264)

| 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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/og/cache-image-response.ts#L303-L371), [packages/next/src/server/og/cache-image-response.ts:378-381](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/og/cache-image-response.ts#L378-L381)

> [!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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/og/cache-image-response.ts#L399-L432)

## Related

- [App Rendering](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/app-rendering/app-rendering)


## Sitemap

See the full [sitemap](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/llms.txt) for all pages in this wiki.
