---
title: "Static Generation"
description: "Static Generation in Next.js pre-renders pages at build time across both the App Router and Pages Router, producing static HTML, React Server Component payloads, and cache artifacts that eliminate ..."
last_updated: "2026-10-12T06:38:56.85659+00:00"
canonical_url: "https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/build-system/static-generation"
---

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

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

- [packages/next/src/build/index.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/index.ts)
- [packages/next/src/export/index.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/export/index.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/export/worker.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/export/worker.ts)
- [packages/next/src/build/static-paths/app.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/static-paths/app.ts)
- [packages/next/src/build/utils.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/utils.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/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/server/render.tsx](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/render.tsx)
- [packages/next/src/build/entries.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/entries.ts)
- [packages/next/src/export/routes/pages.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/export/routes/pages.ts)
- [packages/next/src/build/templates/pages.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/templates/pages.ts)
- [packages/next/src/build/static-paths/pages.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/static-paths/pages.ts)
- [packages/next/src/build/static-paths/app/extract-pathname-route-param-segments-from-loader-tree.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/static-paths/app/extract-pathname-route-param-segments-from-loader-tree.ts)
- [packages/next/src/build/static-paths/utils.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/static-paths/utils.ts)
- [packages/next/src/export/helpers/create-incremental-cache.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/export/helpers/create-incremental-cache.ts)
- [packages/next/src/lib/format-dynamic-import-path.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/lib/format-dynamic-import-path.ts)
- [packages/next/src/server/app-render/async-local-storage.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/app-render/async-local-storage.ts)
- [packages/next/src/server/async-storage/work-store.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/async-storage/work-store.ts)
- [packages/next/src/shared/lib/router/utils/parse-loader-tree.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/shared/lib/router/utils/parse-loader-tree.ts)
</details>

## Overview

Static Generation in Next.js pre-renders pages at build time across both the [App Router](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/client-runtime/app-router) and Pages Router, producing static HTML, React Server Component payloads, and cache artifacts that eliminate per-request server computation for eligible routes.

