Skip to content

Commit cfe39e2

Browse files
committed
fix(build): enable prerendering of interception routes with generateStaticParams
Fixes a bug where interception routes in parallel slots could not be prerendered using generateStaticParams. The previous implementation only examined "children" segments when building static paths, missing segments from parallel routes that contribute to the pathname. Introduces extractPathnameSegments() which: - Traverses the entire loader tree (children + parallel routes) using BFS - Correctly tracks URL depth (skipping route groups/parallel markers, including interception markers which are actual URL components) - Validates that static segment prefixes match the target pathname - Returns ALL segments that contribute to pathname construction This enables routes like app/@modal/(.)photo/[id] to be prerendered when they export generateStaticParams, fixing 404 responses that occurred when these routes were accessed directly.
1 parent 2ca55f9 commit cfe39e2

File tree

5 files changed

+1194
-32
lines changed

5 files changed

+1194
-32
lines changed

packages/next/src/build/static-paths/app.ts

Lines changed: 91 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import escapePathDelimiters from '../../shared/lib/router/utils/escape-path-deli
2121
import { createIncrementalCache } from '../../export/helpers/create-incremental-cache'
2222
import type { NextConfigComplete } from '../../server/config-shared'
2323
import type { DynamicParamTypes } from '../../shared/lib/app-router-types'
24-
import { InvariantError } from '../../shared/lib/invariant-error'
2524
import { getParamProperties } from '../../shared/lib/router/utils/get-segment-param'
2625
import type { AppRouteModule } from '../../server/route-modules/app-route/module.compiled'
2726
import { filterUniqueParams } from './app/filter-unique-params'
@@ -30,6 +29,7 @@ import { calculateFallbackMode } from './app/calculate-fallback-mode'
3029
import { assignErrorIfEmpty } from './app/assign-error-if-empty'
3130
import { resolveParallelRouteParams } from './app/resolve-parallel-route-params'
3231
import { generateRouteStaticParams } from './app/generate-route-static-params'
32+
import { extractPathnameSegments } from './app/extract-pathname-segments'
3333

