1
- import { NetlifyConfig } from '@netlify/build'
1
+ /* eslint-disable max-lines */
2
+ import type { NetlifyConfig } from '@netlify/build'
3
+ import { yellowBright } from 'chalk'
2
4
import { readJSON } from 'fs-extra'
3
- import { NextConfig } from 'next'
4
- import { PrerenderManifest } from 'next/dist/build'
5
+ import type { NextConfig } from 'next'
6
+ import type { PrerenderManifest , SsgRoute } from 'next/dist/build'
7
+ import { outdent } from 'outdent'
5
8
import { join } from 'pathe'
6
9
7
10
import { HANDLER_FUNCTION_PATH , HIDDEN_PATHS , ODB_FUNCTION_PATH } from '../constants'
8
11
12
+ import { getMiddleware } from './files'
9
13
import { RoutesManifest } from './types'
10
14
import {
11
15
getApiRewrites ,
@@ -14,9 +18,11 @@ import {
14
18
redirectsForNextRoute ,
15
19
redirectsForNextRouteWithData ,
16
20
routeToDataRoute ,
17
- targetForFallback ,
18
21
} from './utils'
19
22
23
+ const matchesMiddleware = ( middleware : Array < string > , route : string ) : boolean =>
24
+ middleware . some ( ( middlewarePath ) => route . startsWith ( middlewarePath ) )
25
+
20
26
const generateLocaleRedirects = ( {
21
27
i18n,
22
28
basePath,
@@ -65,6 +71,150 @@ export const generateStaticRedirects = ({
65
71
}
66
72
}
67
73
74
+ /**
75
+ * Routes that match middleware need to always use the SSR function
76
+ * This generates a rewrite for every middleware in every locale, both with and without a splat
77
+ */
78
+ const generateMiddlewareRewrites = ( { basePath, middleware, i18n, buildId } ) => {
79
+ const handlerRewrite = ( from : string ) => ( {
80
+ from : `${ basePath } ${ from } ` ,
81
+ to : HANDLER_FUNCTION_PATH ,
82
+ status : 200 ,
83
+ } )
84
+
85
+ return (
86
+ middleware
87
+ . map ( ( route ) => {
88
+ const unlocalized = [ handlerRewrite ( `${ route } ` ) , handlerRewrite ( `${ route } /*` ) ]
89
+ if ( i18n ?. locales ?. length > 0 ) {
90
+ const localized = i18n . locales . map ( ( locale ) => [
91
+ handlerRewrite ( `/${ locale } ${ route } ` ) ,
92
+ handlerRewrite ( `/${ locale } ${ route } /*` ) ,
93
+ handlerRewrite ( `/_next/data/${ buildId } /${ locale } ${ route } /*` ) ,
94
+ ] )
95
+ // With i18n, all data routes are prefixed with the locale, but the HTML also has the unprefixed default
96
+ return [ ...unlocalized , ...localized ]
97
+ }
98
+ return [ ...unlocalized , handlerRewrite ( `/_next/data/${ buildId } ${ route } /*` ) ]
99
+ } )
100
+ // Flatten the array of arrays. Can't use flatMap as it might be 2 levels deep
101
+ . flat ( 2 )
102
+ )
103
+ }
104
+
105
+ const generateStaticIsrRewrites = ( {
106
+ staticRouteEntries,
107
+ basePath,
108
+ i18n,
109
+ buildId,
110
+ middleware,
111
+ } : {
112
+ staticRouteEntries : Array < [ string , SsgRoute ] >
113
+ basePath : string
114
+ i18n : NextConfig [ 'i18n' ]
115
+ buildId : string
116
+ middleware : Array < string >
117
+ } ) : {
118
+ staticRoutePaths : Set < string >
119
+ staticIsrRoutesThatMatchMiddleware : Array < string >
120
+ staticIsrRewrites : NetlifyConfig [ 'redirects' ]
121
+ } => {
122
+ const staticIsrRoutesThatMatchMiddleware : Array < string > = [ ]
123
+ const staticRoutePaths = new Set < string > ( )
124
+ const staticIsrRewrites : NetlifyConfig [ 'redirects' ] = [ ]
125
+ staticRouteEntries . forEach ( ( [ route , { initialRevalidateSeconds } ] ) => {
126
+ if ( isApiRoute ( route ) ) {
127
+ return
128
+ }
129
+ staticRoutePaths . add ( route )
130
+
131
+ if ( initialRevalidateSeconds === false ) {
132
+ // These can be ignored, as they're static files handled by the CDN
133
+ return
134
+ }
135
+ // The default locale is served from the root, not the localised path
136
+ if ( i18n ?. defaultLocale && route . startsWith ( `/${ i18n . defaultLocale } /` ) ) {
137
+ route = route . slice ( i18n . defaultLocale . length + 1 )
138
+ staticRoutePaths . add ( route )
139
+ if ( matchesMiddleware ( middleware , route ) ) {
140
+ staticIsrRoutesThatMatchMiddleware . push ( route )
141
+ }
142
+ staticIsrRewrites . push (
143
+ ...redirectsForNextRouteWithData ( {
144
+ route,
145
+ dataRoute : routeToDataRoute ( route , buildId , i18n . defaultLocale ) ,
146
+ basePath,
147
+ to : ODB_FUNCTION_PATH ,
148
+ force : true ,
149
+ } ) ,
150
+ )
151
+ } else if ( matchesMiddleware ( middleware , route ) ) {
152
+ // Routes that match middleware can't use the ODB
153
+ staticIsrRoutesThatMatchMiddleware . push ( route )
154
+ } else {
155
+ // ISR routes use the ODB handler
156
+ staticIsrRewrites . push (
157
+ // No i18n, because the route is already localized
158
+ ...redirectsForNextRoute ( { route, basePath, to : ODB_FUNCTION_PATH , force : true , buildId, i18n : null } ) ,
159
+ )
160
+ }
161
+ } )
162
+
163
+ return {
164
+ staticRoutePaths,
165
+ staticIsrRoutesThatMatchMiddleware,
166
+ staticIsrRewrites,
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Generate rewrites for all dynamic routes
172
+ */
173
+ const generateDynamicRewrites = ( {
174
+ dynamicRoutes,
175
+ prerenderedDynamicRoutes,
176
+ middleware,
177
+ basePath,
178
+ buildId,
179
+ i18n,
180
+ } : {
181
+ dynamicRoutes : RoutesManifest [ 'dynamicRoutes' ]
182
+ prerenderedDynamicRoutes : PrerenderManifest [ 'dynamicRoutes' ]
183
+ basePath : string
184
+ i18n : NextConfig [ 'i18n' ]
185
+ buildId : string
186
+ middleware : Array < string >
187
+ } ) : {
188
+ dynamicRoutesThatMatchMiddleware : Array < string >
189
+ dynamicRewrites : NetlifyConfig [ 'redirects' ]
190
+ } => {
191
+ const dynamicRewrites : NetlifyConfig [ 'redirects' ] = [ ]
192
+ const dynamicRoutesThatMatchMiddleware : Array < string > = [ ]
193
+ dynamicRoutes . forEach ( ( route ) => {
194
+ if ( isApiRoute ( route . page ) ) {
195
+ return
196
+ }
197
+ if ( route . page in prerenderedDynamicRoutes ) {
198
+ if ( matchesMiddleware ( middleware , route . page ) ) {
199
+ dynamicRoutesThatMatchMiddleware . push ( route . page )
200
+ } else {
201
+ dynamicRewrites . push (
202
+ ...redirectsForNextRoute ( { buildId, route : route . page , basePath, to : ODB_FUNCTION_PATH , status : 200 , i18n } ) ,
203
+ )
204
+ }
205
+ } else {
206
+ // If the route isn't prerendered, it's SSR
207
+ dynamicRewrites . push (
208
+ ...redirectsForNextRoute ( { route : route . page , buildId, basePath, to : HANDLER_FUNCTION_PATH , i18n } ) ,
209
+ )
210
+ }
211
+ } )
212
+ return {
213
+ dynamicRoutesThatMatchMiddleware,
214
+ dynamicRewrites,
215
+ }
216
+ }
217
+
68
218
export const generateRedirects = async ( {
69
219
netlifyConfig,
70
220
nextConfig : { i18n, basePath, trailingSlash, appDir } ,
@@ -102,43 +252,26 @@ export const generateRedirects = async ({
102
252
...( await getPreviewRewrites ( { basePath, appDir } ) ) ,
103
253
)
104
254
105
- const staticRouteEntries = Object . entries ( prerenderedStaticRoutes )
255
+ const middleware = await getMiddleware ( netlifyConfig . build . publish )
106
256
107
- const staticRoutePaths = new Set < string > ( )
257
+ netlifyConfig . redirects . push ( ... generateMiddlewareRewrites ( { basePath , i18n , middleware , buildId } ) )
108
258
109
- // First add all static ISR routes
110
- staticRouteEntries . forEach ( ( [ route , { initialRevalidateSeconds } ] ) => {
111
- if ( isApiRoute ( route ) ) {
112
- return
113
- }
114
- staticRoutePaths . add ( route )
259
+ const staticRouteEntries = Object . entries ( prerenderedStaticRoutes )
115
260
116
- if ( initialRevalidateSeconds === false ) {
117
- // These can be ignored, as they're static files handled by the CDN
118
- return
119
- }
120
- // The default locale is served from the root, not the localised path
121
- if ( i18n ?. defaultLocale && route . startsWith ( `/${ i18n . defaultLocale } /` ) ) {
122
- route = route . slice ( i18n . defaultLocale . length + 1 )
123
- staticRoutePaths . add ( route )
261
+ const routesThatMatchMiddleware : Array < string > = [ ]
124
262
125
- netlifyConfig . redirects . push (
126
- ...redirectsForNextRouteWithData ( {
127
- route,
128
- dataRoute : routeToDataRoute ( route , buildId , i18n . defaultLocale ) ,
129
- basePath,
130
- to : ODB_FUNCTION_PATH ,
131
- force : true ,
132
- } ) ,
133
- )
134
- } else {
135
- // ISR routes use the ODB handler
136
- netlifyConfig . redirects . push (
137
- // No i18n, because the route is already localized
138
- ...redirectsForNextRoute ( { route, basePath, to : ODB_FUNCTION_PATH , force : true , buildId, i18n : null } ) ,
139
- )
140
- }
263
+ const { staticRoutePaths, staticIsrRewrites, staticIsrRoutesThatMatchMiddleware } = generateStaticIsrRewrites ( {
264
+ staticRouteEntries,
265
+ basePath,
266
+ i18n,
267
+ buildId,
268
+ middleware,
141
269
} )
270
+
271
+ routesThatMatchMiddleware . push ( ...staticIsrRoutesThatMatchMiddleware )
272
+
273
+ netlifyConfig . redirects . push ( ...staticIsrRewrites )
274
+
142
275
// Add rewrites for all static SSR routes. This is Next 12+
143
276
staticRoutes ?. forEach ( ( route ) => {
144
277
if ( staticRoutePaths . has ( route . page ) || isApiRoute ( route . page ) ) {
@@ -150,28 +283,36 @@ export const generateRedirects = async ({
150
283
)
151
284
} )
152
285
// Add rewrites for all dynamic routes (both SSR and ISR)
153
- dynamicRoutes . forEach ( ( route ) => {
154
- if ( isApiRoute ( route . page ) ) {
155
- return
156
- }
157
- if ( route . page in prerenderedDynamicRoutes ) {
158
- const { fallback } = prerenderedDynamicRoutes [ route . page ]
159
-
160
- const { to, status } = targetForFallback ( fallback )
161
-
162
- netlifyConfig . redirects . push ( ...redirectsForNextRoute ( { buildId, route : route . page , basePath, to, status, i18n } ) )
163
- } else {
164
- // If the route isn't prerendered, it's SSR
165
- netlifyConfig . redirects . push (
166
- ...redirectsForNextRoute ( { route : route . page , buildId, basePath, to : HANDLER_FUNCTION_PATH , i18n } ) ,
167
- )
168
- }
286
+ const { dynamicRewrites, dynamicRoutesThatMatchMiddleware } = generateDynamicRewrites ( {
287
+ dynamicRoutes,
288
+ prerenderedDynamicRoutes,
289
+ middleware,
290
+ basePath,
291
+ buildId,
292
+ i18n,
169
293
} )
294
+ netlifyConfig . redirects . push ( ...dynamicRewrites )
295
+ routesThatMatchMiddleware . push ( ...dynamicRoutesThatMatchMiddleware )
170
296
171
297
// Final fallback
172
298
netlifyConfig . redirects . push ( {
173
299
from : `${ basePath } /*` ,
174
300
to : HANDLER_FUNCTION_PATH ,
175
301
status : 200 ,
176
302
} )
303
+
304
+ const middlewareMatches = new Set ( routesThatMatchMiddleware ) . size
305
+ if ( middlewareMatches > 0 ) {
306
+ console . log (
307
+ yellowBright ( outdent `
308
+ There ${
309
+ middlewareMatches === 1
310
+ ? `is one statically-generated or ISR route that matches`
311
+ : `are ${ middlewareMatches } statically-generated or ISR routes that match`
312
+ } a middleware function. Matched routes will always be served from the SSR function and will not use ISR or be served from the CDN.
313
+ If this was not intended, ensure that your middleware only matches routes that you intend to use SSR.
314
+ ` ) ,
315
+ )
316
+ }
177
317
}
318
+ /* eslint-enable max-lines */
0 commit comments