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

Commit 403fbbd

Browse files
committed
add support for preview mode of pre-rendered pages using getStaticProps
1 parent a8b5637 commit 403fbbd

16 files changed

+275
-37
lines changed

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,9 @@ jspm_packages
3939

4040
# OS
4141
.DS_Store
42+
43+
# Local Netlify folder
44+
.netlify
45+
46+
# Cypress Netlify Site ID
47+
cypress/fixtures/.netlify/state.json
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export default async function preview(req, res) {
2+
const { query } = req;
3+
const { id } = query;
4+
5+
// Enable Preview Mode by setting the cookies
6+
res.setPreviewData({});
7+
8+
// Redirect to the path from the fetched post
9+
// We don't redirect to req.query.slug as that might lead to open redirect vulnerabilities
10+
res.writeHead(307, { Location: `/previewTest/static` });
11+
res.end();
12+
}

cypress/fixtures/pages/index.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,11 @@ const Index = ({ shows }) => (
230230
<a>previewTest/222</a>
231231
</Link>
232232
</li>
233+
<li>
234+
<Link href="/previewTest/static">
235+
<a>previewTest/static</a>
236+
</Link>
237+
</li>
233238
</ul>
234239

235240
<h1>6. Static Pages Stay Static</h1>
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import Link from "next/link";
2+
3+
const StaticTest = ({ number }) => {
4+
return (
5+
<div>
6+
<p>
7+
This page uses getStaticProps() and is SSRed when in preview mode.
8+
<br />
9+
<br />
10+
By default, it shows the TV show by ID (as static HTML).
11+
<br />
12+
But when in preview mode, it shows person by ID instead (SSRed).
13+
</p>
14+
15+
<hr />
16+
17+
<h1>Number: {number}</h1>
18+
19+
<Link href="/">
20+
<a>Go back home</a>
21+
</Link>
22+
</div>
23+
);
24+
};
25+
26+
export const getStaticProps = async ({ preview }) => {
27+
let number;
28+
29+
// In preview mode, use odd number
30+
if (preview) {
31+
number = 3;
32+
}
33+
// In normal mode, use even number
34+
else {
35+
number = 4;
36+
}
37+
38+
return {
39+
props: {
40+
number,
41+
},
42+
};
43+
};
44+
45+
export default StaticTest;

cypress/integration/default_spec.js

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -529,12 +529,18 @@ describe("API endpoint", () => {
529529
});
530530

531531
describe("Preview Mode", () => {
532-
it("redirects to preview test page", () => {
532+
it("redirects to preview test page with dynamic route", () => {
533533
cy.visit("/api/enterPreview?id=999");
534534

535535
cy.url().should("include", "/previewTest/999");
536536
});
537537

538+
it("redirects to static preview test page", () => {
539+
cy.visit("/api/enterPreviewStatic");
540+
541+
cy.url().should("include", "/previewTest/static");
542+
});
543+
538544
it("sets cookies on client", () => {
539545
Cypress.Cookies.debug(true);
540546
cy.getCookie("__prerender_bypass").should("not.exist");
@@ -546,7 +552,18 @@ describe("Preview Mode", () => {
546552
cy.getCookie("__next_preview_data").should("not.be", null);
547553
});
548554

549-
it("renders page in preview mode", () => {
555+
it("sets cookies on client with static redirect", () => {
556+
Cypress.Cookies.debug(true);
557+
cy.getCookie("__prerender_bypass").should("not.exist");
558+
cy.getCookie("__next_preview_data").should("not.exist");
559+
560+
cy.visit("/api/enterPreviewStatic");
561+
562+
cy.getCookie("__prerender_bypass").should("not.be", null);
563+
cy.getCookie("__next_preview_data").should("not.be", null);
564+
});
565+
566+
it("renders serverSideProps page in preview mode", () => {
550567
cy.visit("/api/enterPreview?id=999");
551568

552569
if (Cypress.env("DEPLOY") === "local") {
@@ -557,7 +574,15 @@ describe("Preview Mode", () => {
557574
cy.get("p").should("contain", "Sebastian Lacause");
558575
});
559576

560-
it("can move in and out of preview mode", () => {
577+
it("renders staticProps page in preview mode", () => {
578+
// cypress local (aka netlify dev) doesn't support cookie-based redirects
579+
if (Cypress.env("DEPLOY") !== "local") {
580+
cy.visit("/api/enterPreviewStatic");
581+
cy.get("h1").should("contain", "Number: 3");
582+
}
583+
});
584+
585+
it("can move in and out of preview mode for SSRed page", () => {
561586
cy.visit("/api/enterPreview?id=999");
562587

563588
if (Cypress.env("DEPLOY") === "local") {
@@ -582,6 +607,45 @@ describe("Preview Mode", () => {
582607
cy.get("h1").should("contain", "Show #222");
583608
cy.get("p").should("contain", "Happyland");
584609
});
610+
611+
it("can move in and out of preview mode for static page", () => {
612+
if (Cypress.env("DEPLOY") !== "local") {
613+
cy.visit("/api/enterPreviewStatic");
614+
cy.window().then((w) => (w.noReload = true));
615+
616+
cy.get("h1").should("contain", "Number: 3");
617+
618+
cy.contains("Go back home").click();
619+
620+
// Verify that we're still in preview mode
621+
cy.contains("previewTest/static").click();
622+
cy.get("h1").should("contain", "Number: 3");
623+
624+
cy.window().should("have.property", "noReload", true);
625+
626+
// Exit preview mode
627+
cy.visit("/api/exitPreview");
628+
629+
// TO-DO: test if this is the static html?
630+
// Verify that we're no longer in preview mode
631+
cy.contains("previewTest/static").click();
632+
cy.get("h1").should("contain", "Number: 4");
633+
}
634+
});
635+
636+
it("hits the prerendered html out of preview mode and netlify function in preview mode", () => {
637+
if (Cypress.env("DEPLOY") !== "local") {
638+
cy.request("/previewTest/static").then((response) => {
639+
expect(response.headers["cache-control"]).to.include("public");
640+
});
641+
642+
cy.visit("/api/enterPreviewStatic");
643+
644+
cy.request("/previewTest/static").then((response) => {
645+
expect(response.headers["cache-control"]).to.include("private");
646+
});
647+
}
648+
});
585649
});
586650

587651
describe("pre-rendered HTML pages", () => {
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
const getPrerenderManifest = require("./getPrerenderManifest");
2+
3+
const { dynamicRoutes } = getPrerenderManifest();
4+
5+
const isSourceRouteWithFallback = (srcRoute) => {
6+
return Object.entries(dynamicRoutes).some(
7+
([route, { fallback }]) => route === srcRoute && fallback !== false
8+
);
9+
};
10+
11+
module.exports = isSourceRouteWithFallback;
Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const { copySync } = require("fs-extra");
1+
const { copySync, existsSync } = require("fs-extra");
22
const { join } = require("path");
33
const {
44
NEXT_DIST_DIR,
@@ -14,24 +14,21 @@ const setupNetlifyFunctionForPage = (filePath) => {
1414
const functionDirectory = join(NETLIFY_FUNCTIONS_PATH, functionName);
1515

1616
// Copy function template
17-
copySync(
18-
FUNCTION_TEMPLATE_PATH,
19-
join(functionDirectory, `${functionName}.js`),
20-
{
21-
overwrite: false,
22-
errorOnExist: true,
23-
}
17+
const functionTemplateCopyPath = join(
18+
functionDirectory,
19+
`${functionName}.js`
2420
);
21+
copySync(FUNCTION_TEMPLATE_PATH, functionTemplateCopyPath, {
22+
overwrite: false,
23+
errorOnExist: true,
24+
});
2525

2626
// Copy page
27-
copySync(
28-
join(NEXT_DIST_DIR, "serverless", filePath),
29-
join(functionDirectory, "nextJsPage.js"),
30-
{
31-
overwrite: false,
32-
errorOnExist: true,
33-
}
34-
);
27+
const nextPageCopyPath = join(functionDirectory, "nextJsPage.js");
28+
copySync(join(NEXT_DIST_DIR, "serverless", filePath), nextPageCopyPath, {
29+
overwrite: false,
30+
errorOnExist: true,
31+
});
3532
};
3633

3734
module.exports = setupNetlifyFunctionForPage;

lib/pages/getStaticProps/pages.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,16 @@ const pages = [];
66
// Get pages using getStaticProps
77
const { routes } = getPrerenderManifest();
88

9-
// Parse pages
9+
// Parse static pages
1010
Object.entries(routes).forEach(
11-
([route, { dataRoute, initialRevalidateSeconds }]) => {
11+
([route, { dataRoute, initialRevalidateSeconds, srcRoute }]) => {
1212
// Ignore pages with revalidate, these will need to be SSRed
1313
if (initialRevalidateSeconds) return;
1414

1515
pages.push({
1616
route,
1717
dataRoute,
18+
srcRoute,
1819
});
1920
}
2021
);

lib/pages/getStaticProps/redirects.js

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,36 @@
1+
const { join } = require("path");
2+
const getFilePathForRoute = require("../../helpers/getFilePathForRoute");
3+
const getNetlifyFunctionName = require("../../helpers/getNetlifyFunctionName");
14
const pages = require("./pages");
25

3-
// Pages with getStaticProps do not need redirects, unless they are using
4-
// fallback: true or a revalidation interval. Both are handled by other files.
6+
// Pages with getStaticProps (without fallback or revalidation) only need
7+
// redirects for handling preview mode.
58
const redirects = [];
69

10+
pages.forEach(({ route, dataRoute, srcRoute }) => {
11+
const relativePath = getFilePathForRoute(srcRoute || route, "js");
12+
const filePath = join("pages", relativePath);
13+
const functionName = getNetlifyFunctionName(filePath);
14+
15+
const conditions = ["Cookie=__prerender_bypass,__next_preview_data"];
16+
const target = `/.netlify/functions/${functionName}`;
17+
18+
// Add one redirect for the page, but only when the NextJS
19+
// preview mode cookies are present
20+
redirects.push({
21+
route,
22+
target,
23+
force: true,
24+
conditions,
25+
});
26+
27+
// Add one redirect for the data route, same conditions
28+
redirects.push({
29+
route: dataRoute,
30+
target,
31+
force: true,
32+
conditions,
33+
});
34+
});
35+
736
module.exports = redirects;

lib/pages/getStaticProps/setup.js

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
const { join } = require("path");
12
const { logTitle, logItem } = require("../../helpers/logger");
23
const { NETLIFY_PUBLISH_PATH } = require("../../config");
34
const getFilePathForRoute = require("../../helpers/getFilePathForRoute");
5+
const isSourceRouteWithFallback = require("../../helpers/isSourceRouteWithFallback");
46
const setupStaticFileForPage = require("../../helpers/setupStaticFileForPage");
7+
const setupNetlifyFunctionForPage = require("../../helpers/setupNetlifyFunctionForPage");
58
const pages = require("./pages");
69

710
// Copy pre-rendered SSG pages
@@ -11,7 +14,11 @@ const setup = () => {
1114
NETLIFY_PUBLISH_PATH
1215
);
1316

14-
pages.forEach(({ route, dataRoute }) => {
17+
// Keep track of the functions that have been set up, so that we do not set up
18+
// a function for the same file path twice
19+
const filePathsDone = [];
20+
21+
pages.forEach(({ route, dataRoute, srcRoute }) => {
1522
logItem(route);
1623

1724
// Copy pre-rendered HTML page
@@ -21,6 +28,19 @@ const setup = () => {
2128
// Copy page's JSON data
2229
const jsonPath = getFilePathForRoute(route, "json");
2330
setupStaticFileForPage(jsonPath, dataRoute);
31+
32+
// // Set up the Netlify function (this is ONLY for preview mode)
33+
const relativePath = getFilePathForRoute(srcRoute || route, "js");
34+
const filePath = join("pages", relativePath);
35+
36+
// Skip if we have already set up a function for this file
37+
// or if the source route has fallback: true
38+
if (filePathsDone.includes(filePath) || isSourceRouteWithFallback(srcRoute))
39+
return;
40+
41+
logItem(filePath);
42+
setupNetlifyFunctionForPage(filePath);
43+
filePathsDone.push(filePath);
2444
});
2545
};
2646

0 commit comments

Comments
 (0)