Skip to content

Commit a1bb287

Browse files
committed
fix: move locale detection to netlify redirects
1 parent d49f56a commit a1bb287

File tree

7 files changed

+255
-171
lines changed

7 files changed

+255
-171
lines changed

src/constants.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ export const HIDDEN_PATHS = [
1515
'/BUILD_ID',
1616
]
1717

18-
module.exports = {
19-
HIDDEN_PATHS,
20-
IMAGE_FUNCTION_NAME,
21-
HANDLER_FUNCTION_NAME,
22-
ODB_FUNCTION_NAME,
23-
}
18+
export const ODB_FUNCTION_PATH = `/.netlify/builders/${ODB_FUNCTION_NAME}`
19+
export const HANDLER_FUNCTION_PATH = `/.netlify/functions/${HANDLER_FUNCTION_NAME}`
20+
21+
export const CATCH_ALL_REGEX = /\/\[\.{3}(.*)](.json)?$/
22+
export const OPTIONAL_CATCH_ALL_REGEX = /\/\[{2}\.{3}(.*)]{2}(.json)?$/
23+
export const DYNAMIC_PARAMETER_REGEX = /\/\[(.*?)]/g

src/helpers/config.ts

Lines changed: 2 additions & 161 deletions
Original file line numberDiff line numberDiff line change
@@ -1,173 +1,15 @@
1-
/* eslint-disable max-lines */
2-
3-
import { NetlifyConfig } from '@netlify/build'
4-
import { yellowBright } from 'chalk'
51
import { readJSON } from 'fs-extra'
6-
import { PrerenderManifest } from 'next/dist/build'
7-
import { outdent } from 'outdent'
82
import { join, dirname, relative } from 'pathe'
93
import slash from 'slash'
104

11-
import { HANDLER_FUNCTION_NAME, ODB_FUNCTION_NAME, HIDDEN_PATHS } from '../constants'
5+
import { HANDLER_FUNCTION_NAME, ODB_FUNCTION_NAME } from '../constants'
126

137
import { RequiredServerFiles } from './requiredServerFilesType'
148

