@@ -21,7 +21,6 @@ import escapePathDelimiters from '../../shared/lib/router/utils/escape-path-deli
2121import { createIncrementalCache } from '../../export/helpers/create-incremental-cache'
2222import type { NextConfigComplete } from '../../server/config-shared'
2323import type { DynamicParamTypes } from '../../shared/lib/app-router-types'
24- import { InvariantError } from '../../shared/lib/invariant-error'
2524import { getParamProperties } from '../../shared/lib/router/utils/get-segment-param'
2625import type { AppRouteModule } from '../../server/route-modules/app-route/module.compiled'
2726import { filterUniqueParams } from './app/filter-unique-params'
@@ -30,6 +29,7 @@ import { calculateFallbackMode } from './app/calculate-fallback-mode'
3029import { assignErrorIfEmpty } from './app/assign-error-if-empty'
3130import { resolveParallelRouteParams } from './app/resolve-parallel-route-params'
3231import { 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 */
136186export 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