@@ -34,6 +34,7 @@ import { UNDERSCORE_NOT_FOUND_ROUTE } from '../../shared/lib/entry-constants' wi
3434import {
3535 getFallbackRouteParams ,
3636 getPlaceholderFallbackRouteParams ,
37+ getStagedFallbackParams ,
3738 buildDynamicSegmentPlaceholder ,
3839 createOpaqueFallbackRouteParams ,
3940 type OpaqueFallbackRouteParams ,
@@ -585,6 +586,42 @@ export function createAppPageEntrypoint({
585586 prerenderInfo ?. fallback === null &&
586587 ( prerenderInfo . fallbackRootParams ?. length ?? 0 ) > 0
587588
589+ // SSG writes and navigation RDC reads use the same completed-shell key.
590+ // Completion uses the matched shell rather than the fully resolved
591+ // pathname. A request for `/prefix/c/foo` can complete
592+ // `/prefix/[one]/[two]` to `/prefix/c/[two]`. This avoids creating an entry
593+ // for every value of `two`.
594+ //
595+ // The completed-shell key also applies when unresolved root params require
596+ // a blocking render. A source shell cannot be shared across root branches.
597+ // Completion resolves the params into the key as follows:
598+ // - Root params and other prerenderable params use concrete values.
599+ // - Never-prerenderable params remain placeholders.
600+ // The values of those placeholder params must not partition the cache.
601+ const fallbackPathname = prerenderMatch
602+ ? typeof prerenderInfo ?. fallback === 'string'
603+ ? prerenderInfo . fallback
604+ : prerenderMatch . source
605+ : null
606+ let completedShellCacheKey : string | null = null
607+ if (
608+ nextConfig . partialPrefetching &&
609+ fallbackPathname &&
610+ prerenderInfo ?. fallbackRouteParams ?. length &&
611+ remainingPrerenderableParams . length > 0
612+ ) {
613+ const cacheKey = buildCompletedShellCacheKey (
614+ fallbackPathname ,
615+ remainingPrerenderableParams ,
616+ params
617+ )
618+
619+ // Only a more complete shell gets a separate cache entry.
620+ if ( cacheKey !== fallbackPathname ) {
621+ completedShellCacheKey = cacheKey
622+ }
623+ }
624+
588625 let ssgCacheKey : string | null = null
589626 let usesCompletedShellCacheKey = false
590627 if (
@@ -595,22 +632,6 @@ export function createAppPageEntrypoint({
595632 ! hasPostponedState &&
596633 ! isDynamicRSCRequest
597634 ) {
598- // For normal SSG routes we cache by the fully resolved pathname. For
599- // partial fallbacks we instead derive the cache key from the shell
600- // that matched this request so `/prefix/[one]/[two]` can specialize into
601- // `/prefix/c/[two]` without promoting all the way to `/prefix/c/foo`.
602- // This includes entries with unresolved ROOT params: those requests are
603- // served blocking (no shell can be shared across root branches), but
604- // the entry they produce is still keyed by the completed shell — root
605- // params and any other prerenderable params resolve into the key while
606- // params that `generateStaticParams` can never provide stay as
607- // placeholders and must not partition the cache.
608- const fallbackPathname = prerenderMatch
609- ? typeof prerenderInfo ?. fallback === 'string'
610- ? prerenderInfo . fallback
611- : prerenderMatch . source
612- : null
613-
614635 if (
615636 // Partial fallback shells are only specialized per request when Partial
616637 // Prefetching is enabled, mirroring the `partialFallback` flag the
@@ -622,21 +643,8 @@ export function createAppPageEntrypoint({
622643 fallbackPathname &&
623644 prerenderInfo ?. fallbackRouteParams ?. length
624645 ) {
625- if ( remainingPrerenderableParams . length > 0 ) {
626- const completedShellCacheKey = buildCompletedShellCacheKey (
627- fallbackPathname ,
628- remainingPrerenderableParams ,
629- params
630- )
631-
632- // If applying the current request params doesn't make the shell any
633- // more complete, then this shell is already at its most complete
634- // form and should remain shared rather than creating a new cache entry.
635- if ( completedShellCacheKey !== fallbackPathname ) {
636- ssgCacheKey = completedShellCacheKey
637- usesCompletedShellCacheKey = true
638- }
639- }
646+ ssgCacheKey = completedShellCacheKey
647+ usesCompletedShellCacheKey = completedShellCacheKey !== null
640648 } else {
641649 ssgCacheKey = resolvedPathname
642650 }
@@ -1204,43 +1212,28 @@ export function createAppPageEntrypoint({
12041212 fallbackRouteParams = null
12051213 }
12061214 } else {
1207- // In dev the prerender manifest isn't populated for ad-hoc
1208- // prefetches (`fallbackMode` is undefined for not-fully-generated
1209- // routes, so the on-demand manifest write is skipped, and
1210- // `getPrerenderManifest` is cached regardless). So
1211- // `prerenderInfo` is unavailable here. Instead base-server
1212- // derives the per-URL fallback set from the dev `getStaticPaths`
1213- // result and threads it via the `fallbackParams` request meta —
1214- // the most-specific prerendered route matching this URL, so
1215- // `generateStaticParams`-covered params resolve in the shell and
1216- // only the uncovered ones are deferred, matching what a
1217- // production build serves. `isDebugFallbackShell` (the explicit
1218- // fallback-shell debug flow) still forces the worst case.
1215+ // Dev selects the source for static-shell debug renders from
1216+ // the per-URL `getStaticPaths` result. The dev prerender
1217+ // manifest does not contain these ad-hoc paths. `base-server`
1218+ // passes the source's fallback params in request metadata.
1219+ // These renders need the source set, not the completed target's
1220+ // potentially smaller `stagedFallbackParams`.
12191221 if ( isDebugFallbackShell ) {
12201222 fallbackRouteParams = getFallbackRouteParams (
12211223 normalizedSrcPage ,
12221224 routeModule
12231225 )
12241226 } else if ( isDebugStaticShell ) {
1225- // base-server threads the per-URL fallback set via the
1226- // `fallbackParams` meta for every dev Cache Components dynamic
1227- // request, so reuse it as the fallback route params for this
1228- // shell render. It only sets the meta for routes that still
1229- // have uncovered params, so an absent meta means this URL is
1230- // fully covered by `generateStaticParams` and there is nothing
1231- // to defer (`null`).
12321227 fallbackRouteParams =
1233- getRequestMeta ( req , 'fallbackParams ' ) ?? null
1228+ getRequestMeta ( req , 'fallbackRouteParams ' ) ?? null
12341229 } else {
12351230 fallbackRouteParams = null
12361231 }
12371232 }
12381233
1239- // When rendering a debug static shell, override the fallback
1240- // params on the request so that the staged rendering correctly
1241- // defers params that are not statically known.
1242- if ( isDebugStaticShell && fallbackRouteParams ) {
1243- addRequestMeta ( req , 'fallbackParams' , fallbackRouteParams )
1234+ // Debug rendering stages the params of the selected source shell.
1235+ if ( isDebugStaticShell ) {
1236+ addRequestMeta ( req , 'stagedFallbackParams' , fallbackRouteParams )
12441237 }
12451238
12461239 // We use the response cache here to handle the revalidation and
@@ -1355,6 +1348,7 @@ export function createAppPageEntrypoint({
13551348 ! isOnDemandRevalidate && ! isRevalidating && minimalPostponed
13561349 ? minimalPostponed
13571350 : undefined
1351+ let hasFullyStaticCacheEntry = false
13581352
13591353 if (
13601354 // If this is a dynamic RSC request or a server action request, we should
@@ -1377,8 +1371,10 @@ export function createAppPageEntrypoint({
13771371 // from entering an infinite loop of revalidations.
13781372 ! forceStaticRender
13791373 ) {
1374+ const incrementalCacheKey =
1375+ completedShellCacheKey ?? resolvedPathname
13801376 const incrementalCacheEntry = await incrementalCache . get (
1381- resolvedPathname ,
1377+ incrementalCacheKey ,
13821378 {
13831379 kind : IncrementalCacheKind . APP_PAGE ,
13841380 isRoutePPREnabled : true ,
@@ -1396,6 +1392,7 @@ export function createAppPageEntrypoint({
13961392 // CRITICAL: we're assigning the postponed data from the cache entry
13971393 // here as we're using the RDC to resume the render.
13981394 postponed = incrementalCacheEntry . value . postponed
1395+ hasFullyStaticCacheEntry = postponed === undefined
13991396
14001397 // If the cache entry is stale, we should trigger a background
14011398 // revalidation so that subsequent requests will get a fresh response.
@@ -1414,7 +1411,7 @@ export function createAppPageEntrypoint({
14141411
14151412 try {
14161413 await responseCache . revalidate (
1417- resolvedPathname ,
1414+ incrementalCacheKey ,
14181415 incrementalCache ,
14191416 isRoutePPREnabled ,
14201417 false ,
@@ -1530,17 +1527,20 @@ export function createAppPageEntrypoint({
15301527 effectiveFallbackRouteParams . length <
15311528 ( prerenderInfo ?. fallbackRouteParams ?. length ?? 0 )
15321529 ? createOpaqueFallbackRouteParams ( effectiveFallbackRouteParams )
1533- : // A render cached under a completed shell cache key must keep
1534- // deferring the params the key leaves as placeholders (the
1535- // ones `generateStaticParams` can never provide) so they
1536- // resume per request instead of baking into the shared entry.
1537- // This is the blocking analog of the background shell
1538- // upgrade above and is likewise self-hosted only: in minimal
1539- // mode the platform proxy owns this contract by stripping
1540- // never-prerenderable params from the request, which defers
1541- // them through the placeholder handling instead.
1530+ : // This render defers the params left unresolved in its key.
1531+ // The render must not store their concrete values in the
1532+ // shared entry. Per-request resumes resolve those params
1533+ // instead. Blocking renders and RSC-triggered revalidations
1534+ // both need this restriction.
1535+ //
1536+ // The serving modes obtain the unresolved params differently:
1537+ // - `next start` defers them in this branch.
1538+ // - Minimal mode uses the platform's placeholders above.
1539+ // The platform strips never-prerenderable param values before
1540+ // calling the origin.
15421541 ! isMinimalMode &&
1543- usesCompletedShellCacheKey &&
1542+ ( usesCompletedShellCacheKey ||
1543+ ( forceStaticRender && completedShellCacheKey !== null ) ) &&
15441544 remainingFallbackRouteParams . length > 0
15451545 ? createOpaqueFallbackRouteParams (
15461546 remainingFallbackRouteParams
@@ -1549,24 +1549,35 @@ export function createAppPageEntrypoint({
15491549 ? getFallbackRouteParams ( normalizedSrcPage , routeModule )
15501550 : null
15511551
1552- // For staged dynamic rendering (Cached Navigations) and debug static
1553- // shell rendering, pass the fallback params via request meta so the
1554- // RequestStore knows which params to defer. We don't pass them as
1555- // fallbackRouteParams because that would replace actual param values
1556- // with opaque placeholders during segment resolution.
1552+ // Staging defers params without replacing their values with opaque
1553+ // placeholders. A resumed render reads the selected artifact's mask
1554+ // from its postponed state.
15571555 if (
15581556 ( isProduction || isDebugStaticShell ) &&
1559- nextConfig . cacheComponents &&
1560- ! isPrerendered &&
1561- prerenderInfo ?. fallbackRouteParams
1557+ nextConfig . cacheComponents
15621558 ) {
1563- const fallbackParams = createOpaqueFallbackRouteParams (
1564- fallbackRouteParamsForRender ?? prerenderInfo . fallbackRouteParams
1565- )
1566-
1567- if ( fallbackParams ) {
1568- addRequestMeta ( req , 'fallbackParams' , fallbackParams )
1559+ let stagedFallbackParams : OpaqueFallbackRouteParams | null
1560+ if ( hasPlaceholderFallbackRouteParams ) {
1561+ stagedFallbackParams = createOpaqueFallbackRouteParams (
1562+ placeholderFallbackRouteParams
1563+ )
1564+ } else if ( isDebugStaticShell ) {
1565+ stagedFallbackParams = fallbackRouteParams
1566+ } else if ( hasFullyStaticCacheEntry || isPrerendered ) {
1567+ stagedFallbackParams = null
1568+ } else if ( postponed && prerenderInfo ?. fallbackRouteParams ) {
1569+ // App-render uses this source-shell set only when the supplied
1570+ // state does not record its own fallback params.
1571+ stagedFallbackParams = createOpaqueFallbackRouteParams (
1572+ prerenderInfo . fallbackRouteParams
1573+ )
1574+ } else if ( prerenderInfo ) {
1575+ stagedFallbackParams = getStagedFallbackParams ( prerenderInfo )
1576+ } else {
1577+ stagedFallbackParams = null
15691578 }
1579+
1580+ addRequestMeta ( req , 'stagedFallbackParams' , stagedFallbackParams )
15701581 }
15711582
15721583 // Forced static and debug renders are prerenders even when the
@@ -2094,19 +2105,14 @@ export function createAppPageEntrypoint({
20942105 const transformer = new TransformStream < Uint8Array , Uint8Array > ( )
20952106 body . push ( transformer . readable )
20962107
2097- // Plumb fallback params via request meta so the RequestStore created
2098- // downstream in app-render.tsx knows which params to defer during the
2099- // resume. We don't pass them as `fallbackRouteParams` because that
2100- // would replace actual param values with opaque placeholders during
2101- // segment resolution; the resolved values are baked into the URL and
2102- // already interpolated into the postponed state.
2108+ // This metadata supplies fallback params when the saved state does not
2109+ // record them. App-render uses the saved state's own set when present.
21032110 if ( nextConfig . cacheComponents && prerenderInfo ?. fallbackRouteParams ) {
2104- const fallbackParams = createOpaqueFallbackRouteParams (
2105- prerenderInfo . fallbackRouteParams
2111+ addRequestMeta (
2112+ req ,
2113+ 'stagedFallbackParams' ,
2114+ createOpaqueFallbackRouteParams ( prerenderInfo . fallbackRouteParams )
21062115 )
2107- if ( fallbackParams ) {
2108- addRequestMeta ( req , 'fallbackParams' , fallbackParams )
2109- }
21102116 }
21112117
21122118 // Perform the render again, but this time, provide the postponed state.
@@ -2115,8 +2121,8 @@ export function createAppPageEntrypoint({
21152121 doRender ( {
21162122 span,
21172123 postponed : cachedData . postponed ,
2118- // This is a resume render, not a fallback render. Fallback params
2119- // (for cacheComponents routes) are plumbed via request meta above .
2124+ // The resume retains concrete param values; staging only defers
2125+ // access .
21202126 fallbackRouteParams : null ,
21212127 renderOperation : 'render' ,
21222128 } )
0 commit comments