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

Commit ef45cc5

Browse files
committed
Server-side-render pages that use incremental static regeneration
Ideally, pages with incremental static regeneration (ISR) should be statically pre-rendered and then re-rendered upon request at the provided revalidation interval. This is currently not possible, because we can only either have a static page or have a page that is server-side rendered. As a temporary workaround, we will be SSRing pages with ISR. This means they will be less performant that regular static pages, but at least their content will be fresh. We believe that a focus on the freshness of the content matches the intention of the user more closely than a focus on the page's performance. Discussion: https://github.com/netlify/next-on-netlify/issues/35 Read more about ISR: https://nextjs.org/blog/next-9-5#stable-incremental-static-regeneration
1 parent e3ce87f commit ef45cc5

File tree

12 files changed

+296
-34
lines changed

12 files changed

+296
-34
lines changed

cypress/fixtures/pages/index.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,22 @@ const Index = ({ shows }) => (
139139
</a>
140140
</Link>
141141
</li>
142+
<li>
143+
<Link
144+
href="/getStaticProps/withRevalidate/[id]"
145+
as="/getStaticProps/withRevalidate/3"
146+
>
147+
<a>getStaticProps/withRevalidate/3 (dynamic route)</a>
148+
</Link>
149+
</li>
150+
<li>
151+
<Link
152+
href="/getStaticProps/withRevalidate/[id]"
153+
as="/getStaticProps/withRevalidate/4"
154+
>
155+
<a>getStaticProps/withRevalidate/4 (dynamic route)</a>
156+
</Link>
157+
</li>
142158
<li>
143159
<Link
144160
href="/getStaticProps/withFallback/[...slug]"

cypress/integration/default_spec.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,22 @@ describe("getStaticProps", () => {
269269
cy.get("p").should("contain", "Dancing with the Stars");
270270
cy.window().should("have.property", "noReload", true);
271271
});
272+
273+
context("with revalidate", () => {
274+
it("loads TV show", () => {
275+
cy.visit("/getStaticProps/with-revalidate");
276+
277+
cy.get("h1").should("contain", "Show #71");
278+
cy.get("p").should("contain", "Dancing with the Stars");
279+
});
280+
281+
it("loads TV shows when SSR-ing", () => {
282+
cy.ssr("/getStaticProps/with-revalidate");
283+
284+
cy.get("h1").should("contain", "Show #71");
285+
cy.get("p").should("contain", "Dancing with the Stars");
286+
});
287+
});
272288
});
273289

274290
context("with dynamic route", () => {
@@ -367,6 +383,42 @@ describe("getStaticProps", () => {
367383
cy.window().should("have.property", "noReload", true);
368384
});
369385
});
386+
387+
context("with revalidate", () => {
388+
it("loads TV show", () => {
389+
cy.visit("/getStaticProps/withRevalidate/75");
390+
391+
cy.get("h1").should("contain", "Show #75");
392+
cy.get("p").should("contain", "The Mindy Project");
393+
});
394+
395+
it("loads TV shows when SSR-ing", () => {
396+
cy.ssr("/getStaticProps/withRevalidate/75");
397+
398+
cy.get("h1").should("contain", "Show #75");
399+
cy.get("p").should("contain", "The Mindy Project");
400+
});
401+
402+
it("loads page props from data .json file when navigating to it", () => {
403+
cy.visit("/");
404+
cy.window().then((w) => (w.noReload = true));
405+
406+
// Navigate to page and test that no reload is performed
407+
// See: https://glebbahmutov.com/blog/detect-page-reload/
408+
cy.contains("getStaticProps/withRevalidate/3").click();
409+
410+
cy.get("h1").should("contain", "Show #3");
411+
cy.get("p").should("contain", "Bitten");
412+
413+
cy.contains("Go back home").click();
414+
cy.contains("getStaticProps/withRevalidate/4").click();
415+
416+
cy.get("h1").should("contain", "Show #4");
417+
cy.get("p").should("contain", "Arrow");
418+
419+
cy.window().should("have.property", "noReload", true);
420+
});
421+
});
370422
});
371423