15-
const defaultFailBuild = (message: string, { error } ): never => {
9+
const defaultFailBuild = (message: string, { error }): never => {
1610
throw new Error(`${message}\n${error && error.stack}`)
1711
}
1812

19-
const ODB_FUNCTION_PATH = `/.netlify/builders/${ODB_FUNCTION_NAME}`
20-
const HANDLER_FUNCTION_PATH = `/.netlify/functions/${HANDLER_FUNCTION_NAME}`
21-
22-
const CATCH_ALL_REGEX = /\/\[\.{3}(.*)](.json)?$/
23-
const OPTIONAL_CATCH_ALL_REGEX = /\/\[{2}\.{3}(.*)]{2}(.json)?$/
24-
const DYNAMIC_PARAMETER_REGEX = /\/\[(.*?)]/g
25-
26-
const getNetlifyRoutes = (nextRoute: string): Array<string> => {
27-
let netlifyRoutes = [nextRoute]
28-
29-
// If the route is an optional catch-all route, we need to add a second
30-
// Netlify route for the base path (when no parameters are present).
31-
// The file ending must be present!
32-
if (OPTIONAL_CATCH_ALL_REGEX.test(nextRoute)) {
33-
let netlifyRoute = nextRoute.replace(OPTIONAL_CATCH_ALL_REGEX, '$2')
34-
35-
// When optional catch-all route is at top-level, the regex on line 19 will
36-
// create an empty string, but actually needs to be a forward slash
37-
if (netlifyRoute === '') netlifyRoute = '/'
38-
39-
// When optional catch-all route is at top-level, the regex on line 19 will
40-
// create an incorrect route for the data route. For example, it creates
41-
// /_next/data/%BUILDID%.json, but NextJS looks for
42-
// /_next/data/%BUILDID%/index.json
43-
netlifyRoute = netlifyRoute.replace(/(\/_next\/data\/[^/]+).json/, '$1/index.json')
44-
45-
// Add second route to the front of the array
46-
netlifyRoutes.unshift(netlifyRoute)
47-
}
48-
49-
// Replace catch-all, e.g., [...slug]
50-
netlifyRoutes = netlifyRoutes.map((route) => route.replace(CATCH_ALL_REGEX, '/:$1/*'))
51-
52-
// Replace optional catch-all, e.g., [[...slug]]
53-
netlifyRoutes = netlifyRoutes.map((route) => route.replace(OPTIONAL_CATCH_ALL_REGEX, '/*'))
54-
55-
// Replace dynamic parameters, e.g., [id]
56-
netlifyRoutes = netlifyRoutes.map((route) => route.replace(DYNAMIC_PARAMETER_REGEX, '/:$1'))
57-
58-
return netlifyRoutes
59-
}
60-
61-
export const generateRedirects = async ({ netlifyConfig, basePath, i18n }: {
62-
netlifyConfig: NetlifyConfig,
63-
basePath: string,
64-
i18n
65-
}) => {
66-
const { dynamicRoutes, routes: staticRoutes }: PrerenderManifest = await readJSON(
67-
join(netlifyConfig.build.publish, 'prerender-manifest.json'),
68-
)
69-
70-
netlifyConfig.redirects.push(
71-
...HIDDEN_PATHS.map((path) => ({
72-
from: `${basePath}${path}`,
73-
to: '/404.html',
74-
status: 404,
75-
force: true,
76-
})),
77-
)
78-
79-
const dataRedirects = []
80-
const pageRedirects = []
81-
const isrRedirects = []
82-
let hasIsr = false
83-
84-
const dynamicRouteEntries = Object.entries(dynamicRoutes)
85-
86-
const staticRouteEntries = Object.entries(staticRoutes)
87-
88-
staticRouteEntries.forEach(([route, { dataRoute, initialRevalidateSeconds }]) => {
89-
// Only look for revalidate as we need to rewrite these to SSR rather than ODB
90-
if (initialRevalidateSeconds === false) {
91-
// These can be ignored, as they're static files handled by the CDN
92-
return
93-
}
94-
if (i18n?.defaultLocale && route.startsWith(`/${i18n.defaultLocale}/`)) {
95-
route = route.slice(i18n.defaultLocale.length + 1)
96-
}
97-
hasIsr = true
98-
isrRedirects.push(...getNetlifyRoutes(dataRoute), ...getNetlifyRoutes(route))
99-
})
100-
101-
dynamicRouteEntries.forEach(([route, { dataRoute, fallback }]) => {
102-
// Add redirects if fallback is "null" (aka blocking) or true/a string
103-
if (fallback === false) {
104-
return
105-
}
106-
pageRedirects.push(...getNetlifyRoutes(route))
107-
dataRedirects.push(...getNetlifyRoutes(dataRoute))
108-
})
109-
110-
if (i18n) {
111-
netlifyConfig.redirects.push({ from: `${basePath}/:locale/_next/static/*`, to: `/static/:splat`, status: 200 })
112-
}
113-
114-
// This is only used in prod, so dev uses `next dev` directly
115-
netlifyConfig.redirects.push(
116-
// Static files are in `static`
117-
{ from: `${basePath}/_next/static/*`, to: `/static/:splat`, status: 200 },
118-
// API routes always need to be served from the regular function
119-
{
120-
from: `${basePath}/api`,
121-
to: HANDLER_FUNCTION_PATH,
122-
status: 200,
123-
},
124-
{
125-
from: `${basePath}/api/*`,
126-
to: HANDLER_FUNCTION_PATH,
127-
status: 200,
128-
},
129-
// Preview mode gets forced to the function, to bypess pre-rendered pages
130-
{
131-
from: `${basePath}/*`,
132-
to: HANDLER_FUNCTION_PATH,
133-
status: 200,
134-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
135-
// @ts-ignore
136-
conditions: { Cookie: ['__prerender_bypass', '__next_preview_data'] },
137-
force: true,
138-
},
139-
// ISR redirects are handled by the regular function. Forced to avoid pre-rendered pages
140-
...isrRedirects.map((redirect) => ({
141-
from: `${basePath}${redirect}`,
142-
to: process.env.EXPERIMENTAL_ODB_TTL ? ODB_FUNCTION_PATH : HANDLER_FUNCTION_PATH,
143-
status: 200,
144-
force: true,
145-
})),
146-
// These are pages with fallback set, which need an ODB
147-
// Data redirects go first, to avoid conflict with splat redirects
148-
...dataRedirects.map((redirect) => ({
149-
from: `${basePath}${redirect}`,
150-
to: ODB_FUNCTION_PATH,
151-
status: 200,
152-
})),
153-
// ...then all the other fallback pages
154-
...pageRedirects.map((redirect) => ({
155-
from: `${basePath}${redirect}`,
156-
to: ODB_FUNCTION_PATH,
157-
status: 200,
158-
})),
159-
// Everything else is handled by the regular function
160-
{ from: `${basePath}/*`, to: HANDLER_FUNCTION_PATH, status: 200 },
161-
)
162-
if (hasIsr) {
163-
console.log(
164-
yellowBright(outdent`
165-
You have some pages that use ISR (pages that use getStaticProps with revalidate set), which is not currently fully-supported by this plugin. Be aware that results may be unreliable.
166-
`),
167-
)
168-
}
169-
}
170-
17113
export const getNextConfig = async function getNextConfig({ publish, failBuild = defaultFailBuild }) {
17214
try {
17315
const { config, appDir, ignore }: RequiredServerFiles = await readJSON(join(publish, 'required-server-files.json'))
@@ -234,4 +76,3 @@ export const configureHandlerFunctions = ({ netlifyConfig, publish, ignore = []
23476
})
23577
})
23678
}
237-
/* eslint-enable max-lines */