Sources: [packages/next/src/build/index.ts:3210-3212](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/index.ts#L3210-L3212), [packages/next/src/export/worker.ts:320-344](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/export/worker.ts#L320-L344), [packages/next/src/export/routes/pages.ts:30-49](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/export/routes/pages.ts#L30-L49)

## Static Route Assessment and Staticity Classification

### Route Module Inspection and Component Loading

The [build pipeline](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/build-system/build-pipeline) evaluates page staticity within a tracing span named `is-page-static-utils`. For routes running on edge runtimes, the build obtains component exports by invoking `getRuntimeContext` with entry paths and WebAssembly bindings resolved from the distribution directory. Non-edge routes are loaded via `loadComponents`, which retrieves userland components, route modules, and data-fetching hooks such as `getServerSideProps`, `getStaticPaths`, and `getStaticProps`.

```typescript
const isPageStaticSpan = trace('is-page-static-utils', parentId)
return isPageStaticSpan
  .traceAsyncFn(async (): Promise<PageIsStaticResult> => {
    setHttpClientAndAgentOptions({
      httpAgentOptions,
    })

    let componentsResult: LoadComponentsReturnType
    let prerenderedRoutes: PrerenderedRoute[] | undefined
    let prerenderRouteMatchers: PrerenderRouteMatcher[] | undefined
    let paramMatching: ParamMatching | undefined
    let prerenderFallbackMode: FallbackMode | undefined
    let appConfig: AppSegmentConfig = {}
    let rootParamKeys: readonly string[] | undefined
    const pathIsEdgeRuntime = isEdgeRuntime(pageRuntime)

    if (pathIsEdgeRuntime) {
      const runtime = await getRuntimeContext({
        paths: edgeInfo.files.map((file: string) => path.join(distDir, file)),
        edgeFunctionEntry: {
          ...edgeInfo,
          wasm: (edgeInfo.wasm ?? []).map((binding: AssetBinding) => ({
            ...binding,
            filePath: path.join(distDir, binding.filePath),
          })),
        },
        name: edgeInfo.name,
        useCache: true,
        distDir,
        clientAssetToken,
      })
      const mod = (
        await runtime.context._ENTRIES[`middleware_${edgeInfo.name}`]
      ).ComponentMod

      // This is not needed during require.
      const buildManifest = {} as BuildManifest

      componentsResult = {
        Component: mod.default,
        Document: mod.Document,
        App: mod.App,
        routeModule: mod.routeModule,
        page,
        ComponentMod: mod,
        pageConfig: mod.config || {},
        buildManifest,
        reactLoadableManifest: {},
        getServerSideProps: mod.getServerSideProps,
        getStaticPaths: mod.getStaticPaths,
        getStaticProps: mod.getStaticProps,
      }
    } else {
      componentsResult = await loadComponents({
        distDir,
        page: originalAppPath || page,
        isAppPath: pageType === 'app',
        isDev: false,
        sriEnabled,
        needsManifestsForLegacyReasons: true,
      })
    }

    const { Component, routeModule } = componentsResult
```
Sources: [packages/next/src/build/utils.ts:909-972](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/utils.ts#L909-L972)

For App Router targets, the build inspects route modules by collecting layout and page segments through `collectSegments`. This step processes the route module with options like `cacheComponents` and `partialPrefetching`, capturing segment configurations and structural requirements.

```typescript
if (pageType === 'app') {
  // @ts-expect-error pageType is app, so we can assume AppPageModule | AppRouteModule
  const ComponentMod: AppPageModule | AppRouteModule =
    componentsResult.ComponentMod

  let collectedSegments: Awaited<ReturnType<typeof collectSegments>>
  try {
    collectedSegments = await collectSegments(
      // We know this is an app page or app route module because we
      // checked above that the page type is 'app'.
      routeModule as AppPageRouteModule | AppRouteRouteModule,
      { cacheComponents, partialPrefetching }
    )
  } catch (err) {
    throw new Error(`Failed to collect configuration for ${page}`, {
      cause: err,
    })
  }
```
Sources: [packages/next/src/build/utils.ts:978-995](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/utils.ts#L978-L995)

> [!NOTE]
> [Edge runtime](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/server-runtime/edge-runtime) route modules are evaluated inside a sandboxed VM context where entry bindings (`_ENTRIES`) provide direct access to the userland module exports.

Sources: [packages/next/src/build/utils.ts:925-943](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/utils.ts#L925-L943)

### Pages Router Method Export Hoisting

In the Pages Router, compiled templates hoist userland methods and check for data-fetching exports such as `getStaticProps`, `getStaticPaths`, and `getServerSideProps`. These exports are bound directly to the generated `routeModule` instance along with `app.default` and `document.default` from the document templates.

```typescript
// Re-export methods.
export const getStaticProps = hoist(userland, 'getStaticProps')
export const getStaticPaths = hoist(userland, 'getStaticPaths')
export const getServerSideProps = hoist(userland, 'getServerSideProps')
export const config = hoist(userland, 'config')
export const reportWebVitals = hoist(userland, 'reportWebVitals')

// Re-export legacy methods.
export const unstable_getStaticProps = hoist(
  userland,
  'unstable_getStaticProps'
)
export const unstable_getStaticPaths = hoist(
  userland,
  'unstable_getStaticPaths'
)
export const unstable_getStaticParams = hoist(
  userland,
  'unstable_getStaticParams'
)
export const unstable_getServerProps = hoist(
  userland,
  'unstable_getServerProps'
)
export const unstable_getServerSideProps = hoist(
  userland,
  'unstable_getServerSideProps'
)
```
Sources: [packages/next/src/build/templates/pages.ts:17-44](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/templates/pages.ts#L17-L44)

The generated handler receives these hoisted methods to process incoming requests and determine execution paths based on the presence of server-side data requirements.

```typescript
export const handler = getHandler({
  srcPage: 'VAR_DEFINITION_PAGE',
  config,
  userland,
  routeModule,
  getStaticPaths,
  getStaticProps,
  getServerSideProps,
})
```
Sources: [packages/next/src/build/templates/pages.ts:66-74](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/templates/pages.ts#L66-L74)

## App Router Static Parameter Extraction

### Loader Tree Traversal and Segment Extraction

The App Router static parameter extraction pipeline processes route hierarchies by inspecting loader trees and matching them against normalized routes. For `AppPageRouteModule`, `extractPathnameRouteParamSegments` invokes `extractPathnameRouteParamSegmentsFromLoaderTree`, which performs a breadth-first search across the loader tree retrieved from `routeModule.userland.loaderTree`.

```typescript
export function extractPathnameRouteParamSegmentsFromLoaderTree(
  loaderTree: LoaderTree,
  route: NormalizedAppRoute
): {
  pathnameRouteParamSegments: Array<{
    readonly name: string
    readonly paramName: string
    readonly paramType: DynamicParamTypes
  }>
  params: Params
} {
  const segmentsByDepth = new Map<
    number,
    {
      readonly name: string
      readonly paramName: string
      readonly paramType: DynamicParamTypes
    }
  >()
  const params: Params = {}

  // BFS traversal with depth and path tracking
  const queue: Array<{
    tree: LoaderTree
    depth: number
    currentPath: NormalizedAppRouteSegment[]
  }> = [{ tree: loaderTree, depth: 0, currentPath: [] }]
```
Sources: [packages/next/src/build/static-paths/app/extract-pathname-route-param-segments-from-loader-tree.ts:86-113](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/static-paths/app/extract-pathname-route-param-segments-from-loader-tree.ts#L86-L113)

During tree traversal, `parseLoaderTree` extracts the segment, parallel routes, and module conventions from each tree node. Route segments are parsed using `parseAppRouteSegment`. Route groups (`route-group`) and parallel route markers (`parallel-route`) are excluded from contributing directly to URL pathname generation.

```typescript
while (queue.length > 0) {
  const { tree, depth, currentPath } = queue.shift()!
  const { segment, parallelRoutes } = parseLoaderTree(tree)

  // Build the path for the current node
  let updatedPath = currentPath
  let nextDepth = depth

  const appSegment = parseAppRouteSegment(segment)

  // Only add to path if it's a real segment that appears in the URL
  // Route groups and parallel markers don't contribute to URL pathname
  if (
    appSegment &&
    appSegment.type !== 'route-group' &&
    appSegment.type !== 'parallel-route'
  ) {
    updatedPath = [...currentPath, appSegment]
    nextDepth = depth + 1
  }
```
Sources: [packages/next/src/build/static-paths/app/extract-pathname-route-param-segments-from-loader-tree.ts:114-134](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/static-paths/app/extract-pathname-route-param-segments-from-loader-tree.ts#L114-L134)

Dynamic segments must satisfy depth and prefix validations before their parameters are accepted. `validatePrefixMatch` checks that accumulated static segments, dynamic parameters, and interception markers align exactly with the target route segments.

```typescript
function validatePrefixMatch(
  currentPath: NormalizedAppRouteSegment[],
  route: NormalizedAppRoute
): boolean {
  for (let i = 0; i < currentPath.length; i++) {
    const pathSegment = currentPath[i]
    const targetPathSegment = route.segments[i]

    // Type mismatch - one is static, one is dynamic
    if (pathSegment.type !== targetPathSegment.type) {
      return false
    }

    // One has an interception marker, the other doesn't.
    if (
      pathSegment.interceptionMarker !== targetPathSegment.interceptionMarker
    ) {
      return false
    }

    // Both are static but names don't match
    if (
      pathSegment.type === 'static' &&
      targetPathSegment.type === 'static' &&
      pathSegment.name !== targetPathSegment.name
    ) {
      return false
    }
    // Both are dynamic but param names don't match
    else if (
      pathSegment.type === 'dynamic' &&
      targetPathSegment.type === 'dynamic' &&
      pathSegment.param.paramType !== targetPathSegment.param.paramType &&
      pathSegment.param.paramName !== targetPathSegment.param.paramName
    ) {
      return false
    }
  }

  return true
}
```
Sources: [packages/next/src/build/static-paths/app/extract-pathname-route-param-segments-from-loader-tree.ts:36-76](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/static-paths/app/extract-pathname-route-param-segments-from-loader-tree.ts#L36-L76)

```mermaid
flowchart TD
    A["Queue Pop (tree, depth, currentPath)"] --> B["parseLoaderTree & parseAppRouteSegment"]
    B --> C{"Is Real Segment?"}
    C -- "No (route-group / parallel-route)" --> D["Keep Path & Depth"]
    C -- "Yes" --> E["Update currentPath & nextDepth"]
    D --> F{"Segment Type === 'dynamic'?"}
    E --> F
    F -- "Yes" --> G{"depth < route.segments.length?"}
    G -- "Yes" --> H{"targetSegment.type === 'dynamic' && paramName matches?"}
    H -- "Yes" --> I{"validatePrefixMatch(currentPath, route)?"}
    I -- "Yes" --> J["segmentsByDepth.set(depth, segmentInfo)"]
    J --> K["resolveParamValue & Queue Parallel Routes"]
    I -- "No" --> K
    H -- "No" --> K
    G -- "No" --> K
    F -- "No" --> K
    K --> L{"Queue not empty?"}
    L -- "Yes" --> A
    L -- "No" --> M["Sort segmentsByDepth by depth & return"]
```
Sources: [packages/next/src/build/static-paths/app/extract-pathname-route-param-segments-from-loader-tree.ts:107-202](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/static-paths/app/extract-pathname-route-param-segments-from-loader-tree.ts#L107-L202)

### Parameter Resolution and Validation Rules

For `AppRouteRouteModule`, parameter segments are extracted by filtering the route segment array directly via `extractPathnameRouteParamSegmentsFromSegments`.

```typescript
export function extractPathnameRouteParamSegmentsFromSegments(
  segments: readonly Readonly<AppSegment>[]
): Array<{
  readonly name: string
  readonly paramName: string
  readonly paramType: DynamicParamTypes
}> {
  // TODO: should we consider what values are already present in the page?

  // For AppRouteRouteModule, filter the segments array to get the route params
  // that contribute to the pathname.
  const result: Array<{
    readonly name: string
    readonly paramName: string
    readonly paramType: DynamicParamTypes
  }> = []

  for (const segment of segments) {
    // Skip segments without param info.
    if (!segment.paramName || !segment.paramType) continue

    // Collect all the route param keys that contribute to the pathname.
    result.push({
      name: segment.name,
      paramName: segment.paramName,
      paramType: segment.paramType,
    })
  }

  return result
}
```
Sources: [packages/next/src/build/static-paths/utils.ts:80-110](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/static-paths/utils.ts#L80-L110)

The core build function `generateStaticParams` evaluates parameter combinations while checking configuration rules and output modes. If `nextConfigOutput` is set to `'export'`, encountering `dynamicParams: true` triggers an error.

```typescript
if (
  segments.some((generate) => generate.config?.dynamicParams === true) &&
  nextConfigOutput === 'export'
) {
  throw new Error(
    '"dynamicParams: true" cannot be used with "output: export". See more info here: https://nextjs.org/docs/app/building-your-application/deploying/static-exports'
  )
}
```
Sources: [packages/next/src/build/static-paths/app.ts:918-925](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/static-paths/app.ts#L918-L925)

| Validation Check | Condition | Outcome |
| :--- | :--- | :--- |
| Export Output Dynamic Check | `segments.some(s => s.config?.dynamicParams === true) && nextConfigOutput === 'export'` | Throws `"dynamicParams: true" cannot be used with "output: export"` error |
| Param Matching Feature Gate | `hasParamMatchingExport && !cacheComponents` | Throws `cannot use parameter matching without enabling cacheComponents` error |
| Navigation Mode Guarantee | `isEnsureStaticPage && paramMatching` with mode `'fallback'` or `'dynamic'` | Throws error prohibiting `'fallback'` or `'dynamic'` parameter matching with `ensureStatic = "navigation"` |
| Static Export Incomplete Params | `nextConfigOutput === 'export' && routeParams.length > 0 && !hadAllParamsGenerated` | Throws incomplete params error listing missing parameter names |

Sources: [packages/next/src/build/static-paths/app.ts:918-957](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/static-paths/app.ts#L918-L957), [packages/next/src/build/static-paths/app.ts:1009-1017](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/static-paths/app.ts#L1009-L1017), [packages/next/src/build/static-paths/app.ts:1126-1134](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/static-paths/app.ts#L1126-L1134)

> [!WARNING]
> When `output: export` is configured, every generated params object returned by `generateStaticParams()` must include all dynamic route parameters; missing parameters cause the build to fail immediately.

Sources: [packages/next/src/build/static-paths/app.ts:1126-1134](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/static-paths/app.ts#L1126-L1134)

> [!NOTE]
> Parallel slots can repeat parameters, and route groups can cause loader tree traversal to encounter deeper URL parameters before shallower ones. Parameters are sorted by depth ascending (`a - b`) to ensure consistent ordering.

Sources: [packages/next/src/build/static-paths/app/extract-pathname-route-param-segments-from-loader-tree.ts:195-200](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/static-paths/app/extract-pathname-route-param-segments-from-loader-tree.ts#L195-L200)

## Pages Router Static Paths Resolution

The build process for the legacy Pages Router invokes the page component's `getStaticPaths` method, passing a cloned array of configured `locales` and the `defaultLocale`. The return value undergoes strict validation to ensure it is a non-null object containing valid keys and a boolean or `'blocking'` `fallback` property.

```typescript
const staticPathsResult = await getStaticPaths({
  // We create a copy here to avoid having the types of `getStaticPaths`
  // change. This ensures that users can't mutate this array and have it
  // poison the reference.
  locales: [...(locales ?? [])],
  defaultLocale,
})

const expectedReturnVal =
  `Expected: { paths: [], fallback: boolean }\n` +
  `See here for more info: https://nextjs.org/docs/messages/invalid-getstaticpaths-value`

if (
  !staticPathsResult ||
  typeof staticPathsResult !== 'object' ||
  Array.isArray(staticPathsResult)
) {
  throw new Error(
    `Invalid value returned from getStaticPaths in ${page}. Received ${typeof staticPathsResult} ${expectedReturnVal}`
  )
}

const invalidStaticPathKeys = Object.keys(staticPathsResult).filter(
  (key) => !(key === 'paths' || key === 'fallback')
)

if (invalidStaticPathKeys.length > 0) {
  throw new Error(
    `Extra keys returned from getStaticPaths in ${page} (${invalidStaticPathKeys.join(
      ', '
    )}) ${expectedReturnVal}`
  )
}

if (
  !(
    typeof staticPathsResult.fallback === 'boolean' ||
    staticPathsResult.fallback === 'blocking'
  )
) {
  throw new Error(
    `The \`fallback\` key must be returned from getStaticPaths in ${page}.\n` +
      expectedReturnVal
  )
}
```
Sources: [packages/next/src/build/static-paths/pages.ts:31-75](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/static-paths/pages.ts#L31-L75)

| Validation Check | Condition | Error Thrown / Outcome |
| :--- | :--- | :--- |
| Non-Object Result | `!staticPathsResult || typeof staticPathsResult !== 'object' || Array.isArray(staticPathsResult)` | Throws invalid value returned error |
| Extra Return Keys | Any key other than `paths` or `fallback` present | Throws extra keys returned error |
| Missing/Invalid Fallback | `fallback` is not a boolean or `'blocking'` | Throws missing fallback key error |
| Invalid Paths Type | `paths` property is not an array | Throws invalid `paths` value error |

Sources: [packages/next/src/build/static-paths/pages.ts:43-84](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/static-paths/pages.ts#L43-L84)

Path entries within the `paths` array can be supplied either as strings or objects containing a `params` dictionary and an optional `locale`. String entries are cleaned of locale prefixes, matched against the route regex, and decoded using `decodeURIComponent` while escaping path delimiters. Object entries are checked for illegal top-level keys outside `params` and `locale`, and their parameters are validated against expected route parameter definitions.

```typescript
const invalidKeys = Object.keys(entry).filter(
  (key) => key !== 'params' && key !== 'locale'
)

if (invalidKeys.length) {
  throw new Error(
    `Additional keys were returned from \`getStaticPaths\` in page "${page}". ` +
      `URL Parameters intended for this dynamic route must be nested under the \`params\` key, i.e.:` +
      `\n\n\treturn { params: { ${routeParameterKeys
        .map((k) => `${k}: ...`)
        .join(', ')} } }` +
      `\n\nKeys that need to be moved: ${invalidKeys.join(', ')}.\n`
  )
}

const { params = {} } = entry
let builtPage = page
let encodedBuiltPage = page

routeParameterKeys.forEach((validParamKey) => {
  const { repeat, optional } = _routeRegex.groups[validParamKey]
  let paramValue = params[validParamKey]
  if (
    optional &&
    params.hasOwnProperty(validParamKey) &&
    (paramValue === null ||
      paramValue === undefined ||
      (paramValue as any) === false)
  ) {
    paramValue = []
  }

  if (
    (repeat && !Array.isArray(paramValue)) ||
    (!repeat && typeof paramValue !== 'string') ||
    typeof paramValue === 'undefined'
  ) {
    throw new Error(
      `A required parameter (${validParamKey}) was not provided as ${
        repeat ? 'an array' : 'a string'
      } received ${typeof paramValue} in getStaticPaths for ${page}`
    )
  }
```
Sources: [packages/next/src/build/static-paths/pages.ts:133-175](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/static-paths/pages.ts#L133-L175)

> [!WARNING]
> Object-provided paths in `getStaticPaths` must place all dynamic route parameters exclusively under the `params` key; returning additional top-level keys triggers an error listing the specific properties that must be moved.

Sources: [packages/next/src/build/static-paths/pages.ts:133-146](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/static-paths/pages.ts#L133-L146)

Parameter values for optional and repeating segments are normalized into arrays or strings respectively, then encoded using `encodeParam` with path delimiter escaping or standard `encodeURIComponent`. Finally, locale identifiers are validated against the configured locales list, and prerender route records are populated with resolved pathnames, params, and fallback modes derived via `parseStaticPathsResult`.

```typescript
  routeParameterKeys.forEach((validParamKey) => {
    const { repeat, optional } = _routeRegex.groups[validParamKey]
    let paramValue = params[validParamKey]
    if (
      optional &&
      params.hasOwnProperty(validParamKey) &&
      (paramValue === null ||
        paramValue === undefined ||
        (paramValue as any) === false)
    ) {
      paramValue = []
    }

    if (
      (repeat && !Array.isArray(paramValue)) ||
      (!repeat && typeof paramValue !== 'string') ||
      typeof paramValue === 'undefined'
    ) {
      throw new Error(
        `A required parameter (${validParamKey}) was not provided as ${
          repeat ? 'an array' : 'a string'
        } received ${typeof paramValue} in getStaticPaths for ${page}`
      )
    }

    let replaced = `[${repeat ? '...' : ''}${validParamKey}]`
    if (optional) {
      replaced = `[${replaced}]`
    }

    builtPage = builtPage.replace(
      replaced,
      encodeParam(paramValue, (value) => escapePathDelimiters(value, true))
    )

    encodedBuiltPage = encodedBuiltPage.replace(
      replaced,
      encodeParam(paramValue, encodeURIComponent)
    )
  })

  if (!builtPage && !encodedBuiltPage) {
    return
  }

  if (entry.locale && !locales?.includes(entry.locale)) {
    throw new Error(
      `Invalid locale returned from getStaticPaths for ${page}, the locale ${entry.locale} is not specified in ${configFileName}`
    )
  }
  const curLocale = entry.locale || defaultLocale || ''

  const pathname = normalizePathname(
    `${curLocale ? `/${curLocale}` : ''}${curLocale && builtPage === '/' ? '' : builtPage}`
  )

  if (!prerenderedRoutesByPathname.has(pathname)) {
    prerenderedRoutesByPathname.set(pathname, {
      params,
      pathname,
      encodedPathname: normalizePathname(
        `${curLocale ? `/${curLocale}` : ''}${
          curLocale && encodedBuiltPage === '/' ? '' : encodedBuiltPage
        }`
      ),
      fallbackRouteParams: undefined,
      fallbackMode: parseStaticPathsResult(staticPathsResult.fallback),
      fallbackRootParams: undefined,
      throwOnEmptyStaticShell: undefined,
    })
  }
}
```
Sources: [packages/next/src/build/static-paths/pages.ts:152-223](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/static-paths/pages.ts#L152-L223)

## Prerender Execution in Worker Pools

### Worker Initialization and Request Mocking

Prerendering execution for individual routes occurs inside isolated worker processes spawned during build. When `exportPages` or `exportPageImpl` initializes, it installs native bindings and code frame support, and establishes global behaviors on `globalThis.__NEXT_DATA__`. Each prerender execution constructs mocked request and response objects using `createRequestResponseMocks` based on the normalized target path, simulating HTTP environments for static generation.

```typescript
;(globalThis as any).__NEXT_DATA__ = {
  nextExport: true,
}
```
Sources: [packages/next/src/export/worker.ts:63-65](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/export/worker.ts#L63-L65), [packages/next/src/export/worker.ts:178-178](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/export/worker.ts#L178-L178), [packages/next/src/export/worker.ts:382-388](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/export/worker.ts#L382-L388)

> [!NOTE]
> Native bindings are explicitly loaded inside the worker process so that code frame rendering—which relies on the native `codeFrameColumns` function—operates correctly during prerendering failures.

Sources: [packages/next/src/export/worker.ts:385-387](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/export/worker.ts#L385-L387)

### Pages Router Prerendering Execution

For legacy Pages Router routes, `exportPagesPage` executes page components while enforcing strict rendering rules. It checks whether `getServerSideProps` is defined, throwing an error if server-side rendering props are requested during static export. For static pages without dynamic parameters, non-dynamic SSG prerendering bypasses redundant execution if already completed.

```typescript
if (components.getServerSideProps) {
  throw new Error(`Error for page ${page}: ${SERVER_PROPS_EXPORT_ERROR}`)
}

// for non-dynamic SSG pages we should have already
// prerendered the file
if (!buildExport && components.getStaticProps && !isDynamic) {
  return
}
```
Sources: [packages/next/src/export/routes/pages.ts:50-58](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/export/routes/pages.ts#L50-L58)

Parameters extracted from paths are merged back into query parameters to support legacy access patterns via `useRouter().query`. When rendering completes, page metadata and static page data files are appended using `MultiFileWriter` to the configured pages data directory and HTML filepath destinations.

| Condition / Check | Target Action | Error / Behavior Outcome |
| :--- | :--- | :--- |
| `components.getServerSideProps` | Present on page components | Throws `SERVER_PROPS_EXPORT_ERROR` |
| `!buildExport && components.getStaticProps && !isDynamic` | Non-dynamic SSG page outside build export | Returns early without re-rendering |
| `typeof components.Component === 'string'` | Static component string | Uses `RenderResult.fromStatic` |
| `hasOrigQueryValues` with static component | Query values supplied on auto-exported page | Throws auto-export query configuration error |

Sources: [packages/next/src/export/routes/pages.ts:50-87](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/export/routes/pages.ts#L50-L87)

## Incremental Cache Setup During Build

### Custom Cache Handler Initialization and File Path Resolution

The [incremental cache](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/server-runtime/incremental-cache) instance utilized during static generation is created through `createIncrementalCache`. When a custom `cacheHandler` string path is supplied, it is processed via `formatDynamicImportPath` to convert absolute filesystem paths or `file://` import URLs into valid file URLs through `resolveCacheHandlerPathToFilesystem`. The imported default module is then assigned as the custom cache implementation `CurCacheHandler`.

```typescript
export async function createIncrementalCache({
  cacheHandler,
  cacheMaxMemorySize,
  fetchCacheKeyPrefix,
  distDir,
  dir,
  flushToDisk,
  cacheHandlers,
  requestHeaders,
}: {
  cacheHandler?: string
  cacheMaxMemorySize: number
  fetchCacheKeyPrefix?: string
  distDir: string
  dir: string
  flushToDisk?: boolean
  requestHeaders?: Record<string, string | string[] | undefined>
  cacheHandlers?: Record<string, string | undefined>
}) {
  // Custom cache handler overrides.
  let CacheHandler: any
  if (cacheHandler) {
    CacheHandler = interopDefault(
      await import(formatDynamicImportPath(dir, cacheHandler)).then(
        (mod) => mod.default || mod
      )
    )
  }
```
Sources: [packages/next/src/export/helpers/create-incremental-cache.ts:13-40](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/export/helpers/create-incremental-cache.ts#L13-L40), [packages/next/src/lib/format-dynamic-import-path.ts:11-34](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/lib/format-dynamic-import-path.ts#L11-L34)

> [!WARNING]
> Passing an absolute Windows path directly to dynamic imports causes it to interpret the drive letter as a protocol prefix (`C:`). `formatDynamicImportPath` avoids this by ensuring paths are correctly resolved and converted to file URLs using `pathToFileURL`.

Sources: [packages/next/src/lib/format-dynamic-import-path.ts:19-34](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/lib/format-dynamic-import-path.ts#L19-L34)

When multiple `cacheHandlers` are provided, the cache handlers subsystem is initialized with `cacheMaxMemorySize`. Each handler entry iterates through the dictionary, imports the corresponding path using `formatDynamicImportPath`, and registers it via `setCacheHandler`.

Sources: [packages/next/src/export/helpers/create-incremental-cache.ts:42-58](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/export/helpers/create-incremental-cache.ts#L42-L58)

### IncrementalCache and Work Store Construction

The core cache instance is instantiated using the `IncrementalCache` class, configured with node filesystem methods (`nodeFs`), a server distribution directory set under `distDir/server`, and version 4 prerender manifest metadata. The created instance is assigned to `globalThis.__incrementalCache` for global process access.

Sources: [packages/next/src/export/helpers/create-incremental-cache.ts:65-87](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/export/helpers/create-incremental-cache.ts#L65-L87)

During prerender execution, `createPrerenderWorkStore` constructs a `WorkStore` instance that tracks fetch metrics, cache lifecycles, and request contexts. It binds either the incremental cache supplied via `renderOpts.incrementalCache` or falls back to the global `__incrementalCache` instance.

```typescript
export function createPrerenderWorkStore(context: WorkStoreContext): WorkStore {
  return createWorkStoreImpl(
    context,
    !!process.env.__NEXT_DEV_SERVER ||
      !!process.env.NEXT_DEBUG_BUILD ||
      process.env.NEXT_SSG_FETCH_METRICS === '1'
  )
}

function createWorkStoreImpl(
  {
    page,
    renderOpts,
    isPrefetchRequest,
    buildId,
    deploymentId,
    previouslyRevalidatedTags,
    nonce,
  }: WorkStoreContext,
  shouldTrackFetchMetrics: boolean
): WorkStore {
  const store: WorkStore = {
    page,
    route: normalizeAppPath(page),
    incrementalCache:
      // we fallback to a global incremental cache for edge-runtime locally
      // so that it can access the fs cache without mocks
      renderOpts.incrementalCache || (globalThis as any).__incrementalCache,
```
Sources: [packages/next/src/server/async-storage/work-store.ts:95-122](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/async-storage/work-store.ts#L95-L122)

| Function / Helper | Primary Role | Associated Options & Parameters |
| :--- | :--- | :--- |
| `createIncrementalCache` | Initializes local or custom cache handlers and sets up `IncrementalCache` | `cacheHandler`, `cacheMaxMemorySize`, `fetchCacheKeyPrefix`, `distDir`, `dir`, `flushToDisk`, `cacheHandlers`, `requestHeaders` |
| `formatDynamicImportPath` | Resolves file paths and URLs into valid file URLs for dynamic imports | `dir`, `filePath` |
| `resolveCacheHandlerPathToFilesystem` | Unwraps `file://` URLs into filesystem paths | `filePath` |
| `createPrerenderWorkStore` | Initializes work storage context for static prerendering | `context` containing `page`, `renderOpts`, `buildId`, `deploymentId` |

Sources: [packages/next/src/export/helpers/create-incremental-cache.ts:13-31](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/export/helpers/create-incremental-cache.ts#L13-L31), [packages/next/src/lib/format-dynamic-import-path.ts:11-26](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/lib/format-dynamic-import-path.ts#L11-L26), [packages/next/src/server/async-storage/work-store.ts:95-102](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/async-storage/work-store.ts#L95-L102)

## Static Export and Artifact Packaging

### Processing Static HTML and RSC Payload Emits

During a static export or production build packaging phase, Next.js validates that configured outputs adhere to static generation constraints. When `nextConfig.output === 'export'`, the builder inspects the routes manifest to catch incompatible features such as interception route rewrites under `routesManifest.rewrites.beforeFiles` and dynamic server action references in the [server actions](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/app-rendering/server-actions) manifest (`serverActionsManifest.node` and `serverActionsManifest.edge`), throwing an `ExportError` if either is found.

```typescript
if (nextConfig.output === 'export') {
  const routesManifest = require(join(distDir, ROUTES_MANIFEST))

  // We already prevent rewrites earlier in the process, however Next.js will insert rewrites
  // for interception routes so we need to check for that here.
  if (routesManifest?.rewrites?.beforeFiles?.length > 0) {
    const hasInterceptionRouteRewrite =
      routesManifest.rewrites.beforeFiles.some(isInterceptionRouteRewrite)

    if (hasInterceptionRouteRewrite) {
      throw new ExportError(
        `Intercepting routes are not supported with static export.\nRead more: https://nextjs.org/docs/app/building-your-application/deploying/static-exports#unsupported-features`
      )
    }
  }

  const actionIds = [
    ...Object.keys(serverActionsManifest.node),
    ...Object.keys(serverActionsManifest.edge),
  ]

  if (
    actionIds.some(
      (actionId) =>
        extractInfoFromServerReferenceId(actionId).type === 'server-action'
    )
  ) {
    throw new ExportError(
      `Server Actions are not supported with static export.\nRead more: https://nextjs.org/docs/app/building-your-application/deploying/static-exports#unsupported-features`
    )
  }
```
Sources: [packages/next/src/export/index.ts:444-474](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/export/index.ts#L444-L474)

> [!WARNING]
> Static export mode prohibits runtime server actions, intercepting route rewrites, default-loader [image optimization](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/built-in-features/image-optimization), and middleware or API routes. Encountering any of these during packaging immediately halts the build with an `ExportError`.

Sources: [packages/next/src/export/index.ts:428-434](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/export/index.ts#L428-L434), [packages/next/src/export/index.ts:454-457](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/export/index.ts#L454-L457), [packages/next/src/export/index.ts:471-474](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/export/index.ts#L471-L474)

### Adapter Output Mapping and Route Manifest Generation

Build completion adapters marshal pages, static files, and middleware into structured output maps for hosting platforms. Static pages and internationalized locale paths are assigned `AdapterOutputType.STATIC_FILE` targets pointing to `.html` outputs, accompanied by `.rsc` payload fallback files (`rsc-fallback.json`) when application router pages are present.

```typescript
if (config.i18n) {
  for (const locale of config.i18n.locales || []) {
    const localePage =
      page === '/' ? `/${locale}` : addPathPrefix(page, `/${locale}`)

    const localeOutput = {
      id: localePage,
      pathname: localePage,
      type: AdapterOutputType.STATIC_FILE,
      filePath: path.join(
        pagesDistDir,
        `${normalizePagePath(localePage)}.html`
      ),
      immutableHash: undefined,
    } satisfies AdapterOutput['STATIC_FILE']

    outputs.staticFiles.push(localeOutput)
    emittedStaticFilePathnames.add(localeOutput.pathname)

    if (appPageKeys && appPageKeys.length > 0) {
      outputs.staticFiles.push({
        id: `${localePage}.rsc`,
        pathname: `${localePage}.rsc`,
        type: AdapterOutputType.STATIC_FILE,
        filePath: rscFallbackPath,
        immutableHash: undefined,
      })
    }
  }
} else {
  const staticOutput = {
    id: page,
    pathname: route,
    type: AdapterOutputType.STATIC_FILE,
    filePath: pageFile.replace(/\.js$/, '.html'),
    immutableHash: undefined,
  } satisfies AdapterOutput['STATIC_FILE']

  outputs.staticFiles.push(staticOutput)
  emittedStaticFilePathnames.add(staticOutput.pathname)

  if (appPageKeys && appPageKeys.length > 0) {
    outputs.staticFiles.push({
      id: `${page}.rsc`,
      pathname: `${route}.rsc`,
      type: AdapterOutputType.STATIC_FILE,
      filePath: rscFallbackPath,
      immutableHash: undefined,
    })
  }
}
```
Sources: [packages/next/src/build/adapter/build-complete.ts:943-993](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/adapter/build-complete.ts#L943-L993)

| Adapter Output Type | Target Path Identifier | Runtime / Source Handling |
| :--- | :--- | :--- |
| `AdapterOutputType.STATIC_FILE` | `.html` or `.rsc` file path | Static HTML / RSC payload file generated during prerendering |
| `AdapterOutputType.PAGES` | Server entry point (`pagesDistDir`) | Node.js runtime execution for server-rendered Pages Router routes |
| `AdapterOutputType.PAGES_API` | Server API entry point | Node.js runtime execution for Pages API endpoints |
| `AdapterOutputType.MIDDLEWARE` | `distDir/server/middleware.js` | Neutral or Node.js runtime execution for global middleware functions |

Sources: [packages/next/src/build/adapter/build-complete.ts:973-980](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/adapter/build-complete.ts#L973-L980), [packages/next/src/build/adapter/build-complete.ts:1016-1031](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/adapter/build-complete.ts#L1016-L1031), [packages/next/src/build/adapter/build-complete.ts:1109-1117](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/adapter/build-complete.ts#L1109-L1117)

## Related

- [Build Pipeline](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/build-system/build-pipeline)
- [Dynamic Rendering](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/server-runtime/dynamic-rendering)
- [Incremental Cache](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/server-runtime/incremental-cache)


## Sitemap

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