3434
/**
3535
* Validates the parameters to ensure they're accessible and have the correct
@@ -127,11 +127,61 @@ function validateParams(
127127
return valid
128128
}
129129

130+
function createReplacements(
131+
segment: Pick<AppSegment, 'paramType'>,
132+
paramValue: string | string[]
133+
) {
134+
// Determine the prefix to use for the interception marker.
135+
let prefix: string
136+
switch (segment.paramType) {
137+
case 'catchall-intercepted-(.)':
138+
case 'dynamic-intercepted-(.)':
139+
prefix = '(.)'
140+
break
141+
case 'catchall-intercepted-(..)(..)':
142+
case 'dynamic-intercepted-(..)(..)':
143+
prefix = '(..)(..)'
144+
break
145+
case 'catchall-intercepted-(..)':
146+
case 'dynamic-intercepted-(..)':
147+
prefix = '(..)'
148+
break
149+
case 'catchall-intercepted-(...)':
150+
case 'dynamic-intercepted-(...)':
151+
prefix = '(...)'
152+
break
153+
default:
154+
prefix = ''
155+
break
156+
}
157+
158+
return {
159+
pathname:
160+
prefix +
161+
encodeParam(paramValue, (value) =>
162+
// Only escape path delimiters if the value is a string, the following
163+
// version will URL encode the value.
164+
escapePathDelimiters(value, true)
165+
),
166+
encodedPathname:
167+
prefix +
168+
encodeParam(
169+
paramValue,
170+
// URL encode the value.
171+
encodeURIComponent
172+
),
173+
}
174+
}
175+
130176
/**
131-
* Builds the static paths for an app using `generateStaticParams`.
177+
* Processes app directory segments to build route parameters from generateStaticParams functions.
178+
* This function walks through the segments array and calls generateStaticParams for each segment that has it,
179+
* combining parent parameters with child parameters to build the complete parameter combinations.
180+
* Uses iterative processing instead of recursion for better performance.
132181
*
133-
* @param params - The parameters for the build.
134-
* @returns The static paths.
182+
* @param segments - Array of app directory segments to process
183+
* @param store - Work store for tracking fetch cache configuration
184+
* @returns Promise that resolves to an array of all parameter combinations
135185
*/
136186
export async function buildAppStaticPaths({
137187
dir,
@@ -246,6 +296,18 @@ export async function buildAppStaticPaths({
246296
}
247297
}
248298

299+
// Extract segments that contribute to the pathname by traversing the loader tree.
300+
// This handles cases where parallel route segments (e.g., interception routes) also
301+
// contribute to pathname construction, not just "children" segments.
302+
const pathnameSegments =
303+
'loaderTree' in ComponentMod.routeModule.userland &&
304+
Array.isArray(ComponentMod.routeModule.userland.loaderTree)
305+
? extractPathnameSegments(
306+
ComponentMod.routeModule.userland.loaderTree,
307+
page
308+
)
309+
: childrenRouteParamSegments // Fallback for route modules without loader tree
310+
249311
const afterRunner = new AfterRunner()
250312

251313
const store = createWorkStore({
@@ -351,15 +413,15 @@ export async function buildAppStaticPaths({
351413
// routes that won't throw on empty static shell for each of them if
352414
// they're available.
353415
paramsToProcess = generateAllParamCombinations(
354-
childrenRouteParamSegments,
416+
pathnameSegments,
355417
routeParams,
356418
rootParamKeys
357419
)
358420

359421
// The fallback route params for this route is a combination of the
360-
// parallel route params and the non-parallel route params.
422+
// parallel route params and the pathname-contributing params.
361423
const fallbackRouteParams: readonly FallbackRouteParam[] = [
362-
...childrenRouteParamSegments.map(({ paramName, paramType: type }) =>
424+
...pathnameSegments.map(({ paramName, paramType: type }) =>
363425
createFallbackRouteParam(paramName, type, false)
364426
),
365427
...parallelFallbackRouteParams,
@@ -383,11 +445,11 @@ export async function buildAppStaticPaths({
383445
}
384446

385447
filterUniqueParams(
386-
childrenRouteParamSegments,
448+
pathnameSegments,
387449
validateParams(
388450
page,
389451
isRoutePPREnabled,
390-
childrenRouteParamSegments,
452+
pathnameSegments,
391453
rootParamKeys,
392454
paramsToProcess
393455
)
@@ -397,28 +459,27 @@ export async function buildAppStaticPaths({
397459

398460
const fallbackRouteParams: FallbackRouteParam[] = []
399461

400-
for (const {
401-
paramName: key,
402-
paramType: type,
403-
} of childrenRouteParamSegments) {
404-
const paramValue = params[key]
462+
for (const { name, paramName, paramType } of pathnameSegments) {
463+
const paramValue = params[paramName]
405464

406465
if (!paramValue) {
407466
if (isRoutePPREnabled) {
408467
// Mark remaining params as fallback params.
409-
fallbackRouteParams.push(createFallbackRouteParam(key, type, false))
468+
fallbackRouteParams.push(
469+
createFallbackRouteParam(paramName, paramType, false)
470+
)
410471
for (
411472
let i =
412-
childrenRouteParamSegments.findIndex(
413-
(param) => param.paramName === key
473+
pathnameSegments.findIndex(
474+
(param) => param.paramName === paramName
414475
) + 1;
415-
i < childrenRouteParamSegments.length;
476+
i < pathnameSegments.length;
416477
i++
417478
) {
418479
fallbackRouteParams.push(
419480
createFallbackRouteParam(
420-
childrenRouteParamSegments[i].paramName,
421-
childrenRouteParamSegments[i].paramType,
481+
pathnameSegments[i].paramName,
482+
pathnameSegments[i].paramType,
422483
false
423484
)
424485
)
@@ -431,22 +492,20 @@ export async function buildAppStaticPaths({
431492
}
432493
}
433494

434-
const segment = childrenRouteParamSegments.find(
435-
({ paramName }) => paramName === key
436-
)
437-
if (!segment) {
438-
throw new InvariantError(
439-
`Param ${key} not found in childrenRouteParamSegments ${childrenRouteParamSegments.map(({ paramName }) => paramName).join(', ')}`
440-
)
441-
}
495+
const replacements = createReplacements({ paramType }, paramValue)
442496

443497
pathname = pathname.replace(
444-
segment.name,
445-
encodeParam(paramValue, (value) => escapePathDelimiters(value, true))
498+
name,
499+
// We're replacing the segment name with the replacement pathname
500+
// which will include the interception marker prefix if it exists.
501+
replacements.pathname
446502
)
503+
447504
encodedPathname = encodedPathname.replace(
448-
segment.name,
449-
encodeParam(paramValue, encodeURIComponent)
505+
name,
506+
// We're replacing the segment name with the replacement encoded
507+
// pathname which will include the encoded param value.
508+
replacements.encodedPathname
450509
)
451510
}
452511

0 commit comments

Comments
 (0)