Skip to content
This repository was archived by the owner on May 10, 2021. It is now read-only.
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,9 @@ jspm_packages

# OS
.DS_Store

# Local Netlify folder
.netlify

# Cypress Netlify Site ID
cypress/fixtures/.netlify/state.json
5 changes: 0 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@
- [Custom Netlify Redirects](#custom-netlify-redirects)
- [Custom Netlify Functions](#custom-netlify-functions)
- [Caveats](#caveats)
- [Preview Mode](#preview-mode)
- [Fallbacks for Pages with `getStaticPaths`](#fallbacks-for-pages-with-getstaticpaths)
- [Credits](#credits)
- [Showcase](#showcase)
Expand Down Expand Up @@ -175,10 +174,6 @@ SSR pages and API endpoints. It is currently not possible to create custom Netli

## Caveats

### Preview Mode

[Next.js Preview Mode](https://nextjs.org/docs/advanced-features/preview-mode) does not work on pages that are pre-rendered (pages with `getStaticProps`). Netlify currently does not support cookie-based redirects, which are needed for supporting preview mode on pre-rendered pages. Preview mode works correctly on any server-side-rendered pages (pages with `getInitialProps` or `getServerSideProps`). See: [Issue #10](https://github.com/netlify/next-on-netlify/issues/10)

### Fallbacks for Pages with `getStaticPaths`

[Fallback pages](https://nextjs.org/docs/basic-features/data-fetching#fallback-true) behave differently with `next-on-netlify` than they do with Next.js. On Next.js, when navigating to a path that is not defined in `getStaticPaths`, it first displays the fallback page. Next.js then generates the HTML in the background and caches it for future requests.
Expand Down
12 changes: 12 additions & 0 deletions cypress/fixtures/pages/api/enterPreviewStatic.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export default async function preview(req, res) {
const { query } = req;
const { id } = query;

// Enable Preview Mode by setting the cookies
res.setPreviewData({});

// Redirect to the path from the fetched post
// We don't redirect to req.query.slug as that might lead to open redirect vulnerabilities
res.writeHead(307, { Location: `/previewTest/static` });
res.end();
}
5 changes: 5 additions & 0 deletions cypress/fixtures/pages/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,11 @@ const Index = ({ shows }) => (
<a>previewTest/222</a>
</Link>
</li>
<li>
<Link href="/previewTest/static">
<a>previewTest/static</a>
</Link>
</li>
</ul>

<h1>6. Static Pages Stay Static</h1>
Expand Down
45 changes: 45 additions & 0 deletions cypress/fixtures/pages/previewTest/static.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import Link from "next/link";

const StaticTest = ({ number }) => {
return (
<div>
<p>
This page uses getStaticProps() and is SSRed when in preview mode.
<br />
<br />
By default, it shows the TV show by ID (as static HTML).
<br />
But when in preview mode, it shows person by ID instead (SSRed).
</p>

<hr />

<h1>Number: {number}</h1>

<Link href="/">
<a>Go back home</a>
</Link>
</div>
);
};

export const getStaticProps = async ({ preview }) => {
let number;

// In preview mode, use odd number
if (preview) {
number = 3;
}
// In normal mode, use even number
else {
number = 4;
}

return {
props: {
number,
},
};
};

export default StaticTest;
70 changes: 67 additions & 3 deletions cypress/integration/default_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -529,12 +529,18 @@ describe("API endpoint", () => {
});

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

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

it("redirects to static preview test page", () => {
cy.visit("/api/enterPreviewStatic");

cy.url().should("include", "/previewTest/static");
});

it("sets cookies on client", () => {
Cypress.Cookies.debug(true);
cy.getCookie("__prerender_bypass").should("not.exist");
Expand All @@ -546,7 +552,18 @@ describe("Preview Mode", () => {
cy.getCookie("__next_preview_data").should("not.be", null);
});

it("renders page in preview mode", () => {
it("sets cookies on client with static redirect", () => {
Cypress.Cookies.debug(true);
cy.getCookie("__prerender_bypass").should("not.exist");
cy.getCookie("__next_preview_data").should("not.exist");

cy.visit("/api/enterPreviewStatic");

cy.getCookie("__prerender_bypass").should("not.be", null);
cy.getCookie("__next_preview_data").should("not.be", null);
});

it("renders serverSideProps page in preview mode", () => {
cy.visit("/api/enterPreview?id=999");

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

it("can move in and out of preview mode", () => {
it("renders staticProps page in preview mode", () => {
// cypress local (aka netlify dev) doesn't support cookie-based redirects
if (Cypress.env("DEPLOY") !== "local") {
cy.visit("/api/enterPreviewStatic");
cy.get("h1").should("contain", "Number: 3");
}
});

it("can move in and out of preview mode for SSRed page", () => {
cy.visit("/api/enterPreview?id=999");

if (Cypress.env("DEPLOY") === "local") {
Expand All @@ -582,6 +607,45 @@ describe("Preview Mode", () => {
cy.get("h1").should("contain", "Show #222");
cy.get("p").should("contain", "Happyland");
});

it("can move in and out of preview mode for static page", () => {
if (Cypress.env("DEPLOY") !== "local") {
cy.visit("/api/enterPreviewStatic");
cy.window().then((w) => (w.noReload = true));

cy.get("h1").should("contain", "Number: 3");

cy.contains("Go back home").click();

// Verify that we're still in preview mode
cy.contains("previewTest/static").click();
cy.get("h1").should("contain", "Number: 3");

cy.window().should("have.property", "noReload", true);

// Exit preview mode
cy.visit("/api/exitPreview");

// TO-DO: test if this is the static html?
// Verify that we're no longer in preview mode
cy.contains("previewTest/static").click();
cy.get("h1").should("contain", "Number: 4");
}
});

it("hits the prerendered html out of preview mode and netlify function in preview mode", () => {
if (Cypress.env("DEPLOY") !== "local") {
cy.request("/previewTest/static").then((response) => {
expect(response.headers["cache-control"]).to.include("public");
});

cy.visit("/api/enterPreviewStatic");

cy.request("/previewTest/static").then((response) => {
expect(response.headers["cache-control"]).to.include("private");
});
}
});
});

describe("pre-rendered HTML pages", () => {
Expand Down
11 changes: 11 additions & 0 deletions lib/helpers/isSourceRouteWithFallback.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const getPrerenderManifest = require("./getPrerenderManifest");

const { dynamicRoutes } = getPrerenderManifest();

const isSourceRouteWithFallback = (srcRoute) => {
return Object.entries(dynamicRoutes).some(
([route, { fallback }]) => route === srcRoute && fallback !== false
);
};

module.exports = isSourceRouteWithFallback;
29 changes: 13 additions & 16 deletions lib/helpers/setupNetlifyFunctionForPage.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { copySync } = require("fs-extra");
const { copySync, existsSync } = require("fs-extra");
const { join } = require("path");
const {
NEXT_DIST_DIR,
Expand All @@ -14,24 +14,21 @@ const setupNetlifyFunctionForPage = (filePath) => {
const functionDirectory = join(NETLIFY_FUNCTIONS_PATH, functionName);

// Copy function template
copySync(
FUNCTION_TEMPLATE_PATH,
join(functionDirectory, `${functionName}.js`),
{
overwrite: false,
errorOnExist: true,
}
const functionTemplateCopyPath = join(
functionDirectory,
`${functionName}.js`
);
copySync(FUNCTION_TEMPLATE_PATH, functionTemplateCopyPath, {
overwrite: false,
errorOnExist: true,
});

// Copy page
copySync(
join(NEXT_DIST_DIR, "serverless", filePath),
join(functionDirectory, "nextJsPage.js"),
{
overwrite: false,
errorOnExist: true,
}
);
const nextPageCopyPath = join(functionDirectory, "nextJsPage.js");
copySync(join(NEXT_DIST_DIR, "serverless", filePath), nextPageCopyPath, {
overwrite: false,
errorOnExist: true,
});
};

module.exports = setupNetlifyFunctionForPage;
5 changes: 3 additions & 2 deletions lib/pages/getStaticProps/pages.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,16 @@ const pages = [];
// Get pages using getStaticProps
const { routes } = getPrerenderManifest();

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

pages.push({
route,
dataRoute,
srcRoute,
});
}
);
Expand Down
33 changes: 31 additions & 2 deletions lib/pages/getStaticProps/redirects.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,36 @@
const { join } = require("path");
const getFilePathForRoute = require("../../helpers/getFilePathForRoute");
const getNetlifyFunctionName = require("../../helpers/getNetlifyFunctionName");
const pages = require("./pages");

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

pages.forEach(({ route, dataRoute, srcRoute }) => {
const relativePath = getFilePathForRoute(srcRoute || route, "js");
const filePath = join("pages", relativePath);
const functionName = getNetlifyFunctionName(filePath);

const conditions = ["Cookie=__prerender_bypass,__next_preview_data"];
const target = `/.netlify/functions/${functionName}`;

// Add one redirect for the page, but only when the NextJS
// preview mode cookies are present
redirects.push({
route,
target,
force: true,
conditions,
});

// Add one redirect for the data route, same conditions
redirects.push({
route: dataRoute,
target,
force: true,
conditions,
});
});

module.exports = redirects;
22 changes: 21 additions & 1 deletion lib/pages/getStaticProps/setup.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
const { join } = require("path");
const { logTitle, logItem } = require("../../helpers/logger");
const { NETLIFY_PUBLISH_PATH } = require("../../config");
const getFilePathForRoute = require("../../helpers/getFilePathForRoute");
const isSourceRouteWithFallback = require("../../helpers/isSourceRouteWithFallback");
const setupStaticFileForPage = require("../../helpers/setupStaticFileForPage");
const setupNetlifyFunctionForPage = require("../../helpers/setupNetlifyFunctionForPage");
const pages = require("./pages");

// Copy pre-rendered SSG pages
Expand All @@ -11,7 +14,11 @@ const setup = () => {
NETLIFY_PUBLISH_PATH
);

pages.forEach(({ route, dataRoute }) => {
// Keep track of the functions that have been set up, so that we do not set up
// a function for the same file path twice
const filePathsDone = [];

pages.forEach(({ route, dataRoute, srcRoute }) => {
logItem(route);

// Copy pre-rendered HTML page
Expand All @@ -21,6 +28,19 @@ const setup = () => {
// Copy page's JSON data
const jsonPath = getFilePathForRoute(route, "json");
setupStaticFileForPage(jsonPath, dataRoute);

// // Set up the Netlify function (this is ONLY for preview mode)
const relativePath = getFilePathForRoute(srcRoute || route, "js");
const filePath = join("pages", relativePath);

// Skip if we have already set up a function for this file
// or if the source route has fallback: true
if (filePathsDone.includes(filePath) || isSourceRouteWithFallback(srcRoute))
return;

logItem(filePath);
setupNetlifyFunctionForPage(filePath);
filePathsDone.push(filePath);
});
};

Expand Down
18 changes: 17 additions & 1 deletion lib/steps/setupRedirects.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,19 @@ const setupRedirects = () => {
// One route may map to multiple Netlify routes: e.g., catch-all pages
// require two Netlify routes in the _redirects file
getNetlifyRoutes(route).map((netlifyRoute) => {
const redirect = `${netlifyRoute} ${nextRedirect.target} 200`;
const {
conditions = [],
force = false,
statusCode = "200",
target,
} = nextRedirect;
const redirectPieces = [
netlifyRoute,
target,
`${statusCode}${force ? "!" : ""}`,
conditions.join(" "),
];
const redirect = redirectPieces.join(" ").trim();
logItem(redirect);
redirects.push(redirect);
});
Expand All @@ -61,6 +73,10 @@ const setupRedirects = () => {
redirects.splice(rootCatchAllIndex, 0, "/_next/* /_next/:splat 200");
}

// Add workaround that was introduced for pre-traffic mesh cookie-based redirects
// TO-DO: remove when internal netlify bug is resolved
if (nextRedirects.length >= 1) redirects.push("/* /:splat 200");

// Write redirects to _redirects file
writeFileSync(join(NETLIFY_PUBLISH_PATH, "_redirects"), redirects.join("\n"));
};
Expand Down
Loading