src/helpers/files.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,18 @@ exports.moveStaticPages = async ({ netlifyConfig, target, i18n }) => {
216216
if (existsSync(defaultLocaleDir)) {
217217
await copy(defaultLocaleDir, `${netlifyConfig.build.publish}/`)
218218
}
219+
const defaultLocaleIndex = join(netlifyConfig.build.publish, `${i18n.defaultLocale}.html`)
220+
const indexHtml = join(netlifyConfig.build.publish, 'index.html')
221+
if (existsSync(defaultLocaleIndex) && !existsSync(indexHtml)) {
222+
try {
223+
await copy(defaultLocaleIndex, indexHtml, { overwrite: false })
224+
await copy(
225+
join(netlifyConfig.build.publish, `${i18n.defaultLocale}.json`),
226+
join(netlifyConfig.build.publish, 'index.json'),
227+
{ overwrite: false },
228+
)
229+
} catch {}
230+
}
219231
}
220232
}
221233

src/helpers/redirects.ts

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import { NetlifyConfig } from '@netlify/build'
2+
import { yellowBright } from 'chalk'
3+
import { readJSON } from 'fs-extra'
4+
import { NextConfig } from 'next'
5+
import { PrerenderManifest } from 'next/dist/build'
6+
import { outdent } from 'outdent'
7+
import { join } from 'pathe'
8+
9+
import { HANDLER_FUNCTION_PATH, HIDDEN_PATHS, ODB_FUNCTION_PATH } from '../constants'
10+
11+
import { netlifyRoutesForNextRoute } from './utils'
12+
13+
const generateLocaleRedirects = ({
14+
i18n,
15+
basePath,
16+
trailingSlash,
17+
}: Pick<NextConfig, 'i18n' | 'basePath' | 'trailingSlash'>): NetlifyConfig['redirects'] => {
18+
const redirects: NetlifyConfig['redirects'] = []
19+
// If the cookie is set, we need to redirect at the origin
20+
redirects.push({
21+
from: `${basePath}${trailingSlash ? '/' : ''}`,
22+
to: HANDLER_FUNCTION_PATH,
23+
status: 200,
24+
force: true,
25+
conditions: {
26+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
27+
// @ts-ignore Incorrect type
28+
Cookie: ['NEXT_LOCALE'],
29+
},
30+
})
31+
i18n.locales.forEach((locale) => {
32+
if (locale === i18n.defaultLocale) {
33+
return
34+
}
35+
redirects.push({
36+
from: `${basePath}/`,
37+
to: `${basePath}/${locale}/`,
38+
status: 301,
39+
conditions: {
40+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
41+
// @ts-ignore Incorrect type
42+
Language: locale,
43+
},
44+
force: true,
45+
})
46+
})
47+
return redirects
48+
}
49+
50+
export const generateRedirects = async ({
51+
netlifyConfig,
52+
nextConfig: { i18n, basePath, trailingSlash },
53+
}: {
54+
netlifyConfig: NetlifyConfig
55+
nextConfig: Pick<NextConfig, 'i18n' | 'basePath' | 'trailingSlash'>
56+
}) => {
57+
const { dynamicRoutes, routes: staticRoutes }: PrerenderManifest = await readJSON(
58+
join(netlifyConfig.build.publish, 'prerender-manifest.json'),
59+
)
60+
61+
netlifyConfig.redirects.push(
62+
...HIDDEN_PATHS.map((path) => ({
63+
from: `${basePath}${path}`,
64+
to: '/404.html',
65+
status: 404,
66+
force: true,
67+
})),
68+
)
69+
70+
if (i18n.localeDetection !== false) {
71+
netlifyConfig.redirects.push(...generateLocaleRedirects({ i18n, basePath, trailingSlash }))
72+
}
73+
74+
const dataRedirects = []
75+
const pageRedirects = []
76+
const isrRedirects = []
77+
let hasIsr = false
78+
79+
const dynamicRouteEntries = Object.entries(dynamicRoutes)
80+
81+
const staticRouteEntries = Object.entries(staticRoutes)
82+
83+
staticRouteEntries.forEach(([route, { dataRoute, initialRevalidateSeconds }]) => {
84+
// Only look for revalidate as we need to rewrite these to SSR rather than ODB
85+
if (initialRevalidateSeconds === false) {
86+
// These can be ignored, as they're static files handled by the CDN
87+
return
88+
}
89+
if (i18n?.defaultLocale && route.startsWith(`/${i18n.defaultLocale}/`)) {
90+
route = route.slice(i18n.defaultLocale.length + 1)
91+
}
92+
hasIsr = true
93+
isrRedirects.push(...netlifyRoutesForNextRoute(dataRoute), ...netlifyRoutesForNextRoute(route))
94+
})
95+
96+
dynamicRouteEntries.forEach(([route, { dataRoute, fallback }]) => {
97+
// Add redirects if fallback is "null" (aka blocking) or true/a string
98+
if (fallback === false) {
99+
return
100+
}
101+
pageRedirects.push(...netlifyRoutesForNextRoute(route))
102+
dataRedirects.push(...netlifyRoutesForNextRoute(dataRoute))
103+
})
104+
105+
if (i18n) {
106+
netlifyConfig.redirects.push({ from: `${basePath}/:locale/_next/static/*`, to: `/static/:splat`, status: 200 })
107+
}
108+
109+
// This is only used in prod, so dev uses `next dev` directly
110+
netlifyConfig.redirects.push(
111+
// Static files are in `static`
112+
{ from: `${basePath}/_next/static/*`, to: `/static/:splat`, status: 200 },
113+
// API routes always need to be served from the regular function
114+
{
115+
from: `${basePath}/api`,
116+
to: HANDLER_FUNCTION_PATH,
117+
status: 200,
118+
},
119+
{
120+
from: `${basePath}/api/*`,
121+
to: HANDLER_FUNCTION_PATH,
122+
status: 200,
123+
},
124+
// Preview mode gets forced to the function, to bypess pre-rendered pages
125+
{
126+
from: `${basePath}/*`,
127+
to: HANDLER_FUNCTION_PATH,
128+
status: 200,
129+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
130+
// @ts-ignore The conditions type is incorrect
131+
conditions: { Cookie: ['__prerender_bypass', '__next_preview_data'] },
132+
force: true,
133+
},
134+
// ISR redirects are handled by the regular function. Forced to avoid pre-rendered pages
135+
...isrRedirects.map((redirect) => ({
136+
from: `${basePath}${redirect}`,
137+
to: process.env.EXPERIMENTAL_ODB_TTL ? ODB_FUNCTION_PATH : HANDLER_FUNCTION_PATH,
138+
status: 200,
139+
force: true,
140+
})),
141+
// These are pages with fallback set, which need an ODB
142+
// Data redirects go first, to avoid conflict with splat redirects
143+
...dataRedirects.map((redirect) => ({
144+
from: `${basePath}${redirect}`,
145+
to: ODB_FUNCTION_PATH,
146+
status: 200,
147+
})),
148+
// ...then all the other fallback pages
149+
...pageRedirects.map((redirect) => ({
150+
from: `${basePath}${redirect}`,
151+
to: ODB_FUNCTION_PATH,
152+
status: 200,
153+
})),
154+
// Everything else is handled by the regular function
155+
{ from: `${basePath}/*`, to: HANDLER_FUNCTION_PATH, status: 200 },
156+
)
157+
if (hasIsr) {
158+
console.log(
159+
yellowBright(outdent`
160+
You have some pages that use ISR (pages that use getStaticProps with revalidate set), which is not currently fully-supported by this plugin. Be aware that results may be unreliable.
161+
`),
162+
)
163+
}
164+
}

0 commit comments

Comments
 (0)