372424
context("with catch-all route", () => {

lib/allNextJsPages.js

Lines changed: 55 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -55,32 +55,20 @@ const getAllPages = () => {
5555
// the JSON data to the Netlify Function.
5656
const dataRoute = dataRoutes.find(({ page }) => page === route);
5757
if (dataRoute)
58-
alternativeRoutes.push(join("/_next/data", buildId, `${route === "/" ? "/index" : route}.json`));
58+
alternativeRoutes.push(
59+
join("/_next/data", buildId, `${route === "/" ? "/index" : route}.json`)
60+
);
5961

6062
pages.push(new Page({ route, type, filePath, alternativeRoutes }));
6163
});
6264

63-
const renderedDynamicSsgPages = {};
64-
6565
// Parse SSG pages
66-
Object.entries(staticSsgPages).forEach(([route, { srcRoute, dataRoute, initialRevalidateSeconds }]) => {
67-
if (initialRevalidateSeconds && initialRevalidateSeconds != false) {
68-
// Use SSR for SSG pages with revalidate
69-
debugger;
70-
if (renderedDynamicSsgPages[srcRoute])
71-
return;
66+
Object.entries(staticSsgPages).forEach(
67+
([route, { dataRoute, initialRevalidateSeconds }]) => {
68+
// Ignore pages with revalidate, these will need to be SSRed and are
69+
// handled a bit later
70+
if (initialRevalidateSeconds) return;
7271

73-
if (srcRoute) {
74-
const dynamicPage = dynamicSsgPages[srcRoute];
75-
if (dynamicPage) {
76-
dataRoute = dynamicPage.dataRoute;
77-
route = srcRoute;
78-
renderedDynamicSsgPages[route] = true;
79-
}
80-
}
81-
const filePath = join("pages", `${route}.js`);
82-
pages.push(new Page({ route, type: "ssr", filePath, alternativeRoutes: [dataRoute] }));
83-
} else {
8472
pages.push(
8573
new Page({
8674
route,
@@ -90,28 +78,67 @@ const getAllPages = () => {
9078
})
9179
);
9280
}
93-
});
81+
);
9482
Object.entries(dynamicSsgPages).forEach(
9583
([route, { dataRoute, fallback }]) => {
9684
// Ignore pages without fallback, these are already handled by the
9785
// static SSG page block above
98-
if (fallback === false || renderedDynamicSsgPages[route]) return;
86+
if (fallback === false) return;
9987

100-
const filePath = join("pages", `${route}.js`);
10188
pages.push(
10289
new Page({
10390
route,
104-
filePath,
91+
filePath: getFilePath(route, ".js"),
10592
type: "ssg-fallback",
10693
alternativeRoutes: [dataRoute],
10794
})
10895
);
10996
}
11097
);
98+
Object.entries(staticSsgPages).forEach(
99+
([route, { dataRoute, srcRoute, initialRevalidateSeconds }]) => {
100+
// Ignore pages without revalidate, these are already handled by the
101+
// static SSG page block above
102+
if (!initialRevalidateSeconds) return;
103+
104+
// If the page has a source route and that source file has already been
105+
// initialized in the block above (dynamicSsgPages), do nothing.
106+
// We do not need to add this route as an alternative route, because the
107+
// dynamic SSG route already covers this static route.
108+
if (srcRoute && pages.find((page) => page.route === srcRoute)) return;
109+
110+
// If the page has a source route and that source file has already been
111+
// initialized in a previous iteration of this forEach loop, add the
112+
// route and dataRoute as alternative routes
113+
const filePath = getFilePath(srcRoute || route, ".js");
114+
const existingPage = pages.find((page) => page.filePath == filePath);
115+
if (existingPage) {
116+
existingPage.alternativeRoutes.push(route);
117+
existingPage.alternativeRoutes.push(dataRoute);
118+
return;
119+
}
120+
121+
// Otherwise, initialize the page
122+
pages.push(
123+
new Page({
124+
route,
125+
filePath,
126+
type: "ssg-revalidate",
127+
alternativeRoutes: [dataRoute],
128+
})
129+
);
130+
}
131+
);
111132

112133
return pages;
113134
};
114135

136+
// Get the file path for a given route with a specific extension:
137+
// /route -> pages/route.js
138+
// If the route is /, the file should be /index.js
139+
const getFilePath = (route, extension) =>
140+
join("pages", route.replace(/^\/$/, "/index") + extension);
141+
115142
// Represent a NextJS page
116143
class Page {
117144
constructor({ route, type, ...otherParams }) {
@@ -135,6 +162,10 @@ class Page {
135162
return this.type === "ssg-fallback";
136163
}
137164

165+
isSsgRevalidate() {
166+
return this.type === "ssg-revalidate";
167+
}
168+
138169
routeFile(ext) {
139170
return `${this.route.replace(/^\/$/, "/index")}${ext}`;
140171
}

lib/setupRedirects.js

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,11 @@ const setupRedirects = () => {
3333
// these static pages.
3434
// See: https://github.com/netlify/next-on-netlify/issues/26
3535
const pagesNeedingRedirect = allNextJsPages.filter(
36-
(page) => isDynamicRoute(page.route) || page.isSsr() || page.isSsgFallback()
36+
(page) =>
37+
isDynamicRoute(page.route) ||
38+
page.isSsr() ||
39+
page.isSsgFallback() ||
40+
page.isSsgRevalidate()
3741
);
3842

3943
// Identify static and dynamically routed pages
@@ -65,18 +69,15 @@ const setupRedirects = () => {
6569
let to;
6670
const from = getNetlifyRoute(route);
6771

68-
// SSR pages
69-
if (page.isSsr()) {
72+
// SSR pages, SSG fallback pages (for non pre-rendered paths), and SSG
73+
// pages with revalidate
74+
if (page.isSsr() || page.isSsgFallback() || page.isSsgRevalidate()) {
7075
to = `/.netlify/functions/${getNetlifyFunctionName(page.filePath)}`;
7176
}
7277
// SSG pages
7378
else if (page.isSsg()) {
7479
to = page.htmlFile;
7580
}
76-
// SSG fallback pages (for non pre-rendered paths)
77-
else if (page.isSsgFallback()) {
78-
to = `/.netlify/functions/${getNetlifyFunctionName(page.filePath)}`;
79-
}
8081
// Pre-rendered HTML pages
8182
else if (page.isHtml()) {
8283
to = `/${path.relative("pages", page.filePath)}`;

lib/setupSsrPages.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@ const setupSsrPages = () => {
1919
NETLIFY_FUNCTIONS_PATH
2020
);
2121

22-
// Get SSR pages and SSG fallback pages (which also need to be rendered
23-
// server-side)
22+
// Get SSR pages and SSG fallback pages and SSG revalidate pages (which also
23+
// need to be rendered server-side)
2424
const ssrPages = allNextJsPages.filter(
25-
(page) => page.isSsr() || page.isSsgFallback()
25+
(page) => page.isSsr() || page.isSsgFallback() || page.isSsgRevalidate()
2626
);
2727

2828
// Create Netlify Function for every page

tests/__snapshots__/defaults.test.js.snap

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ exports[`Routing creates Netlify redirects 1`] = `
66
/getServerSideProps/static /.netlify/functions/next_getServerSideProps_static 200
77
/_next/data/%BUILD_ID%/getServerSideProps/static.json /.netlify/functions/next_getServerSideProps_static 200
88
/ /.netlify/functions/next_index 200
9+
/getStaticProps/with-revalidate /.netlify/functions/next_getStaticProps_withrevalidate 200
10+
/_next/data/%BUILD_ID%/getStaticProps/with-revalidate.json /.netlify/functions/next_getStaticProps_withrevalidate 200
11+
/getStaticProps/withRevalidate/1 /.netlify/functions/next_getStaticProps_withRevalidate_id 200
12+
/_next/data/%BUILD_ID%/getStaticProps/withRevalidate/1.json /.netlify/functions/next_getStaticProps_withRevalidate_id 200
13+
/getStaticProps/withRevalidate/2 /.netlify/functions/next_getStaticProps_withRevalidate_id 200
14+
/_next/data/%BUILD_ID%/getStaticProps/withRevalidate/2.json /.netlify/functions/next_getStaticProps_withRevalidate_id 200
915
/api/shows/:id /.netlify/functions/next_api_shows_id 200
1016
/api/shows/:params/* /.netlify/functions/next_api_shows_params 200
1117
/getServerSideProps/:id /.netlify/functions/next_getServerSideProps_id 200
@@ -14,6 +20,8 @@ exports[`Routing creates Netlify redirects 1`] = `
1420
/_next/data/%BUILD_ID%/getStaticProps/withFallback/:id.json /.netlify/functions/next_getStaticProps_withFallback_id 200
1521
/getStaticProps/withFallback/:slug/* /.netlify/functions/next_getStaticProps_withFallback_slug 200
1622
/_next/data/%BUILD_ID%/getStaticProps/withFallback/:slug/* /.netlify/functions/next_getStaticProps_withFallback_slug 200
23+
/getStaticProps/withRevalidate/withFallback/:id /.netlify/functions/next_getStaticProps_withRevalidate_withFallback_id 200
24+
/_next/data/%BUILD_ID%/getStaticProps/withRevalidate/withFallback/:id.json /.netlify/functions/next_getStaticProps_withRevalidate_withFallback_id 200
1725
/shows/:id /.netlify/functions/next_shows_id 200
1826
/shows/:params/* /.netlify/functions/next_shows_params 200
1927
/static/:id /static/[id].html 200"

tests/defaults.test.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,40 @@ describe("SSG Pages with getStaticProps", () => {
232232
});
233233
});
234234

235+
describe("SSG Pages with getStaticProps and revalidate", () => {
236+
const functionsDir = join(PROJECT_PATH, "out_functions");
237+
238+
test("creates a Netlify Function for each page", () => {
239+
expect(
240+
existsSync(
241+
join(
242+
functionsDir,
243+
"next_getStaticProps_withrevalidate",
244+
"next_getStaticProps_withrevalidate.js"
245+
)
246+
)
247+
).toBe(true);
248+
expect(
249+
existsSync(
250+
join(
251+
functionsDir,
252+
"next_getStaticProps_withRevalidate_id",
253+
"next_getStaticProps_withRevalidate_id.js"
254+
)
255+
)
256+
).toBe(true);
257+
expect(
258+
existsSync(
259+
join(
260+
functionsDir,
261+
"next_getStaticProps_withRevalidate_withFallback_id",
262+
"next_getStaticProps_withRevalidate_withFallback_id.js"
263+
)
264+
)
265+
).toBe(true);
266+
});
267+
});
268+
235269
describe("Static Pages", () => {
236270
test("copies static pages to output directory", () => {
237271
const OUTPUT_PATH = join(PROJECT_PATH, "out_publish");
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import Link from "next/link";
2+
3+
const Show = ({ show }) => (
4+
<div>
5+
<p>This page uses getStaticProps() to pre-fetch a TV show.</p>
6+
7+
<hr />
8+
9+
<h1>Show #{show.id}</h1>
10+
<p>{show.name}</p>
11+
12+
<hr />
13+
14+
<Link href="/">
15+
<a>Go back home</a>
16+
</Link>
17+
</div>
18+
);
19+
20+
export async function getStaticProps(context) {
21+
const res = await fetch(`https://api.tvmaze.com/shows/71`);
22+
const data = await res.json();
23+
24+
return {
25+
props: {
26+
show: data,
27+
},
28+
revalidate: 1,
29+
};
30+
}
31+
32+
export default Show;

0 commit comments

Comments
 (0)