Skip to content

Commit 0772ede

Browse files
committed
fix: don't use ODB for routes that match middleware
1 parent fcf6bc4 commit 0772ede

File tree

9 files changed

+419
-54
lines changed

9 files changed

+419
-54
lines changed

demos/default/local-plugin/package-lock.json

+1-10
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { useRouter } from 'next/router'
2+
import Link from 'next/link'
3+
4+
const Show = ({ show }) => {
5+
const router = useRouter()
6+
7+
// This is never shown on Netlify. We just need it for NextJS to be happy,
8+
// because NextJS will render a fallback HTML page.
9+
if (router.isFallback) {
10+
return <div>Loading...</div>
11+
}
12+
13+
return (
14+
<div>
15+
<p>
16+
Check the network panel for the header <code>x-middleware-date</code> to ensure that it is running
17+
</p>
18+
19+
<hr />
20+
21+
<h1>Show #{show.id}</h1>
22+
<p>{show.name}</p>
23+
24+
<hr />
25+
26+
<Link href="/">
27+
<a>Go back home</a>
28+
</Link>
29+
</div>
30+
)
31+
}
32+
33+
export async function getStaticPaths() {
34+
// Set the paths we want to pre-render
35+
const paths = [{ params: { slug: ['my', 'path', '1'] } }, { params: { slug: ['my', 'path', '2'] } }]
36+
37+
// We'll pre-render these paths at build time.
38+
// { fallback: true } means other routes will be rendered at runtime.
39+
return { paths, fallback: true }
40+
}
41+
42+
export async function getStaticProps({ params }) {
43+
// The ID to render
44+
const { slug } = params
45+
const id = slug[slug.length - 1]
46+
47+
const res = await fetch(`https://api.tvmaze.com/shows/${id}`)
48+
const data = await res.json()
49+
50+
return {
51+
props: {
52+
show: data,
53+
},
54+
}
55+
}
56+
57+
export default Show
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { useRouter } from 'next/router'
2+
import Link from 'next/link'
3+
4+
const Show = ({ show }) => {
5+
const router = useRouter()
6+
7+
// This is never shown on Netlify. We just need it for NextJS to be happy,
8+
// because NextJS will render a fallback HTML page.
9+
if (router.isFallback) {
10+
return <div>Loading...</div>
11+
}
12+
13+
return (
14+
<div>
15+
<p>
16+
Check the network panel for the header <code>x-middleware-date</code> to ensure that it is running
17+
</p>
18+
<hr />
19+
20+
<h1>Show #{show.id}</h1>
21+
<p>{show.name}</p>
22+
23+
<hr />
24+
25+
<Link href="/">
26+
<a>Go back home</a>
27+
</Link>
28+
</div>
29+
)
30+
}
31+
32+
export async function getStaticPaths() {
33+
// Set the paths we want to pre-render
34+
const paths = [{ params: { id: '3' } }, { params: { id: '4' } }]
35+
36+
// We'll pre-render these paths at build time.
37+
// { fallback: true } means other routes will be rendered at runtime.
38+
return { paths, fallback: true }
39+
}
40+
41+
export async function getStaticProps({ params }) {
42+
// The ID to render
43+
const { id } = params
44+
45+
const res = await fetch(`https://api.tvmaze.com/shows/${id}`)
46+
const data = await res.json()
47+
48+
return {
49+
props: {
50+
show: data,
51+
},
52+
}
53+
}
54+
55+
export default Show
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { NextResponse } from 'next/server'
2+
3+
export function middleware() {
4+
const res = NextResponse.next()
5+
res.headers.set('x-middleware-date', new Date().toISOString())
6+
return res
7+
}

demos/default/pages/index.js

