@@ -3,7 +3,7 @@ import type { PagesManifest } from './webpack/plugins/pages-manifest-plugin'
3
3
import type { ExportPathMap , NextConfigComplete } from '../server/config-shared'
4
4
import type { MiddlewareManifest } from './webpack/plugins/middleware-plugin'
5
5
import type { ActionManifest } from './webpack/plugins/flight-client-entry-plugin'
6
- import type { Revalidate } from '../server/lib/revalidate '
6
+ import type { CacheControl , Revalidate } from '../server/lib/cache-control '
7
7
8
8
import '../lib/setup-exception-listeners'
9
9
@@ -209,7 +209,7 @@ import { turbopackBuild } from './turbopack-build'
209
209
210
210
type Fallback = null | boolean | string
211
211
212
- export interface SsgRoute {
212
+ export interface PrerenderManifestRoute {
213
213
dataRoute : string | null
214
214
experimentalBypassFor ?: RouteHas [ ]
215
215
@@ -224,10 +224,20 @@ export interface SsgRoute {
224
224
initialStatus ?: number
225
225
226
226
/**
227
- * The revalidation configuration for this route.
227
+ * The revalidate value for this route. This might be inferred from:
228
+ * - route segment configs
229
+ * - fetch calls
230
+ * - unstable_cache
231
+ * - "use cache"
228
232
*/
229
233
initialRevalidateSeconds : Revalidate
230
234
235
+ /**
236
+ * The expire value for this route, which is inferred from the "use cache"
237
+ * functions that are used by the route, or the expireTime config.
238
+ */
239
+ initialExpireSeconds : number | undefined
240
+
231
241
/**
232
242
* The prefetch data route associated with this page. If not defined, this
233
243
* page does not support prefetching.
@@ -258,7 +268,7 @@ export interface SsgRoute {
258
268
allowHeader : string [ ]
259
269
}
260
270
261
- export interface DynamicSsgRoute {
271
+ export interface DynamicPrerenderManifestRoute {
262
272
dataRoute : string | null
263
273
dataRouteRegex : string | null
264
274
experimentalBypassFor ?: RouteHas [ ]
@@ -270,6 +280,11 @@ export interface DynamicSsgRoute {
270
280
*/
271
281
fallbackRevalidate : Revalidate | undefined
272
282
283
+ /**
284
+ * When defined, it describes the expire configuration for the fallback route.
285
+ */
286
+ fallbackExpire : number | undefined
287
+
273
288
/**
274
289
* The headers that should used when serving the fallback.
275
290
*/
@@ -328,8 +343,8 @@ const ALLOWED_HEADERS: string[] = [
328
343
329
344
export type PrerenderManifest = {
330
345
version : 4
331
- routes : { [ route : string ] : SsgRoute }
332
- dynamicRoutes : { [ route : string ] : DynamicSsgRoute }
346
+ routes : { [ route : string ] : PrerenderManifestRoute }
347
+ dynamicRoutes : { [ route : string ] : DynamicPrerenderManifestRoute }
333
348
notFoundRoutes : string [ ]
334
349
preview : __ApiPreviewProps
335
350
}
@@ -2128,7 +2143,7 @@ export default async function build(
2128
2143
isRoutePPREnabled,
2129
2144
isHybridAmp,
2130
2145
ssgPageRoutes,
2131
- initialRevalidateSeconds : false ,
2146
+ initialCacheControl : undefined ,
2132
2147
runtime : pageRuntime ,
2133
2148
pageDuration : undefined ,
2134
2149
ssgPageDurations : undefined ,
@@ -2708,6 +2723,31 @@ export default async function build(
2708
2723
// If there was no result, there's nothing more to do.
2709
2724
if ( ! exportResult ) return
2710
2725
2726
+ const getCacheControl = (
2727
+ exportPath : string ,
2728
+ defaultRevalidate : Revalidate = false
2729
+ ) : CacheControl => {
2730
+ const cacheControl =
2731
+ exportResult . byPath . get ( exportPath ) ?. cacheControl
2732
+
2733
+ if ( ! cacheControl ) {
2734
+ return { revalidate : defaultRevalidate , expire : undefined }
2735
+ }
2736
+
2737
+ if (
2738
+ cacheControl . revalidate !== false &&
2739
+ cacheControl . revalidate > 0 &&
2740
+ cacheControl . expire === undefined
2741
+ ) {
2742
+ return {
2743
+ revalidate : cacheControl . revalidate ,
2744
+ expire : config . expireTime ,
2745
+ }
2746
+ }
2747
+
2748
+ return cacheControl
2749
+ }
2750
+
2711
2751
if ( debugOutput || process . env . NEXT_SSG_FETCH_METRICS === '1' ) {
2712
2752
recordFetchMetrics ( exportResult )
2713
2753
}
@@ -2739,7 +2779,7 @@ export default async function build(
2739
2779
2740
2780
let hasRevalidateZero =
2741
2781
appConfig . revalidate === 0 ||
2742
- exportResult . byPath . get ( page ) ? .revalidate === 0
2782
+ getCacheControl ( page ) . revalidate === 0
2743
2783
2744
2784
if ( hasRevalidateZero && pageInfos . get ( page ) ?. isStatic ) {
2745
2785
// if the page was marked as being static, but it contains dynamic data
@@ -2855,26 +2895,40 @@ export default async function build(
2855
2895
if ( route . pathname === UNDERSCORE_NOT_FOUND_ROUTE ) continue
2856
2896
2857
2897
const {
2858
- revalidate = appConfig . revalidate ?? false ,
2859
2898
metadata = { } ,
2860
2899
hasEmptyPrelude,
2861
2900
hasPostponed,
2862
2901
} = exportResult . byPath . get ( route . pathname ) ?? { }
2863
2902
2903
+ const cacheControl = getCacheControl (
2904
+ route . pathname ,
2905
+ appConfig . revalidate
2906
+ )
2907
+
2864
2908
pageInfos . set ( route . pathname , {
2865
2909
...( pageInfos . get ( route . pathname ) as PageInfo ) ,
2866
2910
hasPostponed,
2867
2911
hasEmptyPrelude,
2912
+ // TODO: Enable the following line to show "ISR" status in build
2913
+ // output. Requires different presentation to also work for app
2914
+ // router routes.
2915
+ // See https://vercel.slack.com/archives/C02CDC2ALJH/p1739552318644119?thread_ts=1739550179.439319&cid=C02CDC2ALJH
2916
+ // initialCacheControl: cacheControl,
2868
2917
} )
2869
2918
2870
2919
// update the page (eg /blog/[slug]) to also have the postpone metadata
2871
2920
pageInfos . set ( page , {
2872
2921
...( pageInfos . get ( page ) as PageInfo ) ,
2873
2922
hasPostponed,
2874
2923
hasEmptyPrelude,
2924
+ // TODO: Enable the following line to show "ISR" status in build
2925
+ // output. Requires different presentation to also work for app
2926
+ // router routes.
2927
+ // See https://vercel.slack.com/archives/C02CDC2ALJH/p1739552318644119?thread_ts=1739550179.439319&cid=C02CDC2ALJH
2928
+ // initialCacheControl: cacheControl,
2875
2929
} )
2876
2930
2877
- if ( revalidate !== 0 ) {
2931
+ if ( cacheControl . revalidate !== 0 ) {
2878
2932
const normalizedRoute = normalizePagePath ( route . pathname )
2879
2933
2880
2934
let dataRoute : string | null
@@ -2907,7 +2961,8 @@ export default async function build(
2907
2961
: undefined ,
2908
2962
experimentalPPR : isRoutePPREnabled ,
2909
2963
experimentalBypassFor : bypassFor ,
2910
- initialRevalidateSeconds : revalidate ,
2964
+ initialRevalidateSeconds : cacheControl . revalidate ,
2965
+ initialExpireSeconds : cacheControl . expire ,
2911
2966
srcRoute : page ,
2912
2967
dataRoute,
2913
2968
prefetchDataRoute,
@@ -2944,8 +2999,11 @@ export default async function build(
2944
2999
for ( const route of dynamicRoutes ) {
2945
3000
const normalizedRoute = normalizePagePath ( route . pathname )
2946
3001
2947
- const { metadata, revalidate } =
2948
- exportResult . byPath . get ( route . pathname ) ?? { }
3002
+ const metadata = exportResult . byPath . get (
3003
+ route . pathname
3004
+ ) ?. metadata
3005
+
3006
+ const cacheControl = getCacheControl ( route . pathname )
2949
3007
2950
3008
let dataRoute : string | null = null
2951
3009
if ( ! isAppRouteHandler ) {
@@ -2988,12 +3046,13 @@ export default async function build(
2988
3046
const fallbackMode =
2989
3047
route . fallbackMode ?? FallbackMode . NOT_FOUND
2990
3048
2991
- // When we're configured to serve a prerender, we should use the
2992
- // fallback revalidate from the export result. If it can't be
2993
- // found, mark that we should keep the shell forever (`false`).
2994
- let fallbackRevalidate : Revalidate | undefined =
3049
+ // When the route is configured to serve a prerender, we should
3050
+ // use the cache control from the export result. If it can't be
3051
+ // found, mark that we should keep the shell forever
3052
+ // (revalidate: `false` via `getCacheControl()`).
3053
+ const fallbackCacheControl =
2995
3054
isRoutePPREnabled && fallbackMode === FallbackMode . PRERENDER
2996
- ? revalidate ?? false
3055
+ ? cacheControl
2997
3056
: undefined
2998
3057
2999
3058
const fallback : Fallback = fallbackModeToFallbackField (
@@ -3023,7 +3082,8 @@ export default async function build(
3023
3082
) ,
3024
3083
dataRoute,
3025
3084
fallback,
3026
- fallbackRevalidate,
3085
+ fallbackRevalidate : fallbackCacheControl ?. revalidate ,
3086
+ fallbackExpire : fallbackCacheControl ?. expire ,
3027
3087
fallbackStatus : meta . status ,
3028
3088
fallbackHeaders : meta . headers ,
3029
3089
fallbackRootParams : route . fallbackRootParams ,
@@ -3268,10 +3328,11 @@ export default async function build(
3268
3328
for ( const locale of i18n . locales ) {
3269
3329
const localePage = `/${ locale } ${ page === '/' ? '' : page } `
3270
3330
3331
+ const cacheControl = getCacheControl ( localePage )
3332
+
3271
3333
prerenderManifest . routes [ localePage ] = {
3272
- initialRevalidateSeconds :
3273
- exportResult . byPath . get ( localePage ) ?. revalidate ??
3274
- false ,
3334
+ initialRevalidateSeconds : cacheControl . revalidate ,
3335
+ initialExpireSeconds : cacheControl . expire ,
3275
3336
experimentalPPR : undefined ,
3276
3337
renderingMode : undefined ,
3277
3338
srcRoute : null ,
@@ -3285,9 +3346,11 @@ export default async function build(
3285
3346
}
3286
3347
}
3287
3348
} else {
3349
+ const cacheControl = getCacheControl ( page )
3350
+
3288
3351
prerenderManifest . routes [ page ] = {
3289
- initialRevalidateSeconds :
3290
- exportResult . byPath . get ( page ) ?. revalidate ?? false ,
3352
+ initialRevalidateSeconds : cacheControl . revalidate ,
3353
+ initialExpireSeconds : cacheControl . expire ,
3291
3354
experimentalPPR : undefined ,
3292
3355
renderingMode : undefined ,
3293
3356
srcRoute : null ,
@@ -3301,10 +3364,8 @@ export default async function build(
3301
3364
allowHeader : ALLOWED_HEADERS ,
3302
3365
}
3303
3366
}
3304
- // Set Page Revalidation Interval
3305
3367
if ( pageInfo ) {
3306
- pageInfo . initialRevalidateSeconds =
3307
- exportResult . byPath . get ( page ) ?. revalidate ?? false
3368
+ pageInfo . initialCacheControl = getCacheControl ( page )
3308
3369
}
3309
3370
} else {
3310
3371
// For a dynamic SSG page, we did not copy its data exports and only
@@ -3350,15 +3411,11 @@ export default async function build(
3350
3411
)
3351
3412
}
3352
3413
3353
- const initialRevalidateSeconds =
3354
- exportResult . byPath . get ( route . pathname ) ?. revalidate ?? false
3355
-
3356
- if ( typeof initialRevalidateSeconds === 'undefined' ) {
3357
- throw new Error ( "Invariant: page wasn't built" )
3358
- }
3414
+ const cacheControl = getCacheControl ( route . pathname )
3359
3415
3360
3416
prerenderManifest . routes [ route . pathname ] = {
3361
- initialRevalidateSeconds,
3417
+ initialRevalidateSeconds : cacheControl . revalidate ,
3418
+ initialExpireSeconds : cacheControl . expire ,
3362
3419
experimentalPPR : undefined ,
3363
3420
renderingMode : undefined ,
3364
3421
srcRoute : page ,
@@ -3372,9 +3429,8 @@ export default async function build(
3372
3429
allowHeader : ALLOWED_HEADERS ,
3373
3430
}
3374
3431
3375
- // Set route Revalidation Interval
3376
3432
if ( pageInfo ) {
3377
- pageInfo . initialRevalidateSeconds = initialRevalidateSeconds
3433
+ pageInfo . initialCacheControl = cacheControl
3378
3434
}
3379
3435
}
3380
3436
}
@@ -3476,6 +3532,7 @@ export default async function build(
3476
3532
? `${ normalizedRoute } .html`
3477
3533
: false ,
3478
3534
fallbackRevalidate : undefined ,
3535
+ fallbackExpire : undefined ,
3479
3536
fallbackSourceRoute : undefined ,
3480
3537
fallbackRootParams : undefined ,
3481
3538
dataRouteRegex : normalizeRouteRegex (
0 commit comments