├── cypress ├── fixtures │ ├── .netlify │ │ └── .keep │ ├── public │ │ ├── file.txt │ │ ├── folder │ │ │ └── file.txt │ │ └── subfolder │ │ │ └── file.txt │ ├── next.config.js │ ├── pages │ │ ├── api │ │ │ ├── context.js │ │ │ ├── redirect.js │ │ │ ├── static.js │ │ │ ├── exitPreview.js │ │ │ ├── enterPreview.js │ │ │ ├── enterPreviewStatic.js │ │ │ └── shows │ │ │ │ ├── [id].js │ │ │ │ └── [...params].js │ │ ├── getServerSideProps │ │ │ ├── context.js │ │ │ ├── wait-on-empty-event-loop │ │ │ │ └── [wait].js │ │ │ ├── static.js │ │ │ ├── [id].js │ │ │ └── catch │ │ │ │ └── all │ │ │ │ └── [...slug].js │ │ ├── getStaticProps │ │ │ ├── static.js │ │ │ ├── with-revalidate.js │ │ │ ├── [id].js │ │ │ ├── withRevalidate │ │ │ │ └── [id].js │ │ │ └── withFallback │ │ │ │ ├── [id].js │ │ │ │ └── [...slug].js │ │ ├── static.js │ │ ├── static │ │ │ └── [id].js │ │ ├── previewTest │ │ │ ├── static.js │ │ │ └── [id].js │ │ └── shows │ │ │ ├── [id].js │ │ │ └── [...params].js │ ├── next.config.js-with-optionalCatchAll │ ├── next.config.js-with-i18n │ ├── pages-with-i18n │ │ ├── api │ │ │ ├── redirect.js │ │ │ ├── static.js │ │ │ ├── exitPreview.js │ │ │ ├── enterPreview.js │ │ │ ├── enterPreviewStatic.js │ │ │ └── shows │ │ │ │ ├── [id].js │ │ │ │ └── [...params].js │ │ ├── getStaticProps │ │ │ ├── static.js │ │ │ ├── with-revalidate.js │ │ │ ├── [id].js │ │ │ ├── withRevalidate │ │ │ │ └── [id].js │ │ │ └── withFallback │ │ │ │ ├── [id].js │ │ │ │ └── [...slug].js │ │ ├── static.js │ │ ├── static │ │ │ └── [id].js │ │ ├── getServerSideProps │ │ │ ├── static.js │ │ │ ├── [id].js │ │ │ └── catch │ │ │ │ └── all │ │ │ │ └── [...slug].js │ │ ├── previewTest │ │ │ ├── static.js │ │ │ └── [id].js │ │ └── shows │ │ │ ├── [id].js │ │ │ └── [...params].js │ ├── pages-with-optionalCatchAll-at-root │ │ ├── static.js │ │ ├── [[...slug]].js │ │ ├── home.js │ │ ├── subfolder │ │ │ └── [id].js │ │ └── [bar] │ │ │ └── ssr.js │ ├── package.json │ ├── netlify.toml │ └── pages-with-optionalCatchAll │ │ ├── index.js │ │ └── catch │ │ └── [[...all]].js ├── .gitignore ├── plugins │ ├── copyFixture.js │ ├── clearProject.js │ ├── buildProject.js │ ├── getBaseUrl.js │ ├── clearDeployment.js │ ├── index.js │ └── deployProject.js ├── support │ ├── index.js │ └── commands.js └── integration │ └── optionalCatchAll_spec.js ├── tests ├── fixtures │ ├── my-functions │ │ ├── next_image.js │ │ ├── someTestFunction.js │ │ └── next_shows_id │ │ │ └── next_shows_id.js │ ├── .nonfiletracking │ ├── next.config.js │ ├── image.png │ ├── components │ │ └── Header.js │ ├── next.config.js-est │ ├── _redirects │ ├── pages-simple │ │ └── index.js │ ├── next.config.js-with-distDir.js │ ├── pages-with-prerendered-index │ │ ├── index.js │ │ └── shows │ │ │ └── index.js │ ├── next.config.js-with-i18n.js │ ├── package.json │ ├── pages │ │ ├── api │ │ │ ├── static.js │ │ │ ├── hello-background.js │ │ │ └── shows │ │ │ │ ├── [id].js │ │ │ │ └── [...params].js │ │ ├── getStaticProps │ │ │ ├── static.js │ │ │ ├── with-revalidate.js │ │ │ ├── [id].js │ │ │ ├── withRevalidate │ │ │ │ ├── [id].js │ │ │ │ └── withFallback │ │ │ │ │ └── [id].js │ │ │ ├── withFallback │ │ │ │ ├── [id].js │ │ │ │ └── [...slug].js │ │ │ └── withFallbackBlocking │ │ │ │ └── [id].js │ │ ├── static.js │ │ ├── static │ │ │ └── [id].js │ │ ├── getServerSideProps │ │ │ ├── static.js │ │ │ ├── [id].js │ │ │ └── all │ │ │ │ └── [[...slug]].js │ │ ├── shows │ │ │ ├── [id].js │ │ │ └── [...params].js │ │ └── index.js │ ├── _headers │ ├── pages-with-static-props-index │ │ ├── index.js │ │ └── static │ │ │ └── index.js │ ├── next.config.js-with-function.js │ ├── pages-i18n-ssg-index │ │ └── index.js │ ├── pages-with-optionalCatchAll │ │ ├── page.js │ │ └── [[...all]].js │ ├── pages-with-gssp-index │ │ └── index.js │ └── pages-dynamic-imports │ │ ├── deep │ │ └── import.js │ │ └── index.js ├── .gitignore ├── jest.config.js ├── helpers │ ├── clearCache.js │ ├── nextVersion.js │ └── npmRun.js ├── __snapshots__ │ └── optionalCatchAll.test.js.snap ├── customNextDistDir.test.js ├── nextConfigFunction.test.js ├── customHeaders.test.js ├── customRedirects.test.js ├── dynamicImports.test.js ├── optionalCatchAll.test.js ├── getServerSidePropsIndexPage.test.js ├── i18n-ssg-root-index.test.js ├── preRenderedIndexPages.test.js └── staticIndexPages.test.js ├── next-on-netlify.png ├── assets ├── showcase-bigbinary.png ├── showcase-missionbit.png └── showcase-opinionatedreact.png ├── lib ├── helpers │ ├── isApiRoute.js │ ├── isHtmlFile.js │ ├── removeFileExtension.js │ ├── getNextSrcDir.js │ ├── isDynamicRoute.js │ ├── isFrameworkRoute.js │ ├── getI18n.js │ ├── isRootCatchAllRedirect.js │ ├── getRoutesManifest.js │ ├── getNextDistDir.js │ ├── getFilePathForRoute.js │ ├── isRouteWithFallback.js │ ├── isRouteWithDataRoute.js │ ├── addLocaleRedirects.js │ ├── getNextConfig.js │ ├── getPagesManifest.js │ ├── setupStaticFileForPage.js │ ├── isRouteInPrerenderManifest.js │ ├── addDefaultLocaleRedirect.js │ ├── copyDynamicImportChunks.js │ ├── getDataRouteForRoute.js │ ├── logger.js │ ├── getSortedRedirects.js │ ├── getPrerenderManifest.js │ ├── getNetlifyFunctionName.js │ ├── setupNetlifyFunctionForPage.js │ ├── getNetlifyRoutes.js │ └── handleFileTracking.js ├── constants │ └── regex.js ├── pages │ ├── api │ │ ├── redirects.js │ │ ├── pages.js │ │ └── setup.js │ ├── getStaticPropsWithFallback │ │ ├── pages.js │ │ ├── setup.js │ │ └── redirects.js │ ├── getInitialProps │ │ ├── redirects.js │ │ ├── setup.js │ │ └── pages.js │ ├── getStaticProps │ │ ├── pages.js │ │ ├── setup.js │ │ └── redirects.js │ ├── getServerSideProps │ │ ├── setup.js │ │ ├── redirects.js │ │ └── pages.js │ ├── withoutProps │ │ ├── pages.js │ │ ├── redirects.js │ │ └── setup.js │ └── getStaticPropsWithRevalidate │ │ ├── pages.js │ │ ├── setup.js │ │ └── redirects.js ├── steps │ ├── copyPublicFiles.js │ ├── setupImageFunction.js │ ├── setupPages.js │ ├── copyNextAssets.js │ ├── setupHeaders.js │ ├── prepareFolders.js │ └── setupRedirects.js ├── templates │ ├── imageFunction.js │ ├── netlifyFunction.js │ ├── renderNextPage.js │ ├── createRequestObject.js │ └── createResponseObject.js └── config.js ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── fossa.yml │ └── workflow.yml ├── next-on-netlify.js ├── .gitignore ├── .prettierignore ├── LICENSE ├── MIGRATING.md ├── package.json └── index.js /cypress/fixtures/.netlify/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/my-functions/next_image.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cypress/fixtures/public/file.txt: -------------------------------------------------------------------------------- 1 | a text file 2 | -------------------------------------------------------------------------------- /tests/fixtures/my-functions/someTestFunction.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/my-functions/next_shows_id/next_shows_id.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cypress/fixtures/public/folder/file.txt: -------------------------------------------------------------------------------- 1 | a text file in a folder 2 | -------------------------------------------------------------------------------- /cypress/fixtures/public/subfolder/file.txt: -------------------------------------------------------------------------------- 1 | a text file in a subfolder 2 | -------------------------------------------------------------------------------- /tests/fixtures/.nonfiletracking: -------------------------------------------------------------------------------- 1 | next_shows_id 2 | next_image.js 3 | --- 4 | -------------------------------------------------------------------------------- /tests/fixtures/next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | target: "serverless", 3 | }; 4 | -------------------------------------------------------------------------------- /next-on-netlify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netlify/next-on-netlify/HEAD/next-on-netlify.png -------------------------------------------------------------------------------- /tests/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore builds of NextJS that are created for testing purposes 2 | builds/ 3 | -------------------------------------------------------------------------------- /cypress/fixtures/next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | target: "experimental-serverless-trace", 3 | }; 4 | -------------------------------------------------------------------------------- /tests/fixtures/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netlify/next-on-netlify/HEAD/tests/fixtures/image.png -------------------------------------------------------------------------------- /tests/fixtures/components/Header.js: -------------------------------------------------------------------------------- 1 | export default function Header() { 2 | return

header

; 3 | } 4 | -------------------------------------------------------------------------------- /tests/fixtures/next.config.js-est: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | target: "experimental-serverless-trace", 3 | }; 4 | -------------------------------------------------------------------------------- /assets/showcase-bigbinary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netlify/next-on-netlify/HEAD/assets/showcase-bigbinary.png -------------------------------------------------------------------------------- /assets/showcase-missionbit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netlify/next-on-netlify/HEAD/assets/showcase-missionbit.png -------------------------------------------------------------------------------- /tests/fixtures/_redirects: -------------------------------------------------------------------------------- 1 | # Custom Redirect Rules 2 | https://old.example.com/* https://new.example.com/:splat 301! 3 | -------------------------------------------------------------------------------- /cypress/fixtures/pages/api/context.js: -------------------------------------------------------------------------------- 1 | export default async function context(req, res) { 2 | res.json({ req, res }); 3 | } 4 | -------------------------------------------------------------------------------- /tests/fixtures/pages-simple/index.js: -------------------------------------------------------------------------------- 1 | const Index = () =>

This page is pre-rendered.

; 2 | 3 | export default Index; 4 | -------------------------------------------------------------------------------- /assets/showcase-opinionatedreact.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netlify/next-on-netlify/HEAD/assets/showcase-opinionatedreact.png -------------------------------------------------------------------------------- /tests/fixtures/next.config.js-with-distDir.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | target: "serverless", 3 | distDir: ".myCustomDir", 4 | }; 5 | -------------------------------------------------------------------------------- /tests/fixtures/pages-with-prerendered-index/index.js: -------------------------------------------------------------------------------- 1 | const PreRenderedPage = () =>

This page is pre-rendered.

; 2 | 3 | export default PreRenderedPage; 4 | -------------------------------------------------------------------------------- /tests/fixtures/pages-with-prerendered-index/shows/index.js: -------------------------------------------------------------------------------- 1 | const PreRenderedPage = () =>

This page is pre-rendered.