+14-4
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ const Index = ({ shows, nodeEnv }) => {
88

99
return (
1010
<div>
11-
<img src="/next-on-netlify.png" alt="NextJS on Netlify Banner" className='self-center w-full max-h-80 max-w-5xl m-auto' />
11+
<img
12+
src="/next-on-netlify.png"
13+
alt="NextJS on Netlify Banner"
14+
className="self-center w-full max-h-80 max-w-5xl m-auto"
15+
/>
1216

1317
<div>
1418
<Header />
@@ -18,7 +22,8 @@ const Index = ({ shows, nodeEnv }) => {
1822
<h2>Server-Side Rendering</h2>
1923

2024
<p>
21-
This page is server-side rendered. It fetches a random list of five TV shows from the TVmaze REST API. Refresh this page to see it change.
25+
This page is server-side rendered. It fetches a random list of five TV shows from the TVmaze REST API. Refresh
26+
this page to see it change.
2227
</p>
2328
<code>NODE_ENV: {nodeEnv}</code>
2429

@@ -86,8 +91,8 @@ const Index = ({ shows, nodeEnv }) => {
8691

8792
<h2>Localization</h2>
8893
<p>
89-
Localization (i18n) is supported! This demo uses <code>fr</code> with <code>en</code> as the default locale (at{' '}
90-
<code>/</code>).
94+
Localization (i18n) is supported! This demo uses <code>fr</code> with <code>en</code> as the default locale
95+
(at <code>/</code>).
9196
</p>
9297
<strong>The current locale is {locale}</strong>
9398
<p>Click on the links below to see the above text change</p>
@@ -175,6 +180,11 @@ const Index = ({ shows, nodeEnv }) => {
175180
<a>Middleware</a>
176181
</Link>
177182
</li>
183+
<li>
184+
<Link href="/getStaticProps/withFallbackAndMiddleware/4">
185+
<a>Middleware matching a pre-rendered dynamic route</a>
186+
</Link>
187+
</li>
178188
</ul>
179189
<h4>Preview mode</h4>
180190
<p>Preview mode: </p>

src/helpers/files.ts

+11-8
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,15 @@ export const matchesRewrite = (file: string, rewrites: Rewrites): boolean => {
5858
return matchesRedirect(file, rewrites.beforeFiles)
5959
}
6060

61+
export const getMiddleware = async (publish: string): Promise<Array<string>> => {
62+
const manifestPath = join(publish, 'server', 'middleware-manifest.json')
63+
if (existsSync(manifestPath)) {
64+
const manifest = await readJson(manifestPath)
65+
return manifest?.sortedMiddleware ?? []
66+
}
67+
return []
68+
}
69+
6170
// eslint-disable-next-line max-lines-per-function
6271
export const moveStaticPages = async ({
6372
netlifyConfig,
@@ -75,14 +84,8 @@ export const moveStaticPages = async ({
7584
const dataDir = join('_next', 'data', buildId)
7685
await ensureDir(dataDir)
7786
// Load the middleware manifest so we can check if a file matches it before moving
78-
let middleware
79-
const manifestPath = join(outputDir, 'middleware-manifest.json')
80-
if (existsSync(manifestPath)) {
81-
const manifest = await readJson(manifestPath)
82-
if (manifest?.middleware) {
83-
middleware = Object.keys(manifest.middleware).map((path) => path.slice(1))
84-
}
85-
}
87+
const middlewarePaths = await getMiddleware(netlifyConfig.build.publish)
88+
const middleware = middlewarePaths.map((path) => path.slice(1))
8689

8790
const prerenderManifest: PrerenderManifest = await readJson(
8891
join(netlifyConfig.build.publish, 'prerender-manifest.json'),

src/helpers/redirects.ts

+61-7
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1+
/* eslint-disable max-lines */
12
import { NetlifyConfig } from '@netlify/build'
3+
import { yellowBright } from 'chalk'
24
import { readJSON } from 'fs-extra'
35
import { NextConfig } from 'next'
46
import { PrerenderManifest } from 'next/dist/build'
7+
import { outdent } from 'outdent'
58
import { join } from 'pathe'
69

710
import { HANDLER_FUNCTION_PATH, HIDDEN_PATHS, ODB_FUNCTION_PATH } from '../constants'
811

12+
import { getMiddleware } from './files'
913
import { RoutesManifest } from './types'
1014
import {
1115
getApiRewrites,
@@ -14,9 +18,11 @@ import {
1418
redirectsForNextRoute,
1519
redirectsForNextRouteWithData,
1620
routeToDataRoute,
17-
targetForFallback,
1821
} from './utils'
1922

23+
const matchesMiddleware = (middleware: Array<string>, route: string): boolean =>
24+
middleware?.some((middlewarePath) => route.startsWith(middlewarePath))
25+
2026
const generateLocaleRedirects = ({
2127
i18n,
2228
basePath,
@@ -65,6 +71,7 @@ export const generateStaticRedirects = ({
6571
}
6672
}
6773

74+
// eslint-disable-next-line max-lines-per-function
6875
export const generateRedirects = async ({
6976
netlifyConfig,
7077
nextConfig: { i18n, basePath, trailingSlash, appDir },
@@ -102,6 +109,32 @@ export const generateRedirects = async ({
102109
...(await getPreviewRewrites({ basePath, appDir })),
103110
)
104111

112+
const middleware = await getMiddleware(netlifyConfig.build.publish)
113+
const routesThatMatchMiddleware = new Set<string>()
114+
115+
const handlerRewrite = (from: string) => ({
116+
from: `${basePath}${from}`,
117+
to: HANDLER_FUNCTION_PATH,
118+
status: 200,
119+
})
120+
121+
// Routes that match middleware need to always use the SSR function
122+
// This generates a rewrite for every middleware in every locale, both with and without a splat
123+
netlifyConfig.redirects.push(
124+
...middleware
125+
.map((route) => [
126+
handlerRewrite(`${route}`),
127+
handlerRewrite(`${route}/*`),
128+
handlerRewrite(routeToDataRoute(`${route}/*`, buildId)),
129+
...(i18n?.locales?.map((locale) => [
130+
handlerRewrite(`/${locale}${route}`),
131+
handlerRewrite(`/${locale}${route}/*`),
132+
handlerRewrite(routeToDataRoute(`${route}/*`, buildId, locale)),
133+
]) ?? []),
134+
])
135+
.flat(2),
136+
)
137+
105138
const staticRouteEntries = Object.entries(prerenderedStaticRoutes)
106139

107140
const staticRoutePaths = new Set<string>()
@@ -121,7 +154,9 @@ export const generateRedirects = async ({
121154
if (i18n?.defaultLocale && route.startsWith(`/${i18n.defaultLocale}/`)) {
122155
route = route.slice(i18n.defaultLocale.length + 1)
123156
staticRoutePaths.add(route)
124-
157+
if (matchesMiddleware(middleware, route)) {
158+
routesThatMatchMiddleware.add(route)
159+
}
125160
netlifyConfig.redirects.push(
126161
...redirectsForNextRouteWithData({
127162
route,
@@ -132,6 +167,9 @@ export const generateRedirects = async ({
132167
}),
133168
)
134169
} else {
170+
if (matchesMiddleware(middleware, route)) {
171+
routesThatMatchMiddleware.add(route)
172+
}
135173
// ISR routes use the ODB handler
136174
netlifyConfig.redirects.push(
137175
// No i18n, because the route is already localized
@@ -155,11 +193,12 @@ export const generateRedirects = async ({
155193
return
156194
}
157195
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 }))
196+
if (matchesMiddleware(middleware, route.page)) {
197+
routesThatMatchMiddleware.add(route.page)
198+
}
199+
netlifyConfig.redirects.push(
200+
...redirectsForNextRoute({ buildId, route: route.page, basePath, to: ODB_FUNCTION_PATH, status: 200, i18n }),
201+
)
163202
} else {
164203
// If the route isn't prerendered, it's SSR
165204
netlifyConfig.redirects.push(
@@ -174,4 +213,19 @@ export const generateRedirects = async ({
174213
to: HANDLER_FUNCTION_PATH,
175214
status: 200,
176215
})
216+
217+
const middlewareMatches = routesThatMatchMiddleware.size
218+
if (middlewareMatches > 0) {
219+
console.log(
220+
yellowBright(outdent`
221+
There ${
222+
middlewareMatches === 1
223+
? `is one statically-generated or ISR route`
224+
: `are ${middlewareMatches} statically-generated or ISR routes`
225+
} that match a middleware function, which means they will always be served from the SSR function and will not use ISR or be served from the CDN.
226+
If this was not intended, ensure that your middleware only matches routes that you intend to use SSR.
227+
`),
228+
)
229+
}
177230
}
231+
/* eslint-enable max-lines */

src/helpers/utils.ts

+1-17
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,7 @@ import { NetlifyConfig } from '@netlify/build'
22
import globby from 'globby'
33
import { join } from 'pathe'
44

5-
import {
6-
OPTIONAL_CATCH_ALL_REGEX,
7-
CATCH_ALL_REGEX,
8-
DYNAMIC_PARAMETER_REGEX,
9-
ODB_FUNCTION_PATH,
10-
HANDLER_FUNCTION_PATH,
11-
} from '../constants'
5+
import { OPTIONAL_CATCH_ALL_REGEX, CATCH_ALL_REGEX, DYNAMIC_PARAMETER_REGEX, HANDLER_FUNCTION_PATH } from '../constants'
126

137
import { I18n } from './types'
148

@@ -77,16 +71,6 @@ const netlifyRoutesForNextRoute = (route: string, buildId: string, i18n?: I18n):
7771

7872
export const isApiRoute = (route: string) => route.startsWith('/api/') || route === '/api'
7973

80-
export const targetForFallback = (fallback: string | false) => {
81-
if (fallback === null || fallback === false) {
82-
// fallback = null mean "blocking", which uses ODB. For fallback=false then anything prerendered should 404.
83-
// However i18n pages may not have been prerendered, so we still need to hit the origin
84-
return { to: ODB_FUNCTION_PATH, status: 200 }
85-
}
86-
// fallback = true is also ODB
87-
return { to: ODB_FUNCTION_PATH, status: 200 }
88-
}
89-
9074
export const redirectsForNextRoute = ({
9175
route,
9276
buildId,

0 commit comments

Comments
 (0)