Skip to content

Commit 46d8f3a

Browse files
feat: added better custom header support (#1358)
* feat: added better custom header support * test: added some tests for custom headers * fix: fixed issue if there is no routes manifest * test: added custom header E2E test * chore: updated type for the route manifest * chore: updated loading of routes manifest * test: some tests are no longer async * test: some more test for custom headers * test: added some tests for custom headers * test: removed redundant test * feat: added support for basePath in custom headers * test: tests for basePath in custom headers * feat: implemented locale paths for custom headers in the Netlify configuration * test: removed tests that aren't actually testing the feature * test: move back to toEqual from toMatchInlineSnapshot * feat: updated custom headers logic for headers generated in the Netlify configuration * chore: cleaned up some dead code * fix: now generated headers in Netlify configuration take default locale into consideration * Update plugin/src/helpers/config.ts Co-authored-by: Erica Pisani <[email protected]> * test: added onPostBuild test to check for header generation * test: added more onPostBuild test to check for header generation * chore: undoing a copy pasta error * chore: package-lock.json was out of date for nx-next-monorepo-demo * chore: package lock updated again Co-authored-by: Erica Pisani <[email protected]>
1 parent 85dc85b commit 46d8f3a

File tree

5 files changed

+784
-19
lines changed

5 files changed

+784
-19
lines changed

cypress/integration/default/headers.spec.ts

-9
This file was deleted.

demos/default/next.config.js

+18
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,24 @@ module.exports = {
1919
},
2020
],
2121
},
22+
{
23+
source: '/api/:path*',
24+
headers: [
25+
{
26+
key: 'x-custom-api-header',
27+
value: 'my custom api header value',
28+
},
29+
],
30+
},
31+
{
32+
source: '/:path*',
33+
headers: [
34+
{
35+
key: 'x-custom-header-for-everything',
36+
value: 'my custom header for everything value',
37+
},
38+
],
39+
},
2240
]
2341
},
2442
trailingSlash: true,

plugin/src/helpers/config.ts

+96-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
1+
import type { NetlifyConfig } from '@netlify/build'
12
import { readJSON, writeJSON } from 'fs-extra'
3+
import type { Header } from 'next/dist/lib/load-custom-routes'
24
import type { NextConfigComplete } from 'next/dist/server/config-shared'
35
import { join, dirname, relative } from 'pathe'
46
import slash from 'slash'
57

68
import { HANDLER_FUNCTION_NAME, ODB_FUNCTION_NAME } from '../constants'
79

10+
import type { RoutesManifest } from './types'
11+
12+
const ROUTES_MANIFEST_FILE = 'routes-manifest.json'
13+
14+
type NetlifyHeaders = NetlifyConfig['headers']
15+
816
export interface RequiredServerFiles {
917
version?: number
1018
config?: NextConfigComplete
@@ -13,7 +21,10 @@ export interface RequiredServerFiles {
1321
ignore?: string[]
1422
}
1523

16-
export type NextConfig = Pick<RequiredServerFiles, 'appDir' | 'ignore'> & NextConfigComplete
24+
export type NextConfig = Pick<RequiredServerFiles, 'appDir' | 'ignore'> &
25+
NextConfigComplete & {
26+
routesManifest?: RoutesManifest
27+
}
1728

1829
const defaultFailBuild = (message: string, { error }): never => {
1930
throw new Error(`${message}\n${error && error.stack}`)
@@ -30,7 +41,11 @@ export const getNextConfig = async function getNextConfig({
3041
// @ts-ignore
3142
return failBuild('Error loading your Next config')
3243
}
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 }
3449
} catch (error: unknown) {
3550
return failBuild('Error loading your Next config', { error })
3651
}
@@ -108,3 +123,82 @@ export const configureHandlerFunctions = ({ netlifyConfig, publish, ignore = []
108123
})
109124
})
110125
}
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+
}

plugin/src/index.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
getRequiredServerFiles,
1313
updateRequiredServerFiles,
1414
configureHandlerFunctions,
15+
generateCustomHeaders,
1516
} from './helpers/config'
1617
import { updateConfig, writeMiddleware } from './helpers/edge'
1718
import { moveStaticPages, movePublicFiles, patchNextFiles, unpatchNextFiles } from './helpers/files'
@@ -136,6 +137,7 @@ const plugin: NetlifyPlugin = {
136137
netlifyConfig: {
137138
build: { publish },
138139
redirects,
140+
headers,
139141
},
140142
utils: {
141143
status,
@@ -161,7 +163,12 @@ const plugin: NetlifyPlugin = {
161163

162164
await checkForOldFunctions({ functions })
163165
await checkZipSize(join(FUNCTIONS_DIST, `${ODB_FUNCTION_NAME}.zip`))
164-
const { basePath, appDir } = await getNextConfig({ publish, failBuild })
166+
const nextConfig = await getNextConfig({ publish, failBuild })
167+
168+
const { basePath, appDir } = nextConfig
169+
170+
generateCustomHeaders(nextConfig, headers)
171+
165172
warnForProblematicUserRewrites({ basePath, redirects })
166173
warnForRootRedirects({ appDir })
167174
await unpatchNextFiles(basePath)

0 commit comments

Comments
 (0)