; 2 | 3 | export default PreRenderedPage; 4 | -------------------------------------------------------------------------------- /cypress/fixtures/next.config.js-with-optionalCatchAll: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | target: 'serverless', 3 | experimental: { 4 | optionalCatchAll: true 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /tests/fixtures/next.config.js-with-i18n.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | target: "serverless", 3 | i18n: { 4 | locales: ["en", "es"], 5 | defaultLocale: "en", 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /cypress/fixtures/pages/api/redirect.js: -------------------------------------------------------------------------------- 1 | export default async function preview(req, res) { 2 | const { query } = req; 3 | const { to } = query; 4 | 5 | res.redirect(`/shows/${to}`); 6 | } 7 | -------------------------------------------------------------------------------- /lib/helpers/isApiRoute.js: -------------------------------------------------------------------------------- 1 | // Return true if the route is an API route 2 | const isApiRoute = (route) => { 3 | return route.startsWith("/api/"); 4 | }; 5 | 6 | module.exports = isApiRoute; 7 | -------------------------------------------------------------------------------- /cypress/fixtures/next.config.js-with-i18n: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | target: "experimental-serverless-trace", 3 | i18n: { 4 | locales: ["en", "fr"], 5 | defaultLocale: "en" 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /lib/helpers/isHtmlFile.js: -------------------------------------------------------------------------------- 1 | // Return true if the file path is an HTML file 2 | const isHtmlFile = (filePath) => { 3 | return filePath.endsWith(".html"); 4 | }; 5 | 6 | module.exports = isHtmlFile; 7 | -------------------------------------------------------------------------------- /tests/fixtures/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-on-netlify-test", 3 | "scripts": { 4 | "next-build": "next build", 5 | "next-on-netlify": "node ../../../next-on-netlify" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tests/fixtures/pages/api/static.js: -------------------------------------------------------------------------------- 1 | export default (req, res) => { 2 | res.setHeader("Content-Type", "application/json"); 3 | res.status(200); 4 | res.json({ message: "hello world :)" }); 5 | }; 6 | -------------------------------------------------------------------------------- /cypress/fixtures/pages-with-i18n/api/redirect.js: -------------------------------------------------------------------------------- 1 | export default async function preview(req, res) { 2 | const { query } = req; 3 | const { to } = query; 4 | 5 | res.redirect(`/shows/${to}`); 6 | } 7 | -------------------------------------------------------------------------------- /lib/helpers/removeFileExtension.js: -------------------------------------------------------------------------------- 1 | // Remove the file extension form the route 2 | const removeFileExtension = (route) => route.replace(/\.[a-zA-Z]+$/, ""); 3 | 4 | module.exports = removeFileExtension; 5 | -------------------------------------------------------------------------------- /tests/fixtures/pages/api/hello-background.js: -------------------------------------------------------------------------------- 1 | export default (req, res) => { 2 | res.setHeader("Content-Type", "application/json"); 3 | res.status(200); 4 | res.json({ message: "hello world :)" }); 5 | }; 6 | -------------------------------------------------------------------------------- /tests/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // Do not scan the ./builds/ folder and the Cypress folder for tests or 3 | // package.json files 4 | modulePathIgnorePatterns: ["./builds/", "/cypress/"], 5 | }; 6 | -------------------------------------------------------------------------------- /lib/helpers/getNextSrcDir.js: -------------------------------------------------------------------------------- 1 | const { join } = require("path"); 2 | 3 | const getNextSrcDirs = () => { 4 | return ["pages", "src", "public", "styles"].map((dir) => join(".", dir)); 5 | }; 6 | 7 | module.exports = getNextSrcDirs; 8 | -------------------------------------------------------------------------------- /cypress/fixtures/pages/api/static.js: -------------------------------------------------------------------------------- 1 | export default (req, res) => { 2 | // We can set custom headers 3 | res.setHeader("My-Custom-Header", "header123"); 4 | 5 | res.status(200); 6 | res.json({ message: "hello world :)" }); 7 | }; 8 | -------------------------------------------------------------------------------- /tests/fixtures/_headers: -------------------------------------------------------------------------------- 1 | /templates/index.html 2 | # headers for that path: 3 | X-Frame-Options: DENY 4 | X-XSS-Protection: 1; mode=block 5 | 6 | /templates/index2.html 7 | # headers for that path: 8 | X-Frame-Options: SAMEORIGIN 9 | -------------------------------------------------------------------------------- /cypress/fixtures/pages-with-optionalCatchAll-at-root/static.js: -------------------------------------------------------------------------------- 1 | const Static = () =>

static page

; 2 | 3 | export async function getStaticProps() { 4 | return { 5 | props: {}, 6 | }; 7 | } 8 | 9 | export default Static; 10 | -------------------------------------------------------------------------------- /lib/helpers/isDynamicRoute.js: -------------------------------------------------------------------------------- 1 | // Return true if the route uses dynamic routing (e.g., [id] or [...slug]) 2 | const isDynamicRoute = (route) => { 3 | return /\/\[[^\/]+?](?=\/|$)/.test(route); 4 | }; 5 | 6 | module.exports = isDynamicRoute; 7 | -------------------------------------------------------------------------------- /cypress/fixtures/pages-with-i18n/api/static.js: -------------------------------------------------------------------------------- 1 | export default (req, res) => { 2 | // We can set custom headers 3 | res.setHeader("My-Custom-Header", "header123"); 4 | 5 | res.status(200); 6 | res.json({ message: "hello world :)" }); 7 | }; 8 | -------------------------------------------------------------------------------- /cypress/fixtures/pages-with-optionalCatchAll-at-root/[[...slug]].js: -------------------------------------------------------------------------------- 1 | const Page = () =>

root-level optional-catch-all

; 2 | 3 | export async function getServerSideProps() { 4 | return { 5 | props: {}, 6 | }; 7 | } 8 | 9 | export default Page; 10 | -------------------------------------------------------------------------------- /cypress/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore Netlify Site ID 2 | fixtures/.netlify/* 3 | !fixtures/.netlify/.keep 4 | 5 | # Ignore builds of NextJS that are created for testing purposes 6 | builds/ 7 | 8 | # Ignore Cypress videos and screenshots 9 | videos/ 10 | screenshots/ 11 | -------------------------------------------------------------------------------- /lib/helpers/isFrameworkRoute.js: -------------------------------------------------------------------------------- 1 | // Return true if the route is one of the framework pages, such as _app or 2 | // _error 3 | const isFrameworkRoute = (route) => { 4 | return ["/_app", "/_document", "/_error"].includes(route); 5 | }; 6 | 7 | module.exports = isFrameworkRoute; 8 | -------------------------------------------------------------------------------- /tests/helpers/clearCache.js: -------------------------------------------------------------------------------- 1 | // Clear the build cache 2 | 3 | const { join } = require("path"); 4 | const { emptyDirSync } = require("fs-extra"); 5 | 6 | const CACHE_PATH = join(__dirname, "..", "builds"); 7 | 8 | emptyDirSync(CACHE_PATH); 9 | console.log("Cleared", CACHE_PATH, "✔"); 10 | -------------------------------------------------------------------------------- /cypress/fixtures/pages/getServerSideProps/context.js: -------------------------------------------------------------------------------- 1 | const Context = ({ context }) =>
{JSON.stringify(context, 2, " ")}
; 2 | 3 | export const getServerSideProps = async (context) => { 4 | return { 5 | props: { 6 | context, 7 | }, 8 | }; 9 | }; 10 | 11 | export default Context; 12 | -------------------------------------------------------------------------------- /lib/helpers/getI18n.js: -------------------------------------------------------------------------------- 1 | // Get the i1i8n details specified in next.config.js, if any 2 | const getNextConfig = require("./getNextConfig"); 3 | 4 | const getI18n = () => { 5 | const nextConfig = getNextConfig(); 6 | 7 | return nextConfig.i18n || { locales: [] }; 8 | }; 9 | 10 | module.exports = getI18n; 11 | -------------------------------------------------------------------------------- /cypress/fixtures/pages/api/exitPreview.js: -------------------------------------------------------------------------------- 1 | export default async function exit(_, res) { 2 | // Exit the current user from "Preview Mode". This function accepts no args. 3 | res.clearPreviewData(); 4 | 5 | // Redirect the user back to the index page. 6 | res.writeHead(307, { Location: "/" }); 7 | res.end(); 8 | } 9 | -------------------------------------------------------------------------------- /tests/helpers/nextVersion.js: -------------------------------------------------------------------------------- 1 | const { join } = require("path"); 2 | const { readJsonSync } = require("fs-extra"); 3 | 4 | const NEXT_PATH = join(__dirname, "..", "..", "node_modules", "next"); 5 | const { version: NEXT_VERSION } = readJsonSync(join(NEXT_PATH, "package.json")); 6 | 7 | module.exports = { NEXT_VERSION }; 8 | -------------------------------------------------------------------------------- /lib/constants/regex.js: -------------------------------------------------------------------------------- 1 | const CATCH_ALL_REGEX = /\/\[\.{3}(.*)\](.json)?$/; 2 | const OPTIONAL_CATCH_ALL_REGEX = /\/\[{2}\.{3}(.*)\]{2}(.json)?$/; 3 | const DYNAMIC_PARAMETER_REGEX = /\/\[(.*?)\]/g; 4 | 5 | module.exports = { 6 | CATCH_ALL_REGEX, 7 | OPTIONAL_CATCH_ALL_REGEX, 8 | DYNAMIC_PARAMETER_REGEX, 9 | }; 10 | -------------------------------------------------------------------------------- /cypress/fixtures/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-on-netlify-test", 3 | "scripts": { 4 | "build": "next build", 5 | "postbuild": "node ../../../next-on-netlify", 6 | "preview": "netlify dev", 7 | "predeploy": "mkdir -p .git", 8 | "deploy": "netlify deploy --json > deployment.json" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /cypress/fixtures/pages-with-i18n/api/exitPreview.js: -------------------------------------------------------------------------------- 1 | export default async function exit(_, res) { 2 | // Exit the current user from "Preview Mode". This function accepts no args. 3 | res.clearPreviewData(); 4 | 5 | // Redirect the user back to the index page. 6 | res.writeHead(307, { Location: "/" }); 7 | res.end(); 8 | } 9 | -------------------------------------------------------------------------------- /lib/helpers/isRootCatchAllRedirect.js: -------------------------------------------------------------------------------- 1 | // Return true if the redirect is a root level catch-all 2 | // (e.g., /[[...slug]] or /[...slug]) 3 | const isRootCatchAllRedirect = (redirect) => 4 | redirect.startsWith("/*") || 5 | (redirect.startsWith("/:") && redirect.includes("/*")); 6 | 7 | module.exports = isRootCatchAllRedirect; 8 | -------------------------------------------------------------------------------- /lib/helpers/getRoutesManifest.js: -------------------------------------------------------------------------------- 1 | const { join } = require("path"); 2 | const { readJSONSync } = require("fs-extra"); 3 | const { NEXT_DIST_DIR } = require("../config"); 4 | 5 | const getRoutesManifest = () => { 6 | return readJSONSync(join(NEXT_DIST_DIR, "routes-manifest.json")); 7 | }; 8 | 9 | module.exports = getRoutesManifest; 10 | -------------------------------------------------------------------------------- /lib/pages/api/redirects.js: -------------------------------------------------------------------------------- 1 | const getNetlifyFunctionName = require("../../helpers/getNetlifyFunctionName"); 2 | const pages = require("./pages"); 3 | 4 | const redirects = pages.map(({ route, filePath }) => ({ 5 | route, 6 | target: `/.netlify/functions/${getNetlifyFunctionName(filePath, true)}`, 7 | })); 8 | 9 | module.exports = redirects; 10 | -------------------------------------------------------------------------------- /lib/helpers/getNextDistDir.js: -------------------------------------------------------------------------------- 1 | // Get the NextJS distDir specified in next.config.js 2 | const { join } = require("path"); 3 | const getNextConfig = require("./getNextConfig"); 4 | 5 | const getNextDistDir = () => { 6 | const nextConfig = getNextConfig(); 7 | 8 | return join(".", nextConfig.distDir); 9 | }; 10 | 11 | module.exports = getNextDistDir; 12 | -------------------------------------------------------------------------------- /lib/helpers/getFilePathForRoute.js: -------------------------------------------------------------------------------- 1 | // Return the file for the given route 2 | const getFilePathForRoute = (route, extension, locale) => { 3 | // Replace / with /index 4 | const path = route.replace(/^\/$/, "/index"); 5 | 6 | if (locale) return `${locale}${path}.${extension}`; 7 | return `${path}.${extension}`; 8 | }; 9 | 10 | module.exports = getFilePathForRoute; 11 | -------------------------------------------------------------------------------- /lib/helpers/isRouteWithFallback.js: -------------------------------------------------------------------------------- 1 | const getPrerenderManifest = require("./getPrerenderManifest"); 2 | 3 | const { dynamicRoutes } = getPrerenderManifest(); 4 | 5 | const isRouteWithFallback = (route) => { 6 | // Fallback "blocking" routes will have fallback: null in manifest 7 | return dynamicRoutes[route] && dynamicRoutes[route].fallback !== false; 8 | }; 9 | 10 | module.exports = isRouteWithFallback; 11 | -------------------------------------------------------------------------------- /cypress/plugins/copyFixture.js: -------------------------------------------------------------------------------- 1 | const { join } = require("path"); 2 | const { copySync } = require("fs-extra"); 3 | 4 | // Copy the fixture files from fixtures/ to the project folder 5 | const copyFixture = ({ project, from, to }, config) => { 6 | copySync( 7 | join(config.fixturesFolder, from), 8 | join(config.buildsFolder, project, to) 9 | ); 10 | 11 | return true; 12 | }; 13 | 14 | module.exports = copyFixture; 15 | -------------------------------------------------------------------------------- /tests/fixtures/pages-with-static-props-index/index.js: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | const Page = ({ now }) => ( 4 |
5 |

Date.now(): {now}

6 | 7 | 8 | Static page 9 | 10 |
11 | ); 12 | 13 | export async function getStaticProps(context) { 14 | return { 15 | props: { 16 | now: Date.now(), 17 | }, 18 | }; 19 | } 20 | 21 | export default Page; 22 | -------------------------------------------------------------------------------- /tests/fixtures/pages-with-static-props-index/static/index.js: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | const Page = ({ now }) => ( 4 |
5 |

Date.now(): {now}

6 | 7 | 8 | Index page 9 | 10 |
11 | ); 12 | 13 | export async function getStaticProps(context) { 14 | return { 15 | props: { 16 | now: Date.now(), 17 | }, 18 | }; 19 | } 20 | 21 | export default Page; 22 | -------------------------------------------------------------------------------- /cypress/plugins/clearProject.js: -------------------------------------------------------------------------------- 1 | const { join } = require("path"); 2 | const { emptyDirSync } = require("fs-extra"); 3 | 4 | // Clear the project 5 | const clearProject = ({ project }, config) => { 6 | emptyDirSync(join(config.buildsFolder, project)); 7 | emptyDirSync(join(config.buildsFolder, project, "pages")); 8 | emptyDirSync(join(config.buildsFolder, project, ".netlify")); 9 | 10 | return true; 11 | }; 12 | 13 | module.exports = clearProject; 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | Please include a repo to reproduce your issue if you can! 12 | 13 | **To Reproduce** 14 | Steps to reproduce: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Versions** 21 | - Next: 22 | - next-on-netlify: 23 | -------------------------------------------------------------------------------- /cypress/fixtures/pages/api/enterPreview.js: -------------------------------------------------------------------------------- 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/${id}` }); 11 | res.end(); 12 | } 13 | -------------------------------------------------------------------------------- /cypress/fixtures/pages-with-i18n/api/enterPreview.js: -------------------------------------------------------------------------------- 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/${id}` }); 11 | res.end(); 12 | } 13 | -------------------------------------------------------------------------------- /cypress/fixtures/pages/api/enterPreviewStatic.js: -------------------------------------------------------------------------------- 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 | } 13 | -------------------------------------------------------------------------------- /cypress/fixtures/netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | command = "npm run build" 3 | functions = "out_functions" 4 | publish = "out_publish" 5 | 6 | [dev] 7 | # We manually set the framework to static, otherwise Netlify automatically 8 | # detects NextJS and redirects do not work. 9 | # Read more: https://github.com/netlify/cli/blob/master/docs/netlify-dev.md#project-detection 10 | framework = "#static" 11 | functions = "out_functions" 12 | publish = "out_publish" 13 | -------------------------------------------------------------------------------- /cypress/fixtures/pages-with-i18n/api/enterPreviewStatic.js: -------------------------------------------------------------------------------- 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 | } 13 | -------------------------------------------------------------------------------- /lib/helpers/isRouteWithDataRoute.js: -------------------------------------------------------------------------------- 1 | const getRoutesManifest = require("./getRoutesManifest"); 2 | 3 | const { dataRoutes } = getRoutesManifest(); 4 | 5 | // Return true if the route has a data route in the routes manifest 6 | const isRouteWithDataRoute = (route) => { 7 | // If no data routes exist, return false 8 | if (dataRoutes == null) return false; 9 | 10 | return dataRoutes.find((dataRoute) => dataRoute.page === route); 11 | }; 12 | 13 | module.exports = isRouteWithDataRoute; 14 | -------------------------------------------------------------------------------- /tests/fixtures/next.config.js-with-function.js: -------------------------------------------------------------------------------- 1 | const { PHASE_PRODUCTION_BUILD } = require("next/constants"); 2 | 3 | module.exports = (phase, { defaultConfig }) => { 4 | // next-on-netlify uses settings from PHASE_PRODUCTION_BUILD 5 | // This is the same phase that is used when running `next build` 6 | if (phase === PHASE_PRODUCTION_BUILD) { 7 | return { 8 | target: "serverless", 9 | distDir: ".myCustomDir", 10 | }; 11 | } 12 | 13 | // Default options 14 | return {}; 15 | }; 16 | -------------------------------------------------------------------------------- /cypress/fixtures/pages-with-optionalCatchAll-at-root/home.js: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | const Home = () => ( 4 |
5 |

NextJS on Netlify

6 | 7 | 19 |
20 | ); 21 | 22 | export default Home; 23 | -------------------------------------------------------------------------------- /cypress/plugins/buildProject.js: -------------------------------------------------------------------------------- 1 | const { join } = require("path"); 2 | const execa = require("execa"); 3 | 4 | // Build the given NextJS project 5 | const buildProject = ({ project }, config) => { 6 | process.stdout.write(`Building project: ${project}...`); 7 | 8 | // Build project 9 | execa.sync("npm", ["run", "build"], { 10 | cwd: join(config.buildsFolder, project), 11 | preferLocal: true, 12 | }); 13 | 14 | console.log(" Done! ✅"); 15 | return true; 16 | }; 17 | 18 | module.exports = buildProject; 19 | -------------------------------------------------------------------------------- /lib/helpers/addLocaleRedirects.js: -------------------------------------------------------------------------------- 1 | const i18n = require("./getI18n")(); 2 | const getDataRouteForRoute = require("./getDataRouteForRoute"); 3 | 4 | const addLocaleRedirects = (redirects) => (route, target) => { 5 | i18n.locales.forEach((locale) => { 6 | redirects.push({ 7 | route: `/${locale}${route === "/" ? "" : route}`, 8 | target, 9 | }); 10 | redirects.push({ 11 | route: getDataRouteForRoute(route, locale), 12 | target, 13 | }); 14 | }); 15 | }; 16 | 17 | module.exports = addLocaleRedirects; 18 | -------------------------------------------------------------------------------- /tests/helpers/npmRun.js: -------------------------------------------------------------------------------- 1 | const execa = require("execa"); 2 | 3 | // Run the given npm command from the given directory 4 | const npmRun = async (command, fromDirectory) => { 5 | // Execute the command 6 | try { 7 | return await execa("npm", ["run", command], { 8 | cwd: fromDirectory, 9 | preferLocal: true, 10 | }); 11 | } catch (error) { 12 | throw new Error( 13 | `An error occurred during "npm run ${command}" in ${fromDirectory}: ${error.message}` 14 | ); 15 | } 16 | }; 17 | 18 | module.exports = npmRun; 19 | -------------------------------------------------------------------------------- /lib/helpers/getNextConfig.js: -------------------------------------------------------------------------------- 1 | // Get next.config.js 2 | const { PHASE_PRODUCTION_BUILD } = require("next/constants"); 3 | const { default: loadConfig } = require("next/dist/next-server/server/config"); 4 | const { resolve } = require("path"); 5 | 6 | const getNextConfig = () => { 7 | // Load next.config.js 8 | // Use same code as https://github.com/vercel/next.js/blob/25488f4a03db30cade4d086ba49cd9a50a2ac02e/packages/next/build/index.ts#L114 9 | return loadConfig(PHASE_PRODUCTION_BUILD, resolve(".")); 10 | }; 11 | 12 | module.exports = getNextConfig; 13 | -------------------------------------------------------------------------------- /cypress/fixtures/pages-with-optionalCatchAll-at-root/subfolder/[id].js: -------------------------------------------------------------------------------- 1 | const Static = () =>

static page in subfolder

; 2 | 3 | export async function getStaticPaths() { 4 | return { 5 | paths: [ 6 | { 7 | params: { 8 | id: "static", 9 | }, 10 | }, 11 | { 12 | params: { 13 | id: "test", 14 | }, 15 | }, 16 | ], 17 | fallback: true, 18 | }; 19 | } 20 | 21 | export async function getStaticProps() { 22 | return { 23 | props: {}, 24 | }; 25 | } 26 | 27 | export default Static; 28 | -------------------------------------------------------------------------------- /lib/pages/getStaticPropsWithFallback/pages.js: -------------------------------------------------------------------------------- 1 | const getPrerenderManifest = require("../../helpers/getPrerenderManifest"); 2 | 3 | // Collect pages 4 | const pages = []; 5 | 6 | // Get pages using getStaticProps 7 | const { dynamicRoutes } = getPrerenderManifest(); 8 | 9 | // Parse pages 10 | Object.entries(dynamicRoutes).forEach(([route, { dataRoute, fallback }]) => { 11 | // Skip pages without fallback 12 | if (fallback === false) return; 13 | 14 | // Add the page 15 | pages.push({ 16 | route, 17 | dataRoute, 18 | }); 19 | }); 20 | 21 | module.exports = pages; 22 | -------------------------------------------------------------------------------- /lib/pages/api/pages.js: -------------------------------------------------------------------------------- 1 | const getPagesManifest = require("../../helpers/getPagesManifest"); 2 | const isApiRoute = require("../../helpers/isApiRoute"); 3 | 4 | // Collect pages 5 | const pages = []; 6 | 7 | // Get HTML and SSR pages and API endpoints from the NextJS pages manifest 8 | const pagesManifest = getPagesManifest(); 9 | 10 | // Parse pages 11 | Object.entries(pagesManifest).forEach(([route, filePath]) => { 12 | // Skip non-API endpoints 13 | if (!isApiRoute(route)) return; 14 | 15 | // Add page 16 | pages.push({ route, filePath }); 17 | }); 18 | 19 | module.exports = pages; 20 | -------------------------------------------------------------------------------- /cypress/fixtures/pages/api/shows/[id].js: -------------------------------------------------------------------------------- 1 | export default async (req, res) => { 2 | // Get the ID of the show 3 | const { query } = req; 4 | const { id } = query; 5 | 6 | // Get the data 7 | const fetchRes = await fetch(`https://api.tvmaze.com/shows/${id}`); 8 | const data = await fetchRes.json(); 9 | 10 | // If show was found, return it 11 | if (fetchRes.status == 200) { 12 | res.status(200); 13 | res.json({ show: data }); 14 | } 15 | // If show was not found, return error 16 | else { 17 | res.status(404); 18 | res.json({ error: "Show not found" }); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /lib/pages/getInitialProps/redirects.js: -------------------------------------------------------------------------------- 1 | const addLocaleRedirects = require("../../helpers/addLocaleRedirects"); 2 | const getNetlifyFunctionName = require("../../helpers/getNetlifyFunctionName"); 3 | const pages = require("./pages"); 4 | 5 | const redirects = []; 6 | 7 | pages.forEach(({ route, filePath }) => { 8 | const functionName = getNetlifyFunctionName(filePath); 9 | const target = `/.netlify/functions/${functionName}`; 10 | 11 | addLocaleRedirects(redirects)(route, target); 12 | 13 | redirects.push({ 14 | route, 15 | target, 16 | }); 17 | }); 18 | 19 | module.exports = redirects; 20 | -------------------------------------------------------------------------------- /cypress/fixtures/pages-with-i18n/api/shows/[id].js: -------------------------------------------------------------------------------- 1 | export default async (req, res) => { 2 | // Get the ID of the show 3 | const { query } = req; 4 | const { id } = query; 5 | 6 | // Get the data 7 | const fetchRes = await fetch(`https://api.tvmaze.com/shows/${id}`); 8 | const data = await fetchRes.json(); 9 | 10 | // If show was found, return it 11 | if (fetchRes.status == 200) { 12 | res.status(200); 13 | res.json({ show: data }); 14 | } 15 | // If show was not found, return error 16 | else { 17 | res.status(404); 18 | res.json({ error: "Show not found" }); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /lib/steps/copyPublicFiles.js: -------------------------------------------------------------------------------- 1 | const { existsSync, copySync } = require("fs-extra"); 2 | const { logTitle } = require("../helpers/logger"); 3 | const { PUBLIC_PATH } = require("../config"); 4 | 5 | // Copy files from public folder to Netlify publish folder 6 | const copyPublicFiles = (publishPath) => { 7 | // Abort if no public/ folder 8 | if (!existsSync(PUBLIC_PATH)) return; 9 | 10 | // Perform copy operation 11 | if (publishPath !== PUBLIC_PATH) { 12 | logTitle("🌍️ Copying", PUBLIC_PATH, "folder to", publishPath); 13 | copySync(PUBLIC_PATH, publishPath); 14 | } 15 | }; 16 | 17 | module.exports = copyPublicFiles; 18 | -------------------------------------------------------------------------------- /cypress/plugins/getBaseUrl.js: -------------------------------------------------------------------------------- 1 | const { join } = require("path"); 2 | const { readJsonSync } = require("fs-extra"); 3 | 4 | // Set baseurl, either localhost:8888 or based on deployed Netlify URL 5 | const getBaseUrl = ({ project }, config) => { 6 | // Local deployment 7 | if (config.env.DEPLOY === "local") { 8 | return "http://localhost:8888/"; 9 | } 10 | // Deployment on Netlify 11 | else if (config.env.DEPLOY == "netlify") { 12 | const { deploy_url } = readJsonSync( 13 | join(config.buildsFolder, project, "deployment.json") 14 | ); 15 | return deploy_url; 16 | } 17 | }; 18 | 19 | module.exports = getBaseUrl; 20 | -------------------------------------------------------------------------------- /lib/steps/setupImageFunction.js: -------------------------------------------------------------------------------- 1 | const { copySync } = require("fs-extra"); 2 | const { join } = require("path"); 3 | const { NEXT_IMAGE_FUNCTION_NAME, TEMPLATES_DIR } = require("../config"); 4 | 5 | // Move our next/image function into the correct functions directory 6 | const setupImageFunction = (functionsPath) => { 7 | const functionName = `${NEXT_IMAGE_FUNCTION_NAME}.js`; 8 | const functionDirectory = join(functionsPath, functionName); 9 | 10 | copySync(join(TEMPLATES_DIR, "imageFunction.js"), functionDirectory, { 11 | overwrite: false, 12 | errorOnExist: true, 13 | }); 14 | }; 15 | 16 | module.exports = setupImageFunction; 17 | -------------------------------------------------------------------------------- /next-on-netlify.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const { program } = require("commander"); 3 | 4 | const nextOnNetlify = require("./index"); 5 | 6 | program.option( 7 | "--max-log-lines [number]", 8 | "lines of build output to show for each section", 9 | 50 10 | ); 11 | 12 | program 13 | .command("watch") 14 | .description("re-runs next-on-netlify on changes") 15 | .action(() => { 16 | nextOnNetlify({ watch: true }); 17 | }); 18 | 19 | program 20 | .command("build", { isDefault: true }) 21 | .description("runs next-on-netlify") 22 | .action(() => { 23 | nextOnNetlify(); 24 | }); 25 | 26 | program.parse(process.argv); 27 | -------------------------------------------------------------------------------- /lib/pages/getStaticProps/pages.js: -------------------------------------------------------------------------------- 1 | const getPrerenderManifest = require("../../helpers/getPrerenderManifest"); 2 | 3 | // Collect pages 4 | const pages = []; 5 | 6 | // Get pages using getStaticProps 7 | const { routes } = getPrerenderManifest(); 8 | 9 | // Parse static pages 10 | Object.entries(routes).forEach( 11 | ([route, { dataRoute, initialRevalidateSeconds, srcRoute }]) => { 12 | // Ignore pages with revalidate, these will need to be SSRed 13 | if (initialRevalidateSeconds) return; 14 | 15 | pages.push({ 16 | route, 17 | dataRoute, 18 | srcRoute, 19 | }); 20 | } 21 | ); 22 | 23 | module.exports = pages; 24 | -------------------------------------------------------------------------------- /cypress/fixtures/pages/getServerSideProps/wait-on-empty-event-loop/[wait].js: -------------------------------------------------------------------------------- 1 | const WaitForEmptyEventLoop = () =>

Successfully rendered page!

; 2 | 3 | export const getServerSideProps = async ({ params, req }) => { 4 | // Set up long-running process 5 | const timeout = setTimeout(() => {}, 100000); 6 | 7 | // Set behavior of whether to wait for empty event loop 8 | const wait = String(params.wait).toLowerCase() === "true"; 9 | const { context: functionContext } = req.netlifyFunctionParams; 10 | functionContext.callbackWaitsForEmptyEventLoop = wait; 11 | 12 | return { 13 | props: {}, 14 | }; 15 | }; 16 | 17 | export default WaitForEmptyEventLoop; 18 | -------------------------------------------------------------------------------- /lib/steps/setupPages.js: -------------------------------------------------------------------------------- 1 | // Set up all our NextJS pages according to the recipes defined in pages/ 2 | const setupPages = ({ functionsPath, publishPath }) => { 3 | require("../pages/api/setup")(functionsPath); 4 | require("../pages/getInitialProps/setup")(functionsPath); 5 | require("../pages/getServerSideProps/setup")(functionsPath); 6 | require("../pages/getStaticProps/setup")({ functionsPath, publishPath }); 7 | require("../pages/getStaticPropsWithFallback/setup")(functionsPath); 8 | require("../pages/getStaticPropsWithRevalidate/setup")(functionsPath); 9 | require("../pages/withoutProps/setup")(publishPath); 10 | }; 11 | 12 | module.exports = setupPages; 13 | -------------------------------------------------------------------------------- /tests/__snapshots__/optionalCatchAll.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Routing creates Netlify redirects 1`] = ` 4 | "# Next-on-Netlify Redirects 5 | /_next/data/%BUILD_ID%/page.json /.netlify/functions/next_page 200 6 | /page /.netlify/functions/next_page 200 7 | /_next/data/%BUILD_ID%/index.json /.netlify/functions/next_all 200 8 | /_next/data/%BUILD_ID%/* /.netlify/functions/next_all 200 9 | /_next/image* url=:url w=:width q=:quality /.netlify/functions/next_image?url=:url&w=:width&q=:quality 200 10 | / /.netlify/functions/next_all 200 11 | /_next/* /_next/:splat 200 12 | /* /.netlify/functions/next_all 200" 13 | `; 14 | -------------------------------------------------------------------------------- /lib/helpers/getPagesManifest.js: -------------------------------------------------------------------------------- 1 | const { join } = require("path"); 2 | const { existsSync, readJSONSync } = require("fs-extra"); 3 | const { NEXT_DIST_DIR } = require("../config"); 4 | 5 | const getPagesManifest = () => { 6 | const manifestPath = join(NEXT_DIST_DIR, "serverless", "pages-manifest.json"); 7 | if (!existsSync(manifestPath)) return {}; 8 | const contents = readJSONSync(manifestPath); 9 | // Next.js mistakenly puts backslashes in certain paths on Windows, replace 10 | Object.entries(contents).forEach(([key, value]) => { 11 | contents[key] = value.replace(/\\/g, "/"); 12 | }); 13 | return contents; 14 | }; 15 | 16 | module.exports = getPagesManifest; 17 | -------------------------------------------------------------------------------- /lib/pages/api/setup.js: -------------------------------------------------------------------------------- 1 | const { logTitle, logItem } = require("../../helpers/logger"); 2 | const setupNetlifyFunctionForPage = require("../../helpers/setupNetlifyFunctionForPage"); 3 | const pages = require("./pages"); 4 | 5 | // Create a Netlify Function for every API endpoint 6 | const setup = (functionsPath) => { 7 | logTitle( 8 | "💫 Setting up API endpoints as Netlify Functions in", 9 | functionsPath 10 | ); 11 | 12 | // Create Netlify Function for every page 13 | pages.forEach(({ filePath }) => { 14 | logItem(filePath); 15 | setupNetlifyFunctionForPage({ filePath, functionsPath, isApiPage: true }); 16 | }); 17 | }; 18 | 19 | module.exports = setup; 20 | -------------------------------------------------------------------------------- /tests/fixtures/pages-i18n-ssg-index/index.js: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | const Show = ({ show }) => ( 4 |
5 |

This page uses getStaticProps() to pre-fetch a TV show.

6 | 7 |
8 | 9 |

Show #{show.id}

10 |

{show.name}

11 | 12 |
13 | 14 | 15 | Go back home 16 | 17 |
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 | }; 29 | } 30 | 31 | export default Show; 32 | -------------------------------------------------------------------------------- /tests/fixtures/pages/getStaticProps/static.js: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | const Show = ({ show }) => ( 4 |
5 |

This page uses getStaticProps() to pre-fetch a TV show.

6 | 7 |
8 | 9 |

Show #{show.id}

10 |

{show.name}

11 | 12 |
13 | 14 | 15 | Go back home 16 | 17 |
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 | }; 29 | } 30 | 31 | export default Show; 32 | -------------------------------------------------------------------------------- /cypress/fixtures/pages-with-optionalCatchAll/index.js: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | const Index = () => ( 4 |
5 | 22 |
23 | ); 24 | 25 | export default Index; 26 | -------------------------------------------------------------------------------- /cypress/fixtures/pages/getStaticProps/static.js: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | const Show = ({ show }) => ( 4 |
5 |

This page uses getStaticProps() to pre-fetch a TV show.

6 | 7 |
8 | 9 |

Show #{show.id}

10 |

{show.name}

11 | 12 |
13 | 14 | 15 | Go back home 16 | 17 |
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 | }; 29 | } 30 | 31 | export default Show; 32 | -------------------------------------------------------------------------------- /cypress/plugins/clearDeployment.js: -------------------------------------------------------------------------------- 1 | // Clears the active deployment and shuts down servers 2 | const clearDeployment = (_params, config) => { 3 | // Shut down server. Must use -PID for some reason. 4 | // See: https://medium.com/@almenon214/killing-processes-with-node-772ffdd19aad 5 | const { activeDeployment } = config; 6 | if (activeDeployment && activeDeployment.serverPID) { 7 | process.stdout.write("Shutting down server..."); 8 | process.kill(-activeDeployment.serverPID); 9 | console.log(" Done! ✅"); 10 | } 11 | 12 | // Clear active deployment 13 | config.activeDeployment = null; 14 | 15 | return true; 16 | }; 17 | 18 | module.exports = clearDeployment; 19 | -------------------------------------------------------------------------------- /tests/fixtures/pages/api/shows/[id].js: -------------------------------------------------------------------------------- 1 | export default async (req, res) => { 2 | // Respond with JSON 3 | res.setHeader("Content-Type", "application/json"); 4 | 5 | // Get the ID of the show 6 | const { query } = req; 7 | const { id } = query; 8 | 9 | // Get the data 10 | const fetchRes = await fetch(`https://api.tvmaze.com/shows/${id}`); 11 | const data = await fetchRes.json(); 12 | 13 | // If show was found, return it 14 | if (fetchRes.status == 200) { 15 | res.status(200); 16 | res.json({ show: data }); 17 | } 18 | // If show was not found, return error 19 | else { 20 | res.status(404); 21 | res.json({ error: "Show not found" }); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /cypress/fixtures/pages-with-i18n/getStaticProps/static.js: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | const Show = ({ show }) => ( 4 |
5 |

This page uses getStaticProps() to pre-fetch a TV show.

6 | 7 |
8 | 9 |

Show #{show.id}

10 |

{show.name}

11 | 12 |
13 | 14 | 15 | Go back home 16 | 17 |
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 | }; 29 | } 30 | 31 | export default Show; 32 | -------------------------------------------------------------------------------- /lib/pages/getInitialProps/setup.js: -------------------------------------------------------------------------------- 1 | const { logTitle, logItem } = require("../../helpers/logger"); 2 | const setupNetlifyFunctionForPage = require("../../helpers/setupNetlifyFunctionForPage"); 3 | const pages = require("./pages"); 4 | 5 | // Create a Netlify Function for every page with getInitialProps 6 | const setup = (functionsPath) => { 7 | logTitle( 8 | "💫 Setting up pages with getInitialProps as Netlify Functions in", 9 | functionsPath 10 | ); 11 | 12 | // Create Netlify Function for every page 13 | pages.forEach(({ filePath }) => { 14 | logItem(filePath); 15 | setupNetlifyFunctionForPage({ filePath, functionsPath }); 16 | }); 17 | }; 18 | 19 | module.exports = setup; 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /lib/pages/getServerSideProps/setup.js: -------------------------------------------------------------------------------- 1 | const { logTitle, logItem } = require("../../helpers/logger"); 2 | const setupNetlifyFunctionForPage = require("../../helpers/setupNetlifyFunctionForPage"); 3 | const pages = require("./pages"); 4 | 5 | // Create a Netlify Function for every page with getServerSideProps 6 | const setup = (functionsPath) => { 7 | logTitle( 8 | "💫 Setting up pages with getServerSideProps as Netlify Functions in", 9 | functionsPath 10 | ); 11 | 12 | // Create Netlify Function for every page 13 | pages.forEach(({ filePath }) => { 14 | logItem(filePath); 15 | setupNetlifyFunctionForPage({ filePath, functionsPath }); 16 | }); 17 | }; 18 | 19 | module.exports = setup; 20 | -------------------------------------------------------------------------------- /cypress/fixtures/pages/getStaticProps/with-revalidate.js: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | const Show = ({ show }) => ( 4 |
5 |

This page uses getStaticProps() to pre-fetch a TV show.

6 | 7 |
8 | 9 |

Show #{show.id}

10 |

{show.name}

11 | 12 |
13 | 14 | 15 | Go back home 16 | 17 |
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; 33 | -------------------------------------------------------------------------------- /tests/fixtures/pages/getStaticProps/with-revalidate.js: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | const Show = ({ show }) => ( 4 |
5 |

This page uses getStaticProps() to pre-fetch a TV show.

6 | 7 |
8 | 9 |

Show #{show.id}

10 |

{show.name}

11 | 12 |
13 | 14 | 15 | Go back home 16 | 17 |
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; 33 | -------------------------------------------------------------------------------- /cypress/fixtures/pages-with-i18n/getStaticProps/with-revalidate.js: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | const Show = ({ show }) => ( 4 |
5 |

This page uses getStaticProps() to pre-fetch a TV show.

6 | 7 |
8 | 9 |

Show #{show.id}

10 |

{show.name}

11 | 12 |
13 | 14 | 15 | Go back home 16 | 17 |
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; 33 | -------------------------------------------------------------------------------- /cypress/fixtures/pages/api/shows/[...params].js: -------------------------------------------------------------------------------- 1 | export default async (req, res) => { 2 | // Get the params and query string parameters 3 | const { query } = req; 4 | const { params, ...queryStringParams } = query; 5 | 6 | // Get the ID of the show 7 | const id = params[0]; 8 | 9 | // Get the data 10 | const fetchRes = await fetch(`https://api.tvmaze.com/shows/${id}`); 11 | const data = await fetchRes.json(); 12 | 13 | // If show was found, return it 14 | if (fetchRes.status == 200) { 15 | res.status(200); 16 | res.json({ params, queryStringParams, show: data }); 17 | } 18 | // If show was not found, return error 19 | else { 20 | res.status(404); 21 | res.json({ error: "Show not found" }); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /cypress/fixtures/pages-with-i18n/api/shows/[...params].js: -------------------------------------------------------------------------------- 1 | export default async (req, res) => { 2 | // Get the params and query string parameters 3 | const { query } = req; 4 | const { params, ...queryStringParams } = query; 5 | 6 | // Get the ID of the show 7 | const id = params[0]; 8 | 9 | // Get the data 10 | const fetchRes = await fetch(`https://api.tvmaze.com/shows/${id}`); 11 | const data = await fetchRes.json(); 12 | 13 | // If show was found, return it 14 | if (fetchRes.status == 200) { 15 | res.status(200); 16 | res.json({ params, queryStringParams, show: data }); 17 | } 18 | // If show was not found, return error 19 | else { 20 | res.status(404); 21 | res.json({ error: "Show not found" }); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /cypress/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import "./commands"; 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /lib/helpers/setupStaticFileForPage.js: -------------------------------------------------------------------------------- 1 | const { copySync } = require("fs-extra"); 2 | const { join } = require("path"); 3 | const { NEXT_DIST_DIR } = require("../config"); 4 | 5 | // Copy the static asset from pages/inputPath to out_publish/outputPath 6 | const setupStaticFileForPage = ({ 7 | inputPath, 8 | outputPath = null, 9 | publishPath, 10 | }) => { 11 | // If no outputPath is set, default to the same as inputPath 12 | outputPath = outputPath || inputPath; 13 | 14 | // Perform copy operation 15 | copySync( 16 | join(NEXT_DIST_DIR, "serverless", "pages", inputPath), 17 | join(publishPath, outputPath), 18 | { 19 | overwrite: false, 20 | errorOnExist: true, 21 | } 22 | ); 23 | }; 24 | 25 | module.exports = setupStaticFileForPage; 26 | -------------------------------------------------------------------------------- /cypress/fixtures/pages/static.js: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | const Static = (props) => ( 4 |
5 |

6 | This page does not use getInitialProps. 7 |
8 | It is a static page. 9 |
10 | It is never server-side rendered. 11 |
12 | It is served directly by Netlify's CDN. 13 |
14 | The{" "} 15 | 16 | next-on-netlify 17 | {" "} 18 | npm package takes care of deciding which pages to render server-side and 19 | which ones to serve directly via CDN. 20 |

21 | 22 |
23 | 24 | 25 | Go back home 26 | 27 |
28 | ); 29 | 30 | export default Static; 31 | -------------------------------------------------------------------------------- /tests/fixtures/pages/static.js: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | const Static = (props) => ( 4 |
5 |

6 | This page does not use getInitialProps. 7 |
8 | It is a static page. 9 |
10 | It is never server-side rendered. 11 |
12 | It is served directly by Netlify's CDN. 13 |
14 | The{" "} 15 | 16 | next-on-netlify 17 | {" "} 18 | npm package takes care of deciding which pages to render server-side and 19 | which ones to serve directly via CDN. 20 |

21 | 22 |
23 | 24 | 25 | Go back home 26 | 27 |
28 | ); 29 | 30 | export default Static; 31 | -------------------------------------------------------------------------------- /cypress/fixtures/pages-with-i18n/static.js: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | const Static = (props) => ( 4 |
5 |

6 | This page does not use getInitialProps. 7 |
8 | It is a static page. 9 |
10 | It is never server-side rendered. 11 |
12 | It is served directly by Netlify's CDN. 13 |
14 | The{" "} 15 | 16 | next-on-netlify 17 | {" "} 18 | npm package takes care of deciding which pages to render server-side and 19 | which ones to serve directly via CDN. 20 |

21 | 22 |
23 | 24 | 25 | Go back home 26 | 27 |
28 | ); 29 | 30 | export default Static; 31 | -------------------------------------------------------------------------------- /lib/pages/withoutProps/pages.js: -------------------------------------------------------------------------------- 1 | const getPagesManifest = require("../../helpers/getPagesManifest"); 2 | const isHtmlFile = require("../../helpers/isHtmlFile"); 3 | const isRouteInPrerenderManifest = require("../../helpers/isRouteInPrerenderManifest"); 4 | 5 | // Collect pages 6 | const pages = []; 7 | 8 | // Get HTML and SSR pages and API endpoints from the NextJS pages manifest 9 | const pagesManifest = getPagesManifest(); 10 | 11 | // Parse HTML pages 12 | Object.entries(pagesManifest).forEach(([route, filePath]) => { 13 | // Ignore non-HTML files 14 | if (!isHtmlFile(filePath)) return; 15 | 16 | // Skip page if it is actually used with getStaticProps 17 | if (isRouteInPrerenderManifest(route)) return; 18 | 19 | // Add the HTML page 20 | pages.push({ route, filePath }); 21 | }); 22 | 23 | module.exports = pages; 24 | -------------------------------------------------------------------------------- /lib/templates/imageFunction.js: -------------------------------------------------------------------------------- 1 | const jimp = require("jimp"); 2 | 3 | // Function used to mimic next/image and sharp 4 | exports.handler = async (event) => { 5 | const { url, w = 500, q = 75 } = event.queryStringParameters; 6 | const width = parseInt(w); 7 | const quality = parseInt(q); 8 | 9 | const imageUrl = url.startsWith("/") 10 | ? `${process.env.DEPLOY_URL || `http://${event.headers.host}`}${url}` 11 | : url; 12 | const image = await jimp.read(imageUrl); 13 | 14 | image.resize(width, jimp.AUTO).quality(quality); 15 | 16 | const imageBuffer = await image.getBufferAsync(image.getMIME()); 17 | 18 | return { 19 | statusCode: 200, 20 | headers: { 21 | "Content-Type": image.getMIME(), 22 | }, 23 | body: imageBuffer.toString("base64"), 24 | isBase64Encoded: true, 25 | }; 26 | }; 27 | -------------------------------------------------------------------------------- /tests/fixtures/pages/api/shows/[...params].js: -------------------------------------------------------------------------------- 1 | export default async (req, res) => { 2 | // Respond with JSON 3 | res.setHeader("Content-Type", "application/json"); 4 | 5 | // Get the params and query string parameters 6 | const { query } = req; 7 | const { params, ...queryStringParams } = query; 8 | 9 | // Get the ID of the show 10 | const id = params[0]; 11 | 12 | // Get the data 13 | const fetchRes = await fetch(`https://api.tvmaze.com/shows/${id}`); 14 | const data = await fetchRes.json(); 15 | 16 | // If show was found, return it 17 | if (fetchRes.status == 200) { 18 | res.status(200); 19 | res.json({ params, queryStringParams, show: data }); 20 | } 21 | // If show was not found, return error 22 | else { 23 | res.status(404); 24 | res.json({ error: "Show not found" }); 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | .next 39 | 40 | # OS 41 | .DS_Store 42 | 43 | # Local Netlify folder 44 | .netlify 45 | -------------------------------------------------------------------------------- /tests/fixtures/pages-with-optionalCatchAll/page.js: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | const Index = () => ( 4 |
5 | 22 |
23 | ); 24 | 25 | export const getServerSideProps = async ({ params }) => { 26 | const res = await fetch("https://api.tvmaze.com/shows/42"); 27 | const data = await res.json(); 28 | 29 | return { 30 | props: { 31 | show: data, 32 | }, 33 | }; 34 | }; 35 | 36 | export default Index; 37 | -------------------------------------------------------------------------------- /tests/fixtures/pages/static/[id].js: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | const StaticWithID = (props) => ( 4 |
5 |

6 | This page does not use getInitialProps. 7 |
8 | It is a static page. 9 |
10 | It is never server-side rendered. 11 |
12 | It is served directly by Netlify's CDN. 13 |
14 |
15 | But it has a dynamic URL parameter: /static/:id. 16 |
17 | Try changing the ID. It will always render this page, no matter what you 18 | put. 19 |
20 | I am not sure what this is useful for. 21 |
22 | But it's a feature of NextJS, so... I'm supporting it. 23 |

24 | 25 |
26 | 27 | 28 | Go back home 29 | 30 |
31 | ); 32 | 33 | export default StaticWithID; 34 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Test builds 2 | cypress/builds/ 3 | tests/builds/ 4 | 5 | # Markdown 6 | *.md 7 | 8 | # Logs 9 | logs 10 | *.log 11 | npm-debug.log* 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | 24 | # nyc test coverage 25 | .nyc_output 26 | 27 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 28 | .grunt 29 | 30 | # node-waf configuration 31 | .lock-wscript 32 | 33 | # Compiled binary addons (http://nodejs.org/api/addons.html) 34 | build/Release 35 | 36 | # Dependency directories 37 | node_modules 38 | jspm_packages 39 | 40 | # Optional npm cache directory 41 | .npm 42 | 43 | # Optional REPL history 44 | .node_repl_history 45 | .next 46 | 47 | # Github 48 | .github 49 | -------------------------------------------------------------------------------- /cypress/fixtures/pages/static/[id].js: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | const StaticWithID = (props) => ( 4 |
5 |

6 | This page does not use getInitialProps. 7 |
8 | It is a static page. 9 |
10 | It is never server-side rendered. 11 |
12 | It is served directly by Netlify's CDN. 13 |
14 |
15 | But it has a dynamic URL parameter: /static/:id. 16 |
17 | Try changing the ID. It will always render this page, no matter what you 18 | put. 19 |
20 | I am not sure what this is useful for. 21 |
22 | But it's a feature of NextJS, so... I'm supporting it. 23 |

24 | 25 |
26 | 27 | 28 | Go back home 29 | 30 |
31 | ); 32 | 33 | export default StaticWithID; 34 | -------------------------------------------------------------------------------- /cypress/fixtures/pages-with-i18n/static/[id].js: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | const StaticWithID = (props) => ( 4 |
5 |

6 | This page does not use getInitialProps. 7 |
8 | It is a static page. 9 |
10 | It is never server-side rendered. 11 |
12 | It is served directly by Netlify's CDN. 13 |
14 |
15 | But it has a dynamic URL parameter: /static/:id. 16 |
17 | Try changing the ID. It will always render this page, no matter what you 18 | put. 19 |
20 | I am not sure what this is useful for. 21 |
22 | But it's a feature of NextJS, so... I'm supporting it. 23 |

24 | 25 |
26 | 27 | 28 | Go back home 29 | 30 |
31 | ); 32 | 33 | export default StaticWithID; 34 | -------------------------------------------------------------------------------- /lib/pages/getStaticPropsWithRevalidate/pages.js: -------------------------------------------------------------------------------- 1 | const isRouteWithFallback = require("../../helpers/isRouteWithFallback"); 2 | const getPrerenderManifest = require("../../helpers/getPrerenderManifest"); 3 | 4 | // Collect pages 5 | const pages = []; 6 | 7 | // Get pages using getStaticProps 8 | const { routes } = getPrerenderManifest(); 9 | 10 | // Parse pages 11 | Object.entries(routes).forEach( 12 | ([route, { dataRoute, srcRoute, initialRevalidateSeconds }]) => { 13 | // Skip pages without revalidate, these are handled by getStaticProps/pages 14 | if (!initialRevalidateSeconds) return; 15 | 16 | // Skip pages with fallback, these are handled by 17 | // getStaticPropsWithFallback/pages 18 | if (isRouteWithFallback(srcRoute)) return; 19 | 20 | // Add the page 21 | pages.push({ 22 | route, 23 | srcRoute, 24 | dataRoute, 25 | }); 26 | } 27 | ); 28 | 29 | module.exports = pages; 30 | -------------------------------------------------------------------------------- /lib/helpers/isRouteInPrerenderManifest.js: -------------------------------------------------------------------------------- 1 | const getPrerenderManifest = require("./getPrerenderManifest"); 2 | const i18n = require("./getI18n")(); 3 | 4 | const { routes, dynamicRoutes } = getPrerenderManifest(); 5 | const { defaultLocale, locales } = i18n; 6 | 7 | const isRouteInManifestWithI18n = (route) => { 8 | let isStaticRoute = false; 9 | Object.entries(routes).forEach(([staticRoute, { srcRoute }]) => { 10 | // This is because in i18n we set the nakedRoute to be the srcRoute in the manifest 11 | if (route === srcRoute) isStaticRoute = true; 12 | }); 13 | return isStaticRoute || route in dynamicRoutes; 14 | }; 15 | 16 | // Return true if the route is defined in the prerender manifest 17 | const isRouteInPrerenderManifest = (route) => { 18 | if (i18n.defaultLocale) return isRouteInManifestWithI18n(route); 19 | return route in routes || route in dynamicRoutes; 20 | }; 21 | 22 | module.exports = isRouteInPrerenderManifest; 23 | -------------------------------------------------------------------------------- /cypress/fixtures/pages/getServerSideProps/static.js: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | const Show = ({ show }) => ( 4 |
5 |

6 | This page uses getInitialProps() to fetch the show with the ID provided in 7 | the URL: /shows/:id 8 |
9 | Refresh the page to see server-side rendering in action. 10 |
11 | You can also try changing the ID to any other number between 1-10000. 12 |

13 | 14 |
15 | 16 |

Show #{show.id}

17 |

{show.name}

18 | 19 |
20 | 21 | 22 | Go back home 23 | 24 |
25 | ); 26 | 27 | export const getServerSideProps = async ({ params }) => { 28 | const res = await fetch("https://api.tvmaze.com/shows/42"); 29 | const data = await res.json(); 30 | 31 | return { 32 | props: { 33 | show: data, 34 | }, 35 | }; 36 | }; 37 | 38 | export default Show; 39 | -------------------------------------------------------------------------------- /tests/fixtures/pages-with-gssp-index/index.js: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | const Index = ({ show }) => ( 4 |
5 |

6 | This page uses getServerSideProps() to fetch the show with the ID provided 7 | in the URL: /shows/:id 8 |
9 | Refresh the page to see server-side rendering in action. 10 |
11 | You can also try changing the ID to any other number between 1-10000. 12 |

13 | 14 |
15 | 16 |

Show #{show.id}

17 |

{show.name}

18 | 19 |
20 | 21 | 22 | Go back home 23 | 24 |
25 | ); 26 | 27 | export const getServerSideProps = async ({ params }) => { 28 | const res = await fetch("https://api.tvmaze.com/shows/42"); 29 | const data = await res.json(); 30 | 31 | return { 32 | props: { 33 | show: data, 34 | }, 35 | }; 36 | }; 37 | 38 | export default Index; 39 | -------------------------------------------------------------------------------- /tests/fixtures/pages/getServerSideProps/static.js: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | const Show = ({ show }) => ( 4 |
5 |

6 | This page uses getInitialProps() to fetch the show with the ID provided in 7 | the URL: /shows/:id 8 |
9 | Refresh the page to see server-side rendering in action. 10 |
11 | You can also try changing the ID to any other number between 1-10000. 12 |

13 | 14 |
15 | 16 |

Show #{show.id}

17 |

{show.name}

18 | 19 |
20 | 21 | 22 | Go back home 23 | 24 |
25 | ); 26 | 27 | export const getServerSideProps = async ({ params }) => { 28 | const res = await fetch("https://api.tvmaze.com/shows/42"); 29 | const data = await res.json(); 30 | 31 | return { 32 | props: { 33 | show: data, 34 | }, 35 | }; 36 | }; 37 | 38 | export default Show; 39 | -------------------------------------------------------------------------------- /cypress/fixtures/pages-with-i18n/getServerSideProps/static.js: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | const Show = ({ show }) => ( 4 |
5 |

6 | This page uses getInitialProps() to fetch the show with the ID provided in 7 | the URL: /shows/:id 8 |
9 | Refresh the page to see server-side rendering in action. 10 |
11 | You can also try changing the ID to any other number between 1-10000. 12 |

13 | 14 |
15 | 16 |

Show #{show.id}

17 |

{show.name}

18 | 19 |
20 | 21 | 22 | Go back home 23 | 24 |
25 | ); 26 | 27 | export const getServerSideProps = async ({ params }) => { 28 | const res = await fetch("https://api.tvmaze.com/shows/42"); 29 | const data = await res.json(); 30 | 31 | return { 32 | props: { 33 | show: data, 34 | }, 35 | }; 36 | }; 37 | 38 | export default Show; 39 | -------------------------------------------------------------------------------- /.github/workflows/fossa.yml: -------------------------------------------------------------------------------- 1 | name: Dependency License Scanning 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | defaults: 9 | run: 10 | shell: bash 11 | 12 | jobs: 13 | fossa: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v2 18 | - name: Fossa init 19 | run: |- 20 | curl -H 'Cache-Control: no-cache' https://raw.githubusercontent.com/fossas/fossa-cli/master/install.sh | bash 21 | fossa init 22 | - name: Set env 23 | run: echo "line_number=$(grep -n "project" .fossa.yml | cut -f1 -d:)" >> $GITHUB_ENV 24 | - name: Configuration 25 | run: |- 26 | sed -i "${line_number}s|.*| project: git@github.com:${GITHUB_REPOSITORY}.git|" .fossa.yml 27 | cat .fossa.yml 28 | - name: Upload dependencies 29 | run: fossa analyze --debug 30 | env: 31 | FOSSA_API_KEY: ${{ secrets.FOSSA_API_KEY }} 32 | -------------------------------------------------------------------------------- /lib/pages/getStaticPropsWithFallback/setup.js: -------------------------------------------------------------------------------- 1 | const { join } = require("path"); 2 | const { logTitle, logItem } = require("../../helpers/logger"); 3 | const getFilePathForRoute = require("../../helpers/getFilePathForRoute"); 4 | const setupNetlifyFunctionForPage = require("../../helpers/setupNetlifyFunctionForPage"); 5 | const pages = require("./pages"); 6 | 7 | // Create a Netlify Function for every page with getStaticProps and fallback 8 | const setup = (functionsPath) => { 9 | logTitle( 10 | "💫 Setting up pages with getStaticProps and fallback: true", 11 | "as Netlify Functions in", 12 | functionsPath 13 | ); 14 | 15 | // Create Netlify Function for every page 16 | pages.forEach(({ route }) => { 17 | const relativePath = getFilePathForRoute(route, "js"); 18 | const filePath = join("pages", relativePath); 19 | logItem(filePath); 20 | setupNetlifyFunctionForPage({ filePath, functionsPath }); 21 | }); 22 | }; 23 | 24 | module.exports = setup; 25 | -------------------------------------------------------------------------------- /lib/pages/getStaticPropsWithFallback/redirects.js: -------------------------------------------------------------------------------- 1 | const { join } = require("path"); 2 | const addLocaleRedirects = require("../../helpers/addLocaleRedirects"); 3 | const getFilePathForRoute = require("../../helpers/getFilePathForRoute"); 4 | const getNetlifyFunctionName = require("../../helpers/getNetlifyFunctionName"); 5 | const pages = require("./pages"); 6 | 7 | const redirects = []; 8 | 9 | pages.forEach(({ route, dataRoute }) => { 10 | const relativePath = getFilePathForRoute(route, "js"); 11 | const filePath = join("pages", relativePath); 12 | const functionName = getNetlifyFunctionName(filePath); 13 | const target = `/.netlify/functions/${functionName}`; 14 | 15 | addLocaleRedirects(redirects)(route, target); 16 | 17 | // Add one redirect for the page 18 | redirects.push({ 19 | route, 20 | target, 21 | }); 22 | 23 | // Add one redirect for the data route 24 | redirects.push({ 25 | route: dataRoute, 26 | target, 27 | }); 28 | }); 29 | 30 | module.exports = redirects; 31 | -------------------------------------------------------------------------------- /lib/helpers/addDefaultLocaleRedirect.js: -------------------------------------------------------------------------------- 1 | const i18n = require("./getI18n")(); 2 | const { defaultLocale } = i18n; 3 | 4 | // In i18n projects, we need to create redirects from the "naked" route 5 | // to the defaultLocale-prepended route i.e. /static -> /en/static 6 | // Note: there can only one defaultLocale, but we put it in an array to simplify 7 | // logic in redirects.js files via concatenation 8 | const addDefaultLocaleRedirect = (redirects) => ( 9 | route, 10 | target, 11 | additionalParams 12 | ) => { 13 | // If no i18n, skip 14 | if (!defaultLocale) return; 15 | 16 | const routePieces = route.split("/"); 17 | const routeLocale = routePieces[1]; 18 | if (routeLocale === defaultLocale) { 19 | const nakedRoute = 20 | route === `/${routeLocale}` ? "/" : route.replace(`/${routeLocale}`, ""); 21 | redirects.push({ 22 | route: nakedRoute, 23 | target: target || route, 24 | ...additionalParams, 25 | }); 26 | } 27 | }; 28 | 29 | module.exports = addDefaultLocaleRedirect; 30 | -------------------------------------------------------------------------------- /cypress/fixtures/pages/previewTest/static.js: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | const StaticTest = ({ number }) => { 4 | return ( 5 |
6 |

7 | This page uses getStaticProps() and is SSRed when in preview mode. 8 |
9 |
10 | By default, it shows the TV show by ID (as static HTML). 11 |
12 | But when in preview mode, it shows person by ID instead (SSRed). 13 |

14 | 15 |
16 | 17 |

Number: {number}

18 | 19 | 20 | Go back home 21 | 22 |
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; 46 | -------------------------------------------------------------------------------- /lib/helpers/copyDynamicImportChunks.js: -------------------------------------------------------------------------------- 1 | const { join } = require("path"); 2 | const { copySync, readdirSync } = require("fs-extra"); 3 | const { logTitle } = require("../helpers/logger"); 4 | const { NEXT_DIST_DIR } = require("../config"); 5 | 6 | // Check if there are dynamic import chunks and copy to the necessary function dir 7 | const copyDynamicImportChunks = (functionPath) => { 8 | const chunksPath = join(NEXT_DIST_DIR, "serverless"); 9 | const files = readdirSync(chunksPath); 10 | const chunkRegex = new RegExp(/^(\.?[-_$~A-Z0-9a-z]+){1,}\.js$/g); 11 | const excludeFiles = ["init-server.js.js", "on-error-server.js.js"]; 12 | files.forEach((file) => { 13 | if (!excludeFiles.includes(file) && chunkRegex.test(file)) { 14 | logTitle("💼 Copying dynamic import chunks to", functionPath); 15 | copySync(join(chunksPath, file), join(functionPath, file), { 16 | overwrite: false, 17 | errorOnExist: true, 18 | }); 19 | } 20 | }); 21 | }; 22 | 23 | module.exports = copyDynamicImportChunks; 24 | -------------------------------------------------------------------------------- /cypress/fixtures/pages-with-i18n/previewTest/static.js: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | const StaticTest = ({ number }) => { 4 | return ( 5 |
6 |

7 | This page uses getStaticProps() and is SSRed when in preview mode. 8 |
9 |
10 | By default, it shows the TV show by ID (as static HTML). 11 |
12 | But when in preview mode, it shows person by ID instead (SSRed). 13 |

14 | 15 |
16 | 17 |

Number: {number}

18 | 19 | 20 | Go back home 21 | 22 |
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; 46 | -------------------------------------------------------------------------------- /lib/pages/withoutProps/redirects.js: -------------------------------------------------------------------------------- 1 | const addDefaultLocaleRedirect = require("../../helpers/addDefaultLocaleRedirect"); 2 | const isDynamicRoute = require("../../helpers/isDynamicRoute"); 3 | const pages = require("./pages"); 4 | 5 | let redirects = []; 6 | 7 | /** withoutProps pages 8 | * 9 | * Page params { 10 | * route -> '/about', '/initial/[id]' 11 | * filePath -> 'pages/about.html', 'pages/initial[id].html' 12 | * } 13 | * 14 | * Page params in i18n { 15 | * route -> '/en/about', '/fr/initial/[id]' 16 | * filePath -> 'pages/en/about.html', 'pages/fr/initial[id].html' 17 | * } 18 | **/ 19 | 20 | pages.forEach(({ route, filePath }) => { 21 | const target = filePath.replace(/pages/, ""); 22 | 23 | addDefaultLocaleRedirect(redirects)(route, target); 24 | 25 | // Only create normal redirects for pages with dynamic routing 26 | if (!isDynamicRoute(route)) return; 27 | 28 | redirects.push({ 29 | route, 30 | target: filePath.replace(/pages/, ""), 31 | }); 32 | }); 33 | 34 | module.exports = redirects; 35 | -------------------------------------------------------------------------------- /lib/steps/copyNextAssets.js: -------------------------------------------------------------------------------- 1 | const { join } = require("path"); 2 | const { copySync, existsSync } = require("fs-extra"); 3 | const { logTitle } = require("../helpers/logger"); 4 | const { NEXT_DIST_DIR } = require("../config"); 5 | 6 | // Copy the NextJS' static assets from NextJS distDir to Netlify publish folder. 7 | // These need to be available for NextJS to work. 8 | const copyNextAssets = (publishPath) => { 9 | const staticAssetsPath = join(NEXT_DIST_DIR, "static"); 10 | if (!existsSync(staticAssetsPath)) { 11 | throw new Error( 12 | "No static assets found in .next dist (aka no /.next/static). Please check your project configuration. Your next.config.js must be one of `serverless` or `experimental-serverless-trace`. Your build command should include `next build`." 13 | ); 14 | } 15 | logTitle("💼 Copying static NextJS assets to", publishPath); 16 | copySync(staticAssetsPath, join(publishPath, "_next", "static"), { 17 | overwrite: false, 18 | errorOnExist: true, 19 | }); 20 | }; 21 | 22 | module.exports = copyNextAssets; 23 | -------------------------------------------------------------------------------- /cypress/fixtures/pages/getStaticProps/[id].js: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | const Show = ({ show }) => ( 4 |
5 |

This page uses getStaticProps() to pre-fetch a TV show.

6 | 7 |
8 | 9 |

Show #{show.id}

10 |

{show.name}

11 | 12 |
13 | 14 | 15 | Go back home 16 | 17 |
18 | ); 19 | 20 | export async function getStaticPaths() { 21 | // Set the paths we want to pre-render 22 | const paths = [{ params: { id: "1" } }, { params: { id: "2" } }]; 23 | 24 | // We'll pre-render only these paths at build time. 25 | // { fallback: false } means other routes should 404. 26 | return { paths, fallback: false }; 27 | } 28 | 29 | export async function getStaticProps({ params }) { 30 | // The ID to render 31 | const { id } = params; 32 | 33 | const res = await fetch(`https://api.tvmaze.com/shows/${id}`); 34 | const data = await res.json(); 35 | 36 | return { 37 | props: { 38 | show: data, 39 | }, 40 | }; 41 | } 42 | 43 | export default Show; 44 | -------------------------------------------------------------------------------- /tests/fixtures/pages/getStaticProps/[id].js: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | const Show = ({ show }) => ( 4 |
5 |

This page uses getStaticProps() to pre-fetch a TV show.

6 | 7 |
8 | 9 |

Show #{show.id}

10 |

{show.name}

11 | 12 |
13 | 14 | 15 | Go back home 16 | 17 |
18 | ); 19 | 20 | export async function getStaticPaths() { 21 | // Set the paths we want to pre-render 22 | const paths = [{ params: { id: "1" } }, { params: { id: "2" } }]; 23 | 24 | // We'll pre-render only these paths at build time. 25 | // { fallback: false } means other routes should 404. 26 | return { paths, fallback: false }; 27 | } 28 | 29 | export async function getStaticProps({ params }) { 30 | // The ID to render 31 | const { id } = params; 32 | 33 | const res = await fetch(`https://api.tvmaze.com/shows/${id}`); 34 | const data = await res.json(); 35 | 36 | return { 37 | props: { 38 | show: data, 39 | }, 40 | }; 41 | } 42 | 43 | export default Show; 44 | -------------------------------------------------------------------------------- /cypress/fixtures/pages-with-i18n/getStaticProps/[id].js: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | const Show = ({ show }) => ( 4 |
5 |

This page uses getStaticProps() to pre-fetch a TV show.

6 | 7 |
8 | 9 |

Show #{show.id}

10 |

{show.name}

11 | 12 |
13 | 14 | 15 | Go back home 16 | 17 |
18 | ); 19 | 20 | export async function getStaticPaths() { 21 | // Set the paths we want to pre-render 22 | const paths = [{ params: { id: "1" } }, { params: { id: "2" } }]; 23 | 24 | // We'll pre-render only these paths at build time. 25 | // { fallback: false } means other routes should 404. 26 | return { paths, fallback: false }; 27 | } 28 | 29 | export async function getStaticProps({ params }) { 30 | // The ID to render 31 | const { id } = params; 32 | 33 | const res = await fetch(`https://api.tvmaze.com/shows/${id}`); 34 | const data = await res.json(); 35 | 36 | return { 37 | props: { 38 | show: data, 39 | }, 40 | }; 41 | } 42 | 43 | export default Show; 44 | -------------------------------------------------------------------------------- /lib/helpers/getDataRouteForRoute.js: -------------------------------------------------------------------------------- 1 | const { join } = require("path"); 2 | const { readFileSync } = require("fs-extra"); 3 | const { NEXT_DIST_DIR } = require("../config"); 4 | const getFilePathForRoute = require("./getFilePathForRoute"); 5 | 6 | // Get build ID that is used for data routes, e.g. /_next/data/BUILD_ID/... 7 | const fileContents = readFileSync(join(NEXT_DIST_DIR, "BUILD_ID")); 8 | const buildId = fileContents.toString(); 9 | 10 | const getPlainDataRoute = (route) => { 11 | const filePath = getFilePathForRoute(route, "json"); 12 | return `/_next/data/${buildId}${filePath}`; 13 | }; 14 | 15 | const getI18nDataRoute = (route, locale) => { 16 | const filePath = getFilePathForRoute(route, "json"); 17 | return route === "/" 18 | ? getPlainDataRoute(`/${locale}`) 19 | : `/_next/data/${buildId}/${locale}${filePath}`; 20 | }; 21 | 22 | // Return the data route for the given route 23 | const getDataRouteForRoute = (route, locale) => { 24 | if (locale) return getI18nDataRoute(route, locale); 25 | return getPlainDataRoute(route); 26 | }; 27 | 28 | module.exports = getDataRouteForRoute; 29 | -------------------------------------------------------------------------------- /tests/fixtures/pages/getStaticProps/withRevalidate/[id].js: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | const Show = ({ show }) => ( 4 |
5 |

This page uses getStaticProps() to pre-fetch a TV show.

6 | 7 |
8 | 9 |

Show #{show.id}

10 |

{show.name}

11 | 12 |
13 | 14 | 15 | Go back home 16 | 17 |
18 | ); 19 | 20 | export async function getStaticPaths() { 21 | // Set the paths we want to pre-render 22 | const paths = [{ params: { id: "1" } }, { params: { id: "2" } }]; 23 | 24 | // We'll pre-render only these paths at build time. 25 | // { fallback: false } means other routes should 404. 26 | return { paths, fallback: false }; 27 | } 28 | 29 | export async function getStaticProps({ params }) { 30 | // The ID to render 31 | const { id } = params; 32 | 33 | const res = await fetch(`https://api.tvmaze.com/shows/${id}`); 34 | const data = await res.json(); 35 | 36 | return { 37 | props: { 38 | show: data, 39 | }, 40 | revalidate: 1, 41 | }; 42 | } 43 | 44 | export default Show; 45 | -------------------------------------------------------------------------------- /cypress/fixtures/pages-with-optionalCatchAll-at-root/[bar]/ssr.js: -------------------------------------------------------------------------------- 1 | import Error from "next/error"; 2 | import Link from "next/link"; 3 | 4 | const Show = (props) => { 5 | const { errorCode, show } = props; 6 | // If show item was not found, render 404 page 7 | if (errorCode) { 8 | return ; 9 | } 10 | 11 | // Otherwise, render show 12 | return ( 13 |
14 |

Show #{show.id}

15 |

{show.name}

16 | 17 |
18 | 19 | 20 | Go back home to base page 21 | 22 |
23 | ); 24 | }; 25 | 26 | export const getServerSideProps = async ({ params }) => { 27 | // The ID to render 28 | const { bar } = params; 29 | 30 | const res = await fetch(`https://api.tvmaze.com/shows/${bar}`); 31 | const data = await res.json(); 32 | 33 | // Set error code if show item could not be found 34 | const errorCode = res.status > 200 ? res.status : false; 35 | 36 | return { 37 | props: { 38 | errorCode, 39 | show: data, 40 | }, 41 | }; 42 | }; 43 | 44 | export default Show; 45 | -------------------------------------------------------------------------------- /tests/fixtures/pages/getStaticProps/withRevalidate/withFallback/[id].js: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | const Show = ({ show }) => ( 4 |
5 |

This page uses getStaticProps() to pre-fetch a TV show.

6 | 7 |
8 | 9 |

Show #{show?.id}

10 |

{show?.name}

11 | 12 |
13 | 14 | 15 | Go back home 16 | 17 |
18 | ); 19 | 20 | export async function getStaticPaths() { 21 | // Set the paths we want to pre-render 22 | const paths = [{ params: { id: "1" } }, { params: { id: "2" } }]; 23 | 24 | // We'll pre-render only these paths at build time. 25 | // { fallback: false } means other routes should 404. 26 | return { paths, fallback: true }; 27 | } 28 | 29 | export async function getStaticProps({ params }) { 30 | // The ID to render 31 | const { id } = params; 32 | 33 | const res = await fetch(`https://api.tvmaze.com/shows/${id}`); 34 | const data = await res.json(); 35 | 36 | return { 37 | props: { 38 | show: data, 39 | }, 40 | revalidate: 1, 41 | }; 42 | } 43 | 44 | export default Show; 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Finn Woelm 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/fixtures/pages-dynamic-imports/deep/import.js: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import dynamic from "next/dynamic"; 3 | const Header = dynamic( 4 | () => import(/* webpackChunkName: 'header' */ "../../components/Header"), 5 | { ssr: true } 6 | ); 7 | 8 | const Show = ({ show }) => ( 9 |
10 |
11 |

12 | This page uses getInitialProps() to fetch the show with the ID provided in 13 | the URL: /shows/:id 14 |
15 | Refresh the page to see server-side rendering in action. 16 |
17 | You can also try changing the ID to any other number between 1-10000. 18 |

19 | 20 |
21 | 22 |

Show #{show.id}

23 |

{show.name}

24 | 25 |
26 | 27 | 28 | Go back home 29 | 30 |
31 | ); 32 | 33 | export const getServerSideProps = async ({ params }) => { 34 | const res = await fetch("https://api.tvmaze.com/shows/42"); 35 | const data = await res.json(); 36 | 37 | return { 38 | props: { 39 | show: data, 40 | }, 41 | }; 42 | }; 43 | 44 | export default Show; 45 | -------------------------------------------------------------------------------- /.github/workflows/workflow.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: 3 | # Ensure GitHub actions are not run twice for same commits 4 | push: 5 | branches: [master] 6 | tags: ["*"] 7 | pull_request: 8 | types: [opened, synchronize, reopened] 9 | jobs: 10 | build: 11 | runs-on: ${{ matrix.os }} 12 | timeout-minutes: 30 13 | strategy: 14 | matrix: 15 | os: [ubuntu-latest, macOS-latest, windows-latest] 16 | # We should test on 10.13.0 but don't due to a bug in Jest 17 | # https://github.com/facebook/jest/issues/9453 18 | node-version: [10.15.0, 15.x] 19 | exclude: 20 | - os: macOS-latest 21 | node-version: 10.15.0 22 | - os: windows-latest 23 | node-version: 10.15.0 24 | fail-fast: false 25 | steps: 26 | - name: Git checkout 27 | uses: actions/checkout@v2 28 | - name: Node.js ${{ matrix.node-version }} 29 | uses: actions/setup-node@v1 30 | with: 31 | node-version: ${{ matrix.node-version }} 32 | - name: Install dependencies 33 | run: npm install 34 | - name: Tests 35 | run: npm test 36 | -------------------------------------------------------------------------------- /tests/fixtures/pages-with-optionalCatchAll/[[...all]].js: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | import Link from "next/link"; 3 | 4 | const CatchAll = ({ 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
Loading...
; 11 | } 12 | 13 | return ( 14 |
15 |

This page uses getStaticProps() to pre-fetch a TV show.

16 | 17 |
18 | 19 |

Show #{show.id}

20 |

{show.name}

21 | 22 |
23 | 24 | 25 | Go back home 26 | 27 |
28 | ); 29 | }; 30 | 31 | export async function getServerSideProps({ params }) { 32 | // The ID to render 33 | const { all } = params; 34 | const id = all ? all[0] : 1; 35 | 36 | const res = await fetch(`https://api.tvmaze.com/shows/${id}`); 37 | const data = await res.json(); 38 | 39 | return { 40 | props: { 41 | show: data, 42 | }, 43 | }; 44 | } 45 | 46 | export default CatchAll; 47 | -------------------------------------------------------------------------------- /cypress/fixtures/pages-with-optionalCatchAll/catch/[[...all]].js: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | import Link from "next/link"; 3 | 4 | const CatchAll = ({ 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
Loading...
; 11 | } 12 | 13 | return ( 14 |
15 |

This page uses getStaticProps() to pre-fetch a TV show.

16 | 17 |
18 | 19 |

Show #{show.id}

20 |

{show.name}

21 | 22 |
23 | 24 | 25 | Go back home 26 | 27 |
28 | ); 29 | }; 30 | 31 | export async function getServerSideProps({ params }) { 32 | // The ID to render 33 | const { all } = params; 34 | const id = all ? all[0] : 1; 35 | 36 | const res = await fetch(`https://api.tvmaze.com/shows/${id}`); 37 | const data = await res.json(); 38 | 39 | return { 40 | props: { 41 | show: data, 42 | }, 43 | }; 44 | } 45 | 46 | export default CatchAll; 47 | -------------------------------------------------------------------------------- /tests/customNextDistDir.test.js: -------------------------------------------------------------------------------- 1 | // Test next-on-netlify when a custom distDir is set in next.config.js 2 | 3 | const { parse, join } = require("path"); 4 | const buildNextApp = require("./helpers/buildNextApp"); 5 | 6 | // The name of this test file (without extension) 7 | const FILENAME = parse(__filename).name; 8 | 9 | // The directory which will be used for testing. 10 | // We simulate a NextJS app within that directory, with pages, and a 11 | // package.json file. 12 | const PROJECT_PATH = join(__dirname, "builds", FILENAME); 13 | 14 | // Capture the output to verify successful build 15 | let buildOutput; 16 | 17 | beforeAll( 18 | async () => { 19 | buildOutput = await buildNextApp() 20 | .forTest(__filename) 21 | .withPages("pages-with-gssp-index") 22 | .withNextConfig("next.config.js") 23 | .withPackageJson("package.json") 24 | .build(); 25 | }, 26 | // time out after 180 seconds 27 | 180 * 1000 28 | ); 29 | 30 | describe("next-on-netlify", () => { 31 | test("builds successfully", () => { 32 | expect(buildOutput).toMatch("Next on Netlify"); 33 | expect(buildOutput).toMatch("Success! All done!"); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /cypress/fixtures/pages/getStaticProps/withRevalidate/[id].js: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | const Show = ({ show }) => ( 4 |
5 |

This page uses getStaticProps() to pre-fetch a TV show.

6 | 7 |
8 | 9 |

Show #{show.id}

10 |

{show.name}

11 | 12 |
13 | 14 | 15 | Go back home 16 | 17 |
18 | ); 19 | 20 | export async function getStaticPaths() { 21 | // Set the paths we want to pre-render 22 | const paths = [ 23 | { params: { id: "3" } }, 24 | { params: { id: "4" } }, 25 | { params: { id: "75" } }, 26 | ]; 27 | 28 | // We'll pre-render only these paths at build time. 29 | // { fallback: false } means other routes should 404. 30 | return { paths, fallback: false }; 31 | } 32 | 33 | export async function getStaticProps({ params }) { 34 | // The ID to render 35 | const { id } = params; 36 | 37 | const res = await fetch(`https://api.tvmaze.com/shows/${id}`); 38 | const data = await res.json(); 39 | 40 | return { 41 | props: { 42 | show: data, 43 | }, 44 | revalidate: 1, 45 | }; 46 | } 47 | 48 | export default Show; 49 | -------------------------------------------------------------------------------- /cypress/fixtures/pages-with-i18n/getStaticProps/withRevalidate/[id].js: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | const Show = ({ show }) => ( 4 |
5 |

This page uses getStaticProps() to pre-fetch a TV show.

6 | 7 |
8 | 9 |

Show #{show.id}

10 |

{show.name}

11 | 12 |
13 | 14 | 15 | Go back home 16 | 17 |
18 | ); 19 | 20 | export async function getStaticPaths() { 21 | // Set the paths we want to pre-render 22 | const paths = [ 23 | { params: { id: "3" } }, 24 | { params: { id: "4" } }, 25 | { params: { id: "75" } }, 26 | ]; 27 | 28 | // We'll pre-render only these paths at build time. 29 | // { fallback: false } means other routes should 404. 30 | return { paths, fallback: false }; 31 | } 32 | 33 | export async function getStaticProps({ params }) { 34 | // The ID to render 35 | const { id } = params; 36 | 37 | const res = await fetch(`https://api.tvmaze.com/shows/${id}`); 38 | const data = await res.json(); 39 | 40 | return { 41 | props: { 42 | show: data, 43 | }, 44 | revalidate: 1, 45 | }; 46 | } 47 | 48 | export default Show; 49 | -------------------------------------------------------------------------------- /lib/pages/getServerSideProps/redirects.js: -------------------------------------------------------------------------------- 1 | const addLocaleRedirects = require("../../helpers/addLocaleRedirects"); 2 | const getNetlifyFunctionName = require("../../helpers/getNetlifyFunctionName"); 3 | const getDataRouteForRoute = require("../../helpers/getDataRouteForRoute"); 4 | const pages = require("./pages"); 5 | 6 | const redirects = []; 7 | 8 | /** getServerSideProps pages 9 | * 10 | * Page params { 11 | * route -> '/ssr', '/ssr/[id]' 12 | * filePath -> 'pages/ssr.js', 'pages/ssr/[id].js' 13 | * } 14 | **/ 15 | 16 | pages.forEach(({ route, filePath }) => { 17 | const functionName = getNetlifyFunctionName(filePath); 18 | const target = `/.netlify/functions/${functionName}`; 19 | 20 | addLocaleRedirects(redirects)(route, target); 21 | 22 | // Add one redirect for the naked route 23 | // i.e. /ssr 24 | redirects.push({ 25 | route, 26 | target, 27 | }); 28 | 29 | // Add one redirect for the data route; 30 | // pages-manifest doesn't provide the dataRoute for us so we 31 | // construct it ourselves with getDataRouteForRoute 32 | redirects.push({ 33 | route: getDataRouteForRoute(route), 34 | target, 35 | }); 36 | }); 37 | 38 | module.exports = redirects; 39 | -------------------------------------------------------------------------------- /tests/nextConfigFunction.test.js: -------------------------------------------------------------------------------- 1 | // Test next-on-netlify when config is set from a function in next.config.js 2 | // See: https://github.com/netlify/next-on-netlify/issues/25 3 | 4 | const { parse, join } = require("path"); 5 | const buildNextApp = require("./helpers/buildNextApp"); 6 | 7 | // The name of this test file (without extension) 8 | const FILENAME = parse(__filename).name; 9 | 10 | // The directory which will be used for testing. 11 | // We simulate a NextJS app within that directory, with pages, and a 12 | // package.json file. 13 | const PROJECT_PATH = join(__dirname, "builds", FILENAME); 14 | 15 | // Capture the output to verify successful build 16 | let buildOutput; 17 | 18 | beforeAll( 19 | async () => { 20 | buildOutput = await buildNextApp() 21 | .forTest(__filename) 22 | .withPages("pages-simple") 23 | .withNextConfig("next.config.js-with-function.js") 24 | .withPackageJson("package.json") 25 | .build(); 26 | }, 27 | // time out after 180 seconds 28 | 180 * 1000 29 | ); 30 | 31 | describe("next-on-netlify", () => { 32 | test("builds successfully", () => { 33 | expect(buildOutput).toMatch("Next on Netlify"); 34 | expect(buildOutput).toMatch("Success! All done!"); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /lib/templates/netlifyFunction.js: -------------------------------------------------------------------------------- 1 | // TEMPLATE: This file will be copied to the Netlify functions directory when 2 | // running next-on-netlify 3 | 4 | // Render function for the Next.js page 5 | const renderNextPage = require("./renderNextPage"); 6 | 7 | exports.handler = async (event, context, callback) => { 8 | // x-forwarded-host is undefined on Netlify for proxied apps that need it 9 | // fixes https://github.com/netlify/next-on-netlify/issues/46 10 | if (!event.multiValueHeaders.hasOwnProperty("x-forwarded-host")) { 11 | event.multiValueHeaders["x-forwarded-host"] = [event.headers["host"]]; 12 | } 13 | 14 | // Get the request URL 15 | const { path } = event; 16 | console.log("[request]", path); 17 | 18 | // Render the Next.js page 19 | const response = await renderNextPage({ event, context }); 20 | 21 | // Convert header values to string. Netlify does not support integers as 22 | // header values. See: https://github.com/netlify/cli/issues/451 23 | Object.keys(response.multiValueHeaders).forEach((key) => { 24 | response.multiValueHeaders[key] = response.multiValueHeaders[ 25 | key 26 | ].map((value) => String(value)); 27 | }); 28 | 29 | response.multiValueHeaders["Cache-Control"] = ["no-cache"]; 30 | 31 | callback(null, response); 32 | }; 33 | -------------------------------------------------------------------------------- /lib/steps/setupHeaders.js: -------------------------------------------------------------------------------- 1 | const { join } = require("path"); 2 | const { existsSync, readFileSync, writeFileSync } = require("fs-extra"); 3 | const { logTitle, logItem } = require("../helpers/logger"); 4 | const { CUSTOM_HEADERS_PATH } = require("../config"); 5 | 6 | // Setup _headers file to override specific header rules for next assets 7 | const setupHeaders = (publishPath) => { 8 | logTitle("🔀 Setting up headers"); 9 | 10 | // Collect custom redirects defined by the user 11 | const headers = []; 12 | if (existsSync(CUSTOM_HEADERS_PATH)) { 13 | logItem("# Prepending custom headers"); 14 | headers.push(readFileSync(CUSTOM_HEADERS_PATH, "utf8")); 15 | } 16 | 17 | // Add NoN section heading 18 | headers.push("# Next-on-Netlify Headers"); 19 | 20 | // Add rule to override cache control for static chunks 21 | const indentNewLine = (s) => `\n ${s}`; 22 | const staticChunkRule = [ 23 | `/_next/static/chunks/*`, 24 | indentNewLine(`cache-control: public`), 25 | indentNewLine(`cache-control: max-age=31536000`), 26 | indentNewLine(`cache-control: immutable`), 27 | ].join(""); 28 | headers.push(staticChunkRule); 29 | 30 | // Write redirects to _redirects file 31 | writeFileSync(join(publishPath, "_headers"), headers.join("\n")); 32 | }; 33 | 34 | module.exports = setupHeaders; 35 | -------------------------------------------------------------------------------- /lib/pages/getInitialProps/pages.js: -------------------------------------------------------------------------------- 1 | const getPagesManifest = require("../../helpers/getPagesManifest"); 2 | const isHtmlFile = require("../../helpers/isHtmlFile"); 3 | const isFrameworkRoute = require("../../helpers/isFrameworkRoute"); 4 | const isApiRoute = require("../../helpers/isApiRoute"); 5 | const isRouteInPrerenderManifest = require("../../helpers/isRouteInPrerenderManifest"); 6 | const isRouteWithDataRoute = require("../../helpers/isRouteWithDataRoute"); 7 | 8 | // Collect pages 9 | const pages = []; 10 | 11 | // Get HTML and SSR pages and API endpoints from the NextJS pages manifest 12 | const pagesManifest = getPagesManifest(); 13 | 14 | // Parse pages 15 | Object.entries(pagesManifest).forEach(([route, filePath]) => { 16 | // Ignore HTML files 17 | if (isHtmlFile(filePath)) return; 18 | 19 | // Skip framework pages, such as _app and _error 20 | if (isFrameworkRoute(route)) return; 21 | 22 | // Skip API endpoints 23 | if (isApiRoute(route)) return; 24 | 25 | // Skip page if it is actually used with getStaticProps 26 | if (isRouteInPrerenderManifest(route)) return; 27 | 28 | // Skip page if it has a data route (because then it is a page with 29 | // getServerSideProps) 30 | if (isRouteWithDataRoute(route)) return; 31 | 32 | // Add page 33 | pages.push({ route, filePath }); 34 | }); 35 | 36 | module.exports = pages; 37 | -------------------------------------------------------------------------------- /lib/pages/getServerSideProps/pages.js: -------------------------------------------------------------------------------- 1 | const getPagesManifest = require("../../helpers/getPagesManifest"); 2 | const isHtmlFile = require("../../helpers/isHtmlFile"); 3 | const isFrameworkRoute = require("../../helpers/isFrameworkRoute"); 4 | const isApiRoute = require("../../helpers/isApiRoute"); 5 | const isRouteInPrerenderManifest = require("../../helpers/isRouteInPrerenderManifest"); 6 | const isRouteWithDataRoute = require("../../helpers/isRouteWithDataRoute"); 7 | 8 | // Collect pages 9 | const pages = []; 10 | 11 | // Get HTML and SSR pages and API endpoints from the NextJS pages manifest 12 | const pagesManifest = getPagesManifest(); 13 | 14 | // Parse pages 15 | Object.entries(pagesManifest).forEach(([route, filePath]) => { 16 | // Ignore HTML files 17 | if (isHtmlFile(filePath)) return; 18 | 19 | // Skip framework pages, such as _app and _error 20 | if (isFrameworkRoute(route)) return; 21 | 22 | // Skip API endpoints 23 | if (isApiRoute(route)) return; 24 | 25 | // Skip page if it is actually used with getStaticProps 26 | if (isRouteInPrerenderManifest(route)) return; 27 | 28 | // Skip page if it has no data route (because then it is a page with 29 | // getInitialProps) 30 | if (!isRouteWithDataRoute(route)) return; 31 | 32 | // Add page 33 | pages.push({ route, filePath }); 34 | }); 35 | 36 | module.exports = pages; 37 | -------------------------------------------------------------------------------- /lib/templates/renderNextPage.js: -------------------------------------------------------------------------------- 1 | // Load the NextJS page 2 | const nextPage = require("./nextPage"); 3 | const createRequestObject = require("./createRequestObject"); 4 | const createResponseObject = require("./createResponseObject"); 5 | 6 | // Render the Next.js page 7 | const renderNextPage = ({ event, context }) => { 8 | // The Next.js page is rendered inside a promise that is resolved when the 9 | // Next.js page ends the response via `res.end()` 10 | const promise = new Promise((resolve) => { 11 | // Create a Next.js-compatible request and response object 12 | // These mock the ClientRequest and ServerResponse classes from node http 13 | // See: https://nodejs.org/api/http.html 14 | const req = createRequestObject({ event, context }); 15 | const res = createResponseObject({ 16 | onResEnd: (response) => resolve(response), 17 | }); 18 | 19 | // Check if page is a Next.js page or an API route 20 | const isNextPage = nextPage.render instanceof Function; 21 | const isApiRoute = !isNextPage; 22 | 23 | // Perform the render: render() for Next.js page or default() for API route 24 | if (isNextPage) return nextPage.render(req, res); 25 | if (isApiRoute) return nextPage.default(req, res); 26 | }); 27 | 28 | // Return the promise 29 | return promise; 30 | }; 31 | 32 | module.exports = renderNextPage; 33 | -------------------------------------------------------------------------------- /lib/helpers/logger.js: -------------------------------------------------------------------------------- 1 | // Helpers for writing info to console.log 2 | const { program } = require("commander"); 3 | 4 | // Number of items processed in current build step 5 | let itemsCount = 0; 6 | 7 | // Format in bold 8 | const logTitle = (...args) => { 9 | // Finish previous log section 10 | finishLogForBuildStep(); 11 | 12 | // Print title 13 | log(`\x1b[1m${args.join(" ")}\x1b[22m`); 14 | }; 15 | 16 | // Indent by three spaces 17 | const logItem = (...args) => { 18 | // Display item if within max log lines 19 | if (itemsCount < program.maxLogLines) log(" ", ...args); 20 | 21 | itemsCount += 1; 22 | }; 23 | 24 | // Just console.log 25 | const log = (...args) => console.log(...args); 26 | 27 | // Finish log section: Write a single line for any suppressed/hidden items 28 | // and reset the item counter 29 | const finishLogForBuildStep = () => { 30 | // Display number of suppressed log lines 31 | if (itemsCount > program.maxLogLines) { 32 | const hiddenLines = itemsCount - program.maxLogLines; 33 | log( 34 | " ", 35 | "+", 36 | hiddenLines, 37 | "more", 38 | "(run next-on-netlify with --max-log-lines XX to", 39 | "show more or fewer lines)" 40 | ); 41 | } 42 | 43 | // Reset counter 44 | itemsCount = 0; 45 | }; 46 | 47 | module.exports = { 48 | logTitle, 49 | logItem, 50 | log, 51 | }; 52 | -------------------------------------------------------------------------------- /lib/pages/getStaticPropsWithRevalidate/setup.js: -------------------------------------------------------------------------------- 1 | const { join } = require("path"); 2 | const { logTitle, logItem } = require("../../helpers/logger"); 3 | const getFilePathForRoute = require("../../helpers/getFilePathForRoute"); 4 | const setupNetlifyFunctionForPage = require("../../helpers/setupNetlifyFunctionForPage"); 5 | const pages = require("./pages"); 6 | 7 | // Create a Netlify Function for every page with getStaticProps and revalidate 8 | const setup = (functionsPath) => { 9 | logTitle( 10 | "💫 Setting up pages with getStaticProps and revalidation interval", 11 | "as Netlify Functions in", 12 | functionsPath 13 | ); 14 | 15 | // Keep track of the functions that have been set up, so that we do not set up 16 | // a function for the same file path twice 17 | const filePathsDone = []; 18 | 19 | // Create Netlify Function for every page 20 | pages.forEach(({ route, srcRoute }) => { 21 | const relativePath = getFilePathForRoute(srcRoute || route, "js"); 22 | const filePath = join("pages", relativePath); 23 | 24 | // Skip if we have already set up a function for this file 25 | if (filePathsDone.includes(filePath)) return; 26 | 27 | // Set up the function 28 | logItem(filePath); 29 | setupNetlifyFunctionForPage({ filePath, functionsPath }); 30 | filePathsDone.push(filePath); 31 | }); 32 | }; 33 | 34 | module.exports = setup; 35 | -------------------------------------------------------------------------------- /lib/pages/withoutProps/setup.js: -------------------------------------------------------------------------------- 1 | const { join, relative } = require("path"); 2 | const { copySync } = require("fs-extra"); 3 | const { logTitle, logItem } = require("../../helpers/logger"); 4 | const { NEXT_DIST_DIR } = require("../../config"); 5 | const i18n = require("../../helpers/getI18n")(); 6 | const setupStaticFileForPage = require("../../helpers/setupStaticFileForPage"); 7 | const pages = require("./pages"); 8 | 9 | // Identify all pages that have been pre-rendered and copy each one to the 10 | // Netlify publish directory. 11 | const setup = (publishPath) => { 12 | logTitle("🔥 Copying pre-rendered pages without props to", publishPath); 13 | 14 | // Copy each page to the Netlify publish directory 15 | pages.forEach(({ filePath }) => { 16 | logItem(filePath); 17 | 18 | // HACK: If i18n, 404.html needs to be at the top level of the publish directory 19 | if ( 20 | i18n.defaultLocale && 21 | filePath === `pages/${i18n.defaultLocale}/404.html` 22 | ) { 23 | copySync( 24 | join(NEXT_DIST_DIR, "serverless", filePath), 25 | join(publishPath, "404.html") 26 | ); 27 | } 28 | 29 | // The path to the file, relative to the pages directory 30 | const relativePath = relative("pages", filePath); 31 | setupStaticFileForPage({ inputPath: relativePath, publishPath }); 32 | }); 33 | }; 34 | 35 | module.exports = setup; 36 | -------------------------------------------------------------------------------- /tests/fixtures/pages/shows/[id].js: -------------------------------------------------------------------------------- 1 | import Error from "next/error"; 2 | import Link from "next/link"; 3 | 4 | const Show = ({ errorCode, show }) => { 5 | // If show item was not found, render 404 page 6 | if (errorCode) { 7 | return ; 8 | } 9 | 10 | // Otherwise, render show 11 | return ( 12 |
13 |

14 | This page uses getInitialProps() to fetch the show with the ID provided 15 | in the URL: /shows/:id 16 |
17 | Refresh the page to see server-side rendering in action. 18 |
19 | You can also try changing the ID to any other number between 1-10000. 20 |

21 | 22 |
23 | 24 |

Show #{show.id}

25 |

{show.name}

26 | 27 |
28 | 29 | 30 | Go back home 31 | 32 |
33 | ); 34 | }; 35 | 36 | Show.getInitialProps = async ({ res: req, query }) => { 37 | // Get the ID to render 38 | const { id } = query; 39 | 40 | // Get the data 41 | const res = await fetch(`https://api.tvmaze.com/shows/${id}`); 42 | const data = await res.json(); 43 | 44 | // Set error code if show item could not be found 45 | const errorCode = res.status > 200 ? res.status : false; 46 | 47 | return { errorCode, show: data }; 48 | }; 49 | 50 | export default Show; 51 | -------------------------------------------------------------------------------- /cypress/fixtures/pages/shows/[id].js: -------------------------------------------------------------------------------- 1 | import Error from "next/error"; 2 | import Link from "next/link"; 3 | 4 | const Show = ({ errorCode, show }) => { 5 | // If show item was not found, render 404 page 6 | if (errorCode) { 7 | return ; 8 | } 9 | 10 | // Otherwise, render show 11 | return ( 12 |
13 |

14 | This page uses getInitialProps() to fetch the show with the ID provided 15 | in the URL: /shows/:id 16 |
17 | Refresh the page to see server-side rendering in action. 18 |
19 | You can also try changing the ID to any other number between 1-10000. 20 |

21 | 22 |
23 | 24 |

Show #{show.id}

25 |

{show.name}

26 | 27 |
28 | 29 | 30 | Go back home 31 | 32 |
33 | ); 34 | }; 35 | 36 | Show.getInitialProps = async ({ res: req, query }) => { 37 | // Get the ID to render 38 | const { id } = query; 39 | 40 | // Get the data 41 | const res = await fetch(`https://api.tvmaze.com/shows/${id}`); 42 | const data = await res.json(); 43 | 44 | // Set error code if show item could not be found 45 | const errorCode = res.status > 200 ? res.status : false; 46 | 47 | return { errorCode, show: data }; 48 | }; 49 | 50 | export default Show; 51 | -------------------------------------------------------------------------------- /cypress/fixtures/pages-with-i18n/shows/[id].js: -------------------------------------------------------------------------------- 1 | import Error from "next/error"; 2 | import Link from "next/link"; 3 | 4 | const Show = ({ errorCode, show }) => { 5 | // If show item was not found, render 404 page 6 | if (errorCode) { 7 | return ; 8 | } 9 | 10 | // Otherwise, render show 11 | return ( 12 |
13 |

14 | This page uses getInitialProps() to fetch the show with the ID provided 15 | in the URL: /shows/:id 16 |
17 | Refresh the page to see server-side rendering in action. 18 |
19 | You can also try changing the ID to any other number between 1-10000. 20 |

21 | 22 |
23 | 24 |

Show #{show.id}

25 |

{show.name}

26 | 27 |
28 | 29 | 30 | Go back home 31 | 32 |
33 | ); 34 | }; 35 | 36 | Show.getInitialProps = async ({ res: req, query }) => { 37 | // Get the ID to render 38 | const { id } = query; 39 | 40 | // Get the data 41 | const res = await fetch(`https://api.tvmaze.com/shows/${id}`); 42 | const data = await res.json(); 43 | 44 | // Set error code if show item could not be found 45 | const errorCode = res.status > 200 ? res.status : false; 46 | 47 | return { errorCode, show: data }; 48 | }; 49 | 50 | export default Show; 51 | -------------------------------------------------------------------------------- /lib/helpers/getSortedRedirects.js: -------------------------------------------------------------------------------- 1 | const { 2 | getSortedRoutes: getSortedRoutesFromNext, 3 | } = require("next/dist/next-server/lib/router/utils/sorted-routes"); 4 | const removeFileExtension = require("./removeFileExtension"); 5 | 6 | // Return an array of redirects sorted in order of specificity, i.e., more generic 7 | // routes precede more specific ones 8 | const getSortedRedirects = (redirects) => { 9 | // The @sls-next getSortedRoutes does not correctly sort routes with file 10 | // endings (e.g., json), so we remove them before sorting and add them back 11 | // after sorting 12 | const routesWithoutExtensions = redirects.map(({ route }) => 13 | removeFileExtension(route) 14 | ); 15 | 16 | // Sort the "naked" routes 17 | const sortedRoutes = getSortedRoutesFromNext(routesWithoutExtensions); 18 | 19 | // Return original routes in the sorted order 20 | return redirects.sort((a, b) => { 21 | // If routes are different, sort according to Next.js' getSortedRoutes 22 | if (a.route !== b.route) { 23 | return ( 24 | sortedRoutes.indexOf(removeFileExtension(a.route)) - 25 | sortedRoutes.indexOf(removeFileExtension(b.route)) 26 | ); 27 | } 28 | // Otherwise, put the route with more conditions first 29 | return (b.conditions || []).length - (a.conditions || []).length; 30 | }); 31 | }; 32 | 33 | module.exports = getSortedRedirects; 34 | -------------------------------------------------------------------------------- /cypress/fixtures/pages/getServerSideProps/[id].js: -------------------------------------------------------------------------------- 1 | import Error from "next/error"; 2 | import Link from "next/link"; 3 | 4 | const Show = ({ errorCode, show }) => { 5 | // If show item was not found, render 404 page 6 | if (errorCode) { 7 | return ; 8 | } 9 | 10 | // Otherwise, render show 11 | return ( 12 |
13 |

14 | This page uses getInitialProps() to fetch the show with the ID provided 15 | in the URL: /shows/:id 16 |
17 | Refresh the page to see server-side rendering in action. 18 |
19 | You can also try changing the ID to any other number between 1-10000. 20 |

21 | 22 |
23 | 24 |

Show #{show.id}

25 |

{show.name}

26 | 27 |
28 | 29 | 30 | Go back home 31 | 32 |
33 | ); 34 | }; 35 | 36 | export const getServerSideProps = async ({ params }) => { 37 | // The ID to render 38 | const { id } = params; 39 | 40 | const res = await fetch(`https://api.tvmaze.com/shows/${id}`); 41 | const data = await res.json(); 42 | 43 | // Set error code if show item could not be found 44 | const errorCode = res.status > 200 ? res.status : false; 45 | 46 | return { 47 | props: { 48 | errorCode, 49 | show: data, 50 | }, 51 | }; 52 | }; 53 | 54 | export default Show; 55 | -------------------------------------------------------------------------------- /tests/fixtures/pages/getServerSideProps/[id].js: -------------------------------------------------------------------------------- 1 | import Error from "next/error"; 2 | import Link from "next/link"; 3 | 4 | const Show = ({ errorCode, show }) => { 5 | // If show item was not found, render 404 page 6 | if (errorCode) { 7 | return ; 8 | } 9 | 10 | // Otherwise, render show 11 | return ( 12 |
13 |

14 | This page uses getInitialProps() to fetch the show with the ID provided 15 | in the URL: /shows/:id 16 |
17 | Refresh the page to see server-side rendering in action. 18 |
19 | You can also try changing the ID to any other number between 1-10000. 20 |

21 | 22 |
23 | 24 |

Show #{show.id}

25 |

{show.name}

26 | 27 |
28 | 29 | 30 | Go back home 31 | 32 |
33 | ); 34 | }; 35 | 36 | export const getServerSideProps = async ({ params }) => { 37 | // The ID to render 38 | const { id } = params; 39 | 40 | const res = await fetch(`https://api.tvmaze.com/shows/${id}`); 41 | const data = await res.json(); 42 | 43 | // Set error code if show item could not be found 44 | const errorCode = res.status > 200 ? res.status : false; 45 | 46 | return { 47 | props: { 48 | errorCode, 49 | show: data, 50 | }, 51 | }; 52 | }; 53 | 54 | export default Show; 55 | -------------------------------------------------------------------------------- /MIGRATING.md: -------------------------------------------------------------------------------- 1 | ## Migrating to Essential Next.js Plugin 2 | 3 | This guide is to assist `next-on-netlify` users in their migration to Netlify's [Essential Next.js Build Plugin](https://github.com/netlify/netlify-plugin-nextjs). 4 | 5 | Please visit [this issue](https://github.com/netlify/next-on-netlify/issues/176) to ask questions about migrating and/or the deprecation of `next-on-netlify`. 6 | 7 | ### Existing NoN users 8 | 9 | 1. [Uninstall](https://docs.npmjs.com/uninstalling-packages-and-dependencies) `next-on-netlify`. 10 | 2. Remove the `"postbuild": "next-on-netlify"` script from your `package.json`. 11 | 3. Get the Essential Next.js plugin. 12 | - For new Next.js sites on Netlify, the plugin will be automatically installed. You can [opt out of the plugin](https://docs.netlify.com/configure-builds/build-plugins/#remove-a-plugin) by visiting the **Plugins** tab for your site in the Netlify UI. 13 | - For existing sites on Netlify, follow the manual installation instructions in the [plugin README](https://github.com/netlify/netlify-plugin-nextjs#installation-and-configuration), which explains UI-based and file-based installation. 14 | 15 | ### Brand New Users 16 | 17 | Deploy a new Next.js site to Netlify! Your site will automatically build with the plugin. You can [opt out of the plugin](https://docs.netlify.com/configure-builds/build-plugins/#remove-a-plugin) by visiting the **Plugins** tab for your site in the Netlify UI. 18 | -------------------------------------------------------------------------------- /cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // Loads a page and removes all Javascript 2 | // Adapted from https://glebbahmutov.com/blog/ssr-e2e/ 3 | Cypress.Commands.add("ssr", (url) => { 4 | cy.request(url) 5 | .its("body") 6 | .then((html) => { 7 | // remove all