Skip to content
This repository was archived by the owner on May 10, 2021. It is now read-only.

Commit 0412b45

Browse files
committed
Fix redirects for data routes of catch-all pages
When the user navigates client-side (using NextJS Link) to a page that uses `getServerSideProps` or `getStaticProps`, NextJS makes a request for the page's data. This request goes to a special path that follows this pattern: @_next/data/%BUILD_ID%/path/to/page.json For pages with fallback: true, we need to redirect that route to the page's Netlify Function. Dynamic parameters in the data route are already converted correctly, but until now, this was not true for catch-all routes. This commit fixes that by converting data routes with catch-all into the Netlify-compatible redirect using the * symbol. For example, /_next/data/%BUILD_ID%/path/to/page/:...slug.json becomes /_next/data/%BUILD_ID%/path/to/page/*
1 parent 9bf3e9f commit 0412b45

File tree

7 files changed

+204
-5
lines changed

7 files changed

+204
-5
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
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+
This page uses getStaticProps() to pre-fetch a TV show.
17+
</p>
18+
19+
<hr/>
20+
21+
<h1>Show #{show.id}</h1>
22+
<p>
23+
{show.name}
24+
</p>
25+
26+
<hr/>
27+
28+
<Link href="/">
29+
<a>Go back home</a>
30+
</Link>
31+
</div>
32+
)
33+
}
34+
35+
export async function getStaticPaths() {
36+
// Set the paths we want to pre-render
37+
const paths = [
38+
{ params: { slug: ['my', 'path', '1'] } },
39+
{ params: { slug: ['my', 'path', '2'] } }
40+
]
41+
42+
// We'll pre-render these paths at build time.
43+
// { fallback: true } means other routes will be rendered at runtime.
44+
return { paths, fallback: true }
45+
}
46+
47+
48+
export async function getStaticProps({ params }) {
49+
// The ID to render
50+
const { slug } = params
51+
const id = slug[slug.length-1]
52+
53+
const res = await fetch(`https://api.tvmaze.com/shows/${id}`);
54+
const data = await res.json();
55+
56+
return {
57+
props: {
58+
show: data
59+
}
60+
}
61+
}
62+
63+
export default Show

cypress/fixtures/pages/index.js

+15
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,21 @@ const Index = ({ shows }) => (
125125
<a>getStaticProps/withFallback/75 (dynamic route, not pre-rendered)</a>
126126
</Link>
127127
</li>
128+
<li>
129+
<Link href="/getStaticProps/withFallback/[...slug]" as="/getStaticProps/withFallback/my/path/1">
130+
<a>getStaticProps/withFallback/my/path/1 (catch-all route)</a>
131+
</Link>
132+
</li>
133+
<li>
134+
<Link href="/getStaticProps/withFallback/[...slug]" as="/getStaticProps/withFallback/my/path/2">
135+
<a>getStaticProps/withFallback/my/path/2 (catch-all route)</a>
136+
</Link>
137+
</li>
138+
<li>
139+
<Link href="/getStaticProps/withFallback/[...slug]" as="/getStaticProps/withFallback/my/undefined/path/75">
140+
<a>getStaticProps/withFallback/my/undefined/path/75 (catch-all route, not pre-rendered)</a>
141+
</Link>
142+
</li>
128143
</ul>
129144

130145
<h1>5. getServerSideProps? Yes!</h1>

cypress/integration/default_spec.js

+47
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,53 @@ describe('getStaticProps', () => {
307307
})
308308
})
309309
})
310+
311+
context('with catch-all route', () => {
312+
context('with fallback', () => {
313+
it('loads pre-rendered shows 1 and 2', () => {
314+
cy.visit('/getStaticProps/withFallback/my/path/1')
315+
cy.get('h1').should('contain', 'Show #1')
316+
cy.get('p').should('contain', 'Under the Dome')
317+
318+
cy.visit('/getStaticProps/withFallback/my/path/2')
319+
cy.get('h1').should('contain', 'Show #2')
320+
cy.get('p').should('contain', 'Person of Interest')
321+
})
322+
323+
it('loads non-pre-rendered TV show', () => {
324+
cy.visit('/getStaticProps/withFallback/undefined/catch/all/path/75')
325+
326+
cy.get('h1').should('contain', 'Show #75')
327+
cy.get('p').should('contain', 'The Mindy Project')
328+
})
329+
330+
it('loads page props from data .json file when navigating to it', () => {
331+
cy.visit('/')
332+
cy.window().then(w => w.noReload = true)
333+
334+
// Navigate to page and test that no reload is performed
335+
// See: https://glebbahmutov.com/blog/detect-page-reload/
336+
cy.contains('getStaticProps/withFallback/my/path/1').click()
337+
338+
cy.get('h1').should('contain', 'Show #1')
339+
cy.get('p').should('contain', 'Under the Dome')
340+
341+
cy.contains('Go back home').click()
342+
cy.contains('getStaticProps/withFallback/my/path/2').click()
343+
344+
cy.get('h1').should('contain', 'Show #2')
345+
cy.get('p').should('contain', 'Person of Interest')
346+
347+
cy.contains('Go back home').click()
348+
cy.contains('getStaticProps/withFallback/my/undefined/path/75').click()
349+
350+
cy.get('h1').should('contain', 'Show #75')
351+
cy.get('p').should('contain', 'The Mindy Project')
352+
353+
cy.window().should('have.property', 'noReload', true)
354+
})
355+
})
356+
})
310357
})
311358

312359
describe('API endpoint', () => {

lib/getNetlifyRoute.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
// also handles catch all routes /[...param]/ -> /:*
1010
module.exports = dynamicRoute => {
1111
// replace any catch all group first
12-
const expressified = dynamicRoute.replace(/\[\.\.\.(.*)]$/, "*");
12+
const expressified = dynamicRoute.replace(/\[\.\.\.(.*)](.json)?$/, "*");
1313

1414
// now replace other dynamic route groups
1515
return expressified.replace(/\[(.*?)]/g, ":$1");

tests/__snapshots__/defaults.test.js.snap

+5-1
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@ exports[`Routing creates Netlify redirects 1`] = `
99
/ /.netlify/functions/next_index 200
1010
/static /static.html 200
1111
/404 /404.html 200
12+
/getStaticProps/static /getStaticProps/static.html 200
1213
/getStaticProps/1 /getStaticProps/1.html 200
1314
/getStaticProps/2 /getStaticProps/2.html 200
14-
/getStaticProps/static /getStaticProps/static.html 200
15+
/getStaticProps/withFallback/my/path/1 /getStaticProps/withFallback/my/path/1.html 200
16+
/getStaticProps/withFallback/my/path/2 /getStaticProps/withFallback/my/path/2.html 200
1517
/getStaticProps/withFallback/3 /getStaticProps/withFallback/3.html 200
1618
/getStaticProps/withFallback/4 /getStaticProps/withFallback/4.html 200
1719
/api/shows/:id /.netlify/functions/next_api_shows_id 200
@@ -20,6 +22,8 @@ exports[`Routing creates Netlify redirects 1`] = `
2022
/_next/data/%BUILD_ID%/getServerSideProps/:id.json /.netlify/functions/next_getServerSideProps_id 200
2123
/getStaticProps/withFallback/:id /.netlify/functions/next_getStaticProps_withFallback_id 200
2224
/_next/data/%BUILD_ID%/getStaticProps/withFallback/:id.json /.netlify/functions/next_getStaticProps_withFallback_id 200
25+
/getStaticProps/withFallback/* /.netlify/functions/next_getStaticProps_withFallback_slug 200
26+
/_next/data/%BUILD_ID%/getStaticProps/withFallback/* /.netlify/functions/next_getStaticProps_withFallback_slug 200
2327
/shows/:id /.netlify/functions/next_shows_id 200
2428
/shows/* /.netlify/functions/next_shows_params 200
2529
/static/:id /static/[id].html 200"

tests/defaults.test.js

+10-3
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ describe('SSG Pages with getStaticProps', () => {
104104
expect(existsSync(join(OUTPUT_PATH, "getStaticProps", "2.html"))).toBe(true)
105105
expect(existsSync(join(OUTPUT_PATH, "getStaticProps", "withFallback", "3.html"))).toBe(true)
106106
expect(existsSync(join(OUTPUT_PATH, "getStaticProps", "withFallback", "4.html"))).toBe(true)
107+
expect(existsSync(join(OUTPUT_PATH, "getStaticProps", "withFallback", "my", "path", "1.html"))).toBe(true)
108+
expect(existsSync(join(OUTPUT_PATH, "getStaticProps", "withFallback", "my", "path", "2.html"))).toBe(true)
107109
})
108110

109111
test('creates data .json file in /_next/data/ directory', () => {
@@ -117,11 +119,16 @@ describe('SSG Pages with getStaticProps', () => {
117119
expect(existsSync(join(dataDir, "getStaticProps", "2.json"))).toBe(true)
118120
expect(existsSync(join(dataDir, "getStaticProps", "withFallback", "3.json"))).toBe(true)
119121
expect(existsSync(join(dataDir, "getStaticProps", "withFallback", "4.json"))).toBe(true)
122+
expect(existsSync(join(dataDir, "getStaticProps", "withFallback", "my", "path", "1.json"))).toBe(true)
123+
expect(existsSync(join(dataDir, "getStaticProps", "withFallback", "my", "path", "2.json"))).toBe(true)
120124
})
121125

122-
test('creates Netlify Function for pages with fallback', () => {
123-
const functionPath = "next_getStaticProps_withFallback_id/next_getStaticProps_withFallback_id.js"
124-
expect(existsSync(join(PROJECT_PATH, "out_functions", functionPath))).toBe(true)
126+
test('creates Netlify Functions for pages with fallback', () => {
127+
const functionPath1 = "next_getStaticProps_withFallback_id/next_getStaticProps_withFallback_id.js"
128+
expect(existsSync(join(PROJECT_PATH, "out_functions", functionPath1))).toBe(true)
129+
130+
const functionPath2 = "next_getStaticProps_withFallback_slug/next_getStaticProps_withFallback_slug.js"
131+
expect(existsSync(join(PROJECT_PATH, "out_functions", functionPath2))).toBe(true)
125132
})
126133
})
127134

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
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+
This page uses getStaticProps() to pre-fetch a TV show.
17+
</p>
18+
19+
<hr/>
20+
21+
<h1>Show #{show.id}</h1>
22+
<p>
23+
{show.name}
24+
</p>
25+
26+
<hr/>
27+
28+
<Link href="/">
29+
<a>Go back home</a>
30+
</Link>
31+
</div>
32+
)
33+
}
34+
35+
export async function getStaticPaths() {
36+
// Set the paths we want to pre-render
37+
const paths = [
38+
{ params: { slug: ['my', 'path', '1'] } },
39+
{ params: { slug: ['my', 'path', '2'] } }
40+
]
41+
42+
// We'll pre-render these paths at build time.
43+
// { fallback: true } means other routes will be rendered at runtime.
44+
return { paths, fallback: true }
45+
}
46+
47+
48+
export async function getStaticProps({ params }) {
49+
// The ID to render
50+
const { slug } = params
51+
const id = slug[slug.length-1]
52+
53+
const res = await fetch(`https://api.tvmaze.com/shows/${id}`);
54+
const data = await res.json();
55+
56+
return {
57+
props: {
58+
show: data
59+
}
60+
}
61+
}
62+
63+
export default Show

0 commit comments

Comments
 (0)