Skip to content

Commit 6b61643

Browse files
fix: correct redirect priority and correctly handle ISR pages assets (#826)
* fix: correct redirect sorting * fix: don't move isr files * chore: demo fixes * fix: patch fs write methods * fix: update tests * fix: handle error and remove patch * fix: disable isr disk cache * fix: add isr warning * chore: add comments * fix: snapshot Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
1 parent 3256839 commit 6b61643

File tree

8 files changed

+283
-140
lines changed

8 files changed

+283
-140
lines changed

demo/pages/api/hello.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
22

33
export default (req, res) => {
4-
res.status(200).json({ name: 'John Doe' })
4+
res.status(200).json({ name: 'John Doe', query: req.query })
55
}

demo/pages/getStaticProps/withRevalidate/[id].js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import Link from 'next/link'
22

3-
const Show = ({ show }) => (
3+
const Show = ({ show, time }) => (
44
<div>
55
<p>This page uses getStaticProps() to pre-fetch a TV show.</p>
66

77
<hr />
88

99
<h1>Show #{show.id}</h1>
1010
<p>{show.name}</p>
11-
11+
<p>Rendered at {time}</p>
1212
<hr />
1313

1414
<Link href="/">
@@ -32,10 +32,12 @@ export async function getStaticProps({ params }) {
3232

3333
const res = await fetch(`https://api.tvmaze.com/shows/${id}`)
3434
const data = await res.json()
35-
35+
const time = new Date().toLocaleTimeString()
36+
await new Promise((resolve) => setTimeout(resolve, 1000))
3637
return {
3738
props: {
3839
show: data,
40+
time,
3941
},
4042
revalidate: 1,
4143
}

demo/pages/index.js

Lines changed: 26 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -4,29 +4,18 @@ const Header = dynamic(() => import(/* webpackChunkName: 'header' */ '../compone
44
import { useRouter } from 'next/router'
55

66
const Index = ({ shows }) => {
7-
const { locale } = useRouter();
7+
const { locale } = useRouter()
88

99
return (
1010
<div>
1111
<img src="/next-on-netlify.png" alt="NextJS on Netlify Banner" style={{ maxWidth: '100%' }} />
12-
13-
<Header/>
12+
13+
<Header />
1414

1515
<h1>NextJS on Netlify</h1>
16-
<p>
17-
This is a demo of a NextJS application with Server-Side Rendering (SSR).
18-
<br />
19-
It is hosted on Netlify.
20-
<br />
21-
Server-side rendering is handled by Netlify Functions.
22-
<br />
23-
Minimal configuration is required.
24-
<br />
25-
Everything is handled by the <a href="https://www.npmjs.com/package/next-on-netlify">next-on-netlify</a> npm
26-
package.
27-
</p>
16+
<p>This is a demo of a NextJS application with Server-Side Rendering (SSR).</p>
2817

29-
<h2>1. Server-Side Rendering Made Easy</h2>
18+
<h2>Server-Side Rendering</h2>
3019
<p>
3120
This page is server-side rendered.
3221
<br />
@@ -38,7 +27,7 @@ const Index = ({ shows }) => {
3827
<ul data-testid="list-server-side">
3928
{shows.map(({ id, name }) => (
4029
<li key={id}>
41-
<Link href="/shows/[id]" as={`/shows/${id}`}>
30+
<Link href={`/shows/${id}`}>
4231
<a>
4332
#{id}: {name}
4433
</a>
@@ -47,17 +36,13 @@ const Index = ({ shows }) => {
4736
))}
4837
</ul>
4938

50-
<h2>2. Full Support for Dynamic Pages</h2>
51-
<p>
52-
Dynamic pages, introduced in NextJS 9.2, are fully supported.
53-
<br />
54-
Click on a show to check out a server-side rendered page with dynamic routing (/shows/:id).
55-
</p>
39+
<h2>Dynamic Pages</h2>
40+
<p>Click on a show to check out a server-side rendered page with dynamic routing (/shows/:id).</p>
5641

5742
<ul data-testid="list-dynamic-pages">
5843
{shows.slice(0, 3).map(({ id, name }) => (
5944
<li key={id}>
60-
<Link href="/shows/[id]" as={`/shows/${id}`}>
45+
<Link href={`/shows/${id}`}>
6146
<a>
6247
#{id}: {name}
6348
</a>
@@ -66,40 +51,27 @@ const Index = ({ shows }) => {
6651
))}
6752
</ul>
6853

69-
<h2>3. Catch-All Routes? Included ✔</h2>
70-
<p>
71-
You can even take advantage of{' '}
72-
<a href="https://nextjs.org/docs/routing/dynamic-routes#catch-all-routes">NextJS catch-all routes feature</a>
73-
.
74-
<br />
75-
Here are three examples:
76-
</p>
54+
<h2>Catch-All Routess</h2>
55+
7756
<ul data-testid="list-catch-all">
7857
<li>
79-
<Link href="/shows/[...params]" as={`/shows/73/whatever/path/you/want`}>
58+
<Link href={`/shows/73/whatever/path/you/want`}>
8059
<a>/shows/73/whatever/path/you/want</a>
8160
</Link>
8261
</li>
8362
<li>
84-
<Link href="/shows/[...params]" as={`/shows/94/whatever/path/you`}>
63+
<Link href={`/shows/94/whatever/path/you`}>
8564
<a>/shows/94/whatever/path/you</a>
8665
</Link>
8766
</li>
8867
<li>
89-
<Link href="/shows/[...params]" as={`/shows/106/whatever/path`}>
68+
<Link href={`/shows/106/whatever/path`}>
9069
<a>/shows/106/whatever/path</a>
9170
</Link>
9271
</li>
9372
</ul>
9473

95-
<h2>4. Static Pages Stay Static</h2>
96-
<p>
97-
next-on-netlify automatically determines which pages are dynamic and which ones are static.
98-
<br />
99-
Only dynamic pages are server-side rendered.
100-
<br />
101-
Static pages are pre-rendered and served directly by Netlify&apos;s CDN.
102-
</p>
74+
<h2>Static Pages</h2>
10375

10476
<ul data-testid="list-static">
10577
<li>
@@ -108,49 +80,47 @@ const Index = ({ shows }) => {
10880
</Link>
10981
</li>
11082
<li>
111-
<Link href="/static/[id]" as="/static/123456789">
83+
<Link href="/static/123456789">
11284
<a>Static NextJS page with dynamic routing: /static/:id</a>
11385
</Link>
11486
</li>
11587
</ul>
11688

117-
<h2>5. Localization As Expected</h2>
89+
<h2>Localization</h2>
11890
<p>
119-
Localization (i18n) is supported! This demo uses <code>fr</code> with <code>en</code> as the default locale (at <code>/</code>).
91+
Localization (i18n) is supported! This demo uses <code>fr</code> with <code>en</code> as the default locale (at{' '}
92+
<code>/</code>).
12093
</p>
12194
<strong>The current locale is {locale}</strong>
12295
<p>Click on the links below to see the above text change</p>
12396
<ul data-testid="list-localization">
12497
<li>
12598
<Link href="/fr">
126-
<a>/fr</a>
99+
<a>/fr</a>
127100
</Link>
128101
</li>
129102
<li>
130103
<Link href="/en">
131-
<a>/en (default locale)</a>
104+
<a>/en (default locale)</a>
132105
</Link>
133106
</li>
134107
</ul>
135-
136-
<h1>Want to Learn More?</h1>
137-
<p>
138-
Check out the <a href="https://github.com/FinnWoelm/next-on-netlify-demo">source code on GitHub</a>.
139-
</p>
140108
</div>
141109
)
142110
}
143111

144112
Index.getInitialProps = async function () {
145-
const dev = process.env.CONTEXT !== 'production';
113+
const dev = process.env.CONTEXT !== 'production'
146114

147115
// Set a random page between 1 and 100
148116
const randomPage = Math.floor(Math.random() * 100) + 1
149117
// FIXME: stub out in dev
150-
const server = dev ? `https://api.tvmaze.com/shows?page=${randomPage}` : `https://api.tvmaze.com/shows?page=${randomPage}`;
118+
const server = dev
119+
? `https://api.tvmaze.com/shows?page=${randomPage}`
120+
: `https://api.tvmaze.com/shows?page=${randomPage}`
151121

152122
// Get the data
153-
const res = await fetch(server);
123+
const res = await fetch(server)
154124
const data = await res.json()
155125

156126
return { shows: data.slice(0, 5) }

src/helpers/config.js

Lines changed: 67 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1-
// @ts-check
1+
/* eslint-disable max-lines */
2+
3+
const { yellowBright } = require('chalk')
24
const { readJSON, existsSync } = require('fs-extra')
5+
const { outdent } = require('outdent')
36
const { join, dirname, relative } = require('pathe')
47
const slash = require('slash')
58

@@ -52,9 +55,9 @@ const getNetlifyRoutes = (nextRoute) => {
5255
}
5356

5457
exports.generateRedirects = async ({ netlifyConfig, basePath, i18n }) => {
55-
const { dynamicRoutes } = await readJSON(join(netlifyConfig.build.publish, 'prerender-manifest.json'))
56-
57-
const redirects = []
58+
const { dynamicRoutes, routes: staticRoutes } = await readJSON(
59+
join(netlifyConfig.build.publish, 'prerender-manifest.json'),
60+
)
5861

5962
netlifyConfig.redirects.push(
6063
...HIDDEN_PATHS.map((path) => ({
@@ -65,15 +68,35 @@ exports.generateRedirects = async ({ netlifyConfig, basePath, i18n }) => {
6568
})),
6669
)
6770

71+
const dataRedirects = []
72+
const pageRedirects = []
73+
const isrRedirects = []
74+
let hasIsr = false
75+
6876
const dynamicRouteEntries = Object.entries(dynamicRoutes)
69-
dynamicRouteEntries.sort((a, b) => a[0].localeCompare(b[0]))
77+
78+
const staticRouteEntries = Object.entries(staticRoutes)
79+
80+
staticRouteEntries.forEach(([route, { dataRoute, initialRevalidateSeconds }]) => {
81+
// Only look for revalidate as we need to rewrite these to SSR rather than ODB
82+
if (initialRevalidateSeconds === false) {
83+
// These can be ignored, as they're static files handled by the CDN
84+
return
85+
}
86+
if (i18n.defaultLocale && route.startsWith(`/${i18n.defaultLocale}/`)) {
87+
route = route.slice(i18n.defaultLocale.length + 1)
88+
}
89+
hasIsr = true
90+
isrRedirects.push(...getNetlifyRoutes(dataRoute), ...getNetlifyRoutes(route))
91+
})
7092

7193
dynamicRouteEntries.forEach(([route, { dataRoute, fallback }]) => {
7294
// Add redirects if fallback is "null" (aka blocking) or true/a string
7395
if (fallback === false) {
7496
return
7597
}
76-
redirects.push(...getNetlifyRoutes(route), ...getNetlifyRoutes(dataRoute))
98+
pageRedirects.push(...getNetlifyRoutes(route))
99+
dataRedirects.push(...getNetlifyRoutes(dataRoute))
77100
})
78101

79102
if (i18n) {
@@ -82,21 +105,57 @@ exports.generateRedirects = async ({ netlifyConfig, basePath, i18n }) => {
82105

83106
// This is only used in prod, so dev uses `next dev` directly
84107
netlifyConfig.redirects.push(
108+
// Static files are in `static`
85109
{ from: `${basePath}/_next/static/*`, to: `/static/:splat`, status: 200 },
110+
// API routes always need to be served from the regular function
111+
{
112+
from: `${basePath}/api`,
113+
to: HANDLER_FUNCTION_PATH,
114+
status: 200,
115+
},
116+
{
117+
from: `${basePath}/api/*`,
118+
to: HANDLER_FUNCTION_PATH,
119+
status: 200,
120+
},
121+
// Preview mode gets forced to the function, to bypess pre-rendered pages
86122
{
87123
from: `${basePath}/*`,
88124
to: HANDLER_FUNCTION_PATH,
89125
status: 200,
90126
conditions: { Cookie: ['__prerender_bypass', '__next_preview_data'] },
91127
force: true,
92128
},
93-
...redirects.map((redirect) => ({
129+
// ISR redirects are handled by the regular function. Forced to avoid pre-rendered pages
130+
...isrRedirects.map((redirect) => ({
131+
from: `${basePath}${redirect}`,
132+
to: HANDLER_FUNCTION_PATH,
133+
status: 200,
134+
force: true,
135+
})),
136+
// These are pages with fallback set, which need an ODB
137+
// Data redirects go first, to avoid conflict with splat redirects
138+
...dataRedirects.map((redirect) => ({
94139
from: `${basePath}${redirect}`,
95140
to: ODB_FUNCTION_PATH,
96141
status: 200,
97142
})),
143+
// ...then all the other fallback pages
144+
...pageRedirects.map((redirect) => ({
145+
from: `${basePath}${redirect}`,
146+
to: ODB_FUNCTION_PATH,
147+
status: 200,
148+
})),
149+
// Everything else is handled by the regular function
98150
{ from: `${basePath}/*`, to: HANDLER_FUNCTION_PATH, status: 200 },
99151
)
152+
if (hasIsr) {
153+
console.log(
154+
yellowBright(outdent`
155+
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.
156+
`),
157+
)
158+
}
100159
}
101160

102161
exports.getNextConfig = async function getNextConfig({ publish, failBuild = defaultFailBuild }) {
@@ -159,3 +218,4 @@ exports.configureHandlerFunctions = ({ netlifyConfig, publish, ignore = [] }) =>
159218
})
160219
})
161220
}
221+
/* eslint-enable max-lines */

src/helpers/files.js

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,29 @@ exports.moveStaticPages = async ({ netlifyConfig, target, i18n }) => {
4747
}
4848
}
4949

50+
const prerenderManifest = await readJson(join(netlifyConfig.build.publish, 'prerender-manifest.json'))
51+
52+
const isrFiles = new Set()
53+
54+
Object.entries(prerenderManifest.routes).forEach(([route, { initialRevalidateSeconds }]) => {
55+
if (initialRevalidateSeconds) {
56+
// Find all files used by ISR routes
57+
const trimmedPath = route.slice(1)
58+
isrFiles.add(`${trimmedPath}.html`)
59+
isrFiles.add(`${trimmedPath}.json`)
60+
}
61+
})
62+
5063
const files = []
5164
const moveFile = async (file) => {
5265
const source = join(root, file)
5366
files.push(file)
5467
const dest = join(netlifyConfig.build.publish, file)
55-
await move(source, dest)
68+
try {
69+
await move(source, dest)
70+
} catch (error) {
71+
console.warn('Error moving file', source, error)
72+
}
5673
}
5774
// Move all static files, except error documents and nft manifests
5875
const pages = await globby(['**/*.{html,json}', '!**/(500|404|*.js.nft).{html,json}'], {
@@ -67,6 +84,10 @@ exports.moveStaticPages = async ({ netlifyConfig, target, i18n }) => {
6784
const limit = pLimit(Math.max(2, cpus().length))
6885
const promises = pages.map(async (rawPath) => {
6986
const filePath = slash(rawPath)
87+
// Don't move ISR files, as they're used for the first request
88+
if (isrFiles.has(filePath)) {
89+
return
90+
}
7091
if (isDynamicRoute(filePath)) {
7192
return
7293
}

0 commit comments

Comments
 (0)