1
+ import type { NetlifyConfig } from '@netlify/build'
1
2
import { readJSON , writeJSON } from 'fs-extra'
3
+ import type { Header } from 'next/dist/lib/load-custom-routes'
2
4
import type { NextConfigComplete } from 'next/dist/server/config-shared'
3
5
import { join , dirname , relative } from 'pathe'
4
6
import slash from 'slash'
5
7
6
8
import { HANDLER_FUNCTION_NAME , ODB_FUNCTION_NAME } from '../constants'
7
9
10
+ import type { RoutesManifest } from './types'
11
+
12
+ const ROUTES_MANIFEST_FILE = 'routes-manifest.json'
13
+
14
+ type NetlifyHeaders = NetlifyConfig [ 'headers' ]
15
+
8
16
export interface RequiredServerFiles {
9
17
version ?: number
10
18
config ?: NextConfigComplete
@@ -13,7 +21,10 @@ export interface RequiredServerFiles {
13
21
ignore ?: string [ ]
14
22
}
15
23
16
- export type NextConfig = Pick < RequiredServerFiles , 'appDir' | 'ignore' > & NextConfigComplete
24
+ export type NextConfig = Pick < RequiredServerFiles , 'appDir' | 'ignore' > &
25
+ NextConfigComplete & {
26
+ routesManifest ?: RoutesManifest
27
+ }
17
28
18
29
const defaultFailBuild = ( message : string , { error } ) : never => {
19
30
throw new Error ( `${ message } \n${ error && error . stack } ` )
@@ -30,7 +41,11 @@ export const getNextConfig = async function getNextConfig({
30
41
// @ts -ignore
31
42
return failBuild ( 'Error loading your Next config' )
32
43
}
33
- return { ...config , appDir, ignore }
44
+
45
+ const routesManifest : RoutesManifest = await readJSON ( join ( publish , ROUTES_MANIFEST_FILE ) )
46
+
47
+ // If you need access to other manifest files, you can add them here as well
48
+ return { ...config , appDir, ignore, routesManifest }
34
49
} catch ( error : unknown ) {
35
50
return failBuild ( 'Error loading your Next config' , { error } )
36
51
}
@@ -108,3 +123,82 @@ export const configureHandlerFunctions = ({ netlifyConfig, publish, ignore = []
108
123
} )
109
124
} )
110
125
}
126
+
127
+ interface BuildHeaderParams {
128
+ path : string
129
+ headers : Header [ 'headers' ]
130
+ locale ?: string
131
+ }
132
+
133
+ const buildHeader = ( buildHeaderParams : BuildHeaderParams ) => {
134
+ const { path, headers } = buildHeaderParams
135
+
136
+ return {
137
+ for : path ,
138
+ values : headers . reduce ( ( builtHeaders , { key, value } ) => {
139
+ builtHeaders [ key ] = value
140
+
141
+ return builtHeaders
142
+ } , { } ) ,
143
+ }
144
+ }
145
+
146
+ // Replace the pattern :path* at the end of a path with * since it's a named splat which the Netlify
147
+ // configuration does not support.
148
+ const sanitizePath = ( path : string ) => path . replace ( / : [ ^ * / ] + \* $ / , '*' )
149
+
150
+ /**
151
+ * Persist NEXT.js custom headers to the Netlify configuration so the headers work with static files
152
+ * See {@link https://nextjs.org/docs/api-reference/next.config.js/headers} for more information on custom
153
+ * headers in Next.js
154
+ *
155
+ * @param nextConfig - The NextJS configuration
156
+ * @param netlifyHeaders - Existing headers that are already configured in the Netlify configuration
157
+ */
158
+ export const generateCustomHeaders = ( nextConfig : NextConfig , netlifyHeaders : NetlifyHeaders = [ ] ) => {
159
+ // The routesManifest is the contents of the routes-manifest.json file which will already contain the generated
160
+ // header paths which take locales and base path into account since this runs after the build. The routes-manifest.json
161
+ // file is located at demos/default/.next/routes-manifest.json once you've build the demo site.
162
+ const {
163
+ routesManifest : { headers : customHeaders = [ ] } ,
164
+ i18n,
165
+ } = nextConfig
166
+
167
+ // Skip `has` based custom headers as they have more complex dynamic conditional header logic
168
+ // that currently isn't supported by the Netlify configuration.
169
+ // Also, this type of dynamic header logic is most likely not for SSG pages.
170
+ for ( const { source, headers, locale : localeEnabled } of customHeaders . filter ( ( customHeader ) => ! customHeader . has ) ) {
171
+ // Explicitly checking false to make the check simpler.
172
+ // Locale specific paths are excluded only if localeEnabled is false. There is no true value for localeEnabled. It's either
173
+ // false or undefined, where undefined means it's true.
174
+ //
175
+ // Again, the routesManifest has already been generated taking locales into account, but the check is required
176
+ // so the paths can be properly set in the Netlify configuration.
177
+ const useLocale = i18n ?. locales ?. length > 0 && localeEnabled !== false
178
+
179
+ if ( useLocale ) {
180
+ const { locales } = i18n
181
+ const joinedLocales = locales . join ( '|' )
182
+
183
+ /**
184
+ * converts e.g.
185
+ * /:nextInternalLocale(en|fr)/some-path
186
+ * to a path for each locale
187
+ * /en/some-path and /fr/some-path as well as /some-path (default locale)
188
+ */
189
+ const defaultLocalePath = sanitizePath ( source ) . replace ( `/:nextInternalLocale(${ joinedLocales } )` , '' )
190
+
191
+ netlifyHeaders . push ( buildHeader ( { path : defaultLocalePath , headers } ) )
192
+
193
+ for ( const locale of locales ) {
194
+ const path = sanitizePath ( source ) . replace ( `:nextInternalLocale(${ joinedLocales } )` , locale )
195
+
196
+ netlifyHeaders . push ( buildHeader ( { path, headers } ) )
197
+ }
198
+ } else {
199
+ const path = sanitizePath ( source )
200
+
201
+ netlifyHeaders . push ( buildHeader ( { path, headers } ) )
202
+ }
203
+ }
204
+ }
0 commit comments