diff --git a/README.md b/README.md index 5c6d561c27..d902eafbd7 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,10 @@ The Next.js Runtime fully supports ISR on Netlify. For more details see Note that Netlify has a minimum TTL of 60 seconds for revalidation. +## Disable Static 404 on Dynamic Routes with fallback:false + +Currently when hitting a non-prerendered path with `fallback=false` it will default to a 404 page. You can now change this default setting by using the environemnt variable `LEGACY_FALLBACK_FALSE=true`. With the environment variable set, those non-prerendered paths will now be routed through using the ISR Handler and will allow you to add redirects for those non-prerendered paths. + ## Use with `next export` If you are using `next export` to generate a static site, you do not need most of the functionality of this Next.js diff --git a/demos/default/next.config.js b/demos/default/next.config.js index 890533b532..c81e87d68d 100644 --- a/demos/default/next.config.js +++ b/demos/default/next.config.js @@ -67,6 +67,11 @@ module.exports = { destination: '/', permanent: true, }, + { + source: '/getStaticProps/4/', + destination: '/', + permanent: true, + }, ] }, // https://nextjs.org/docs/basic-features/image-optimization#domains diff --git a/packages/runtime/src/helpers/redirects.ts b/packages/runtime/src/helpers/redirects.ts index c1710bf7b2..1375bc4472 100644 --- a/packages/runtime/src/helpers/redirects.ts +++ b/packages/runtime/src/helpers/redirects.ts @@ -1,5 +1,6 @@ import type { NetlifyConfig } from '@netlify/build' import { yellowBright } from 'chalk' +import destr from 'destr' import { readJSON } from 'fs-extra' import type { NextConfig } from 'next' import type { PrerenderManifest, SsgRoute } from 'next/dist/build' @@ -194,7 +195,7 @@ const generateStaticIsrRewrites = ({ /** * Generate rewrites for all dynamic routes */ -const generateDynamicRewrites = ({ +export const generateDynamicRewrites = ({ dynamicRoutes, prerenderedDynamicRoutes, middleware, @@ -238,7 +239,11 @@ const generateDynamicRewrites = ({ withData: true, }), ) - } else if (prerenderedDynamicRoutes[route.page].fallback === false && !is404Isr) { + } else if ( + prerenderedDynamicRoutes[route.page].fallback === false && + !is404Isr && + !destr(process.env.LEGACY_FALLBACK_FALSE) + ) { dynamicRewrites.push(...redirectsForNext404Route({ route: route.page, buildId, basePath, i18n })) } else { dynamicRewrites.push( diff --git a/test/helpers/utils.spec.ts b/test/helpers/utils.spec.ts index 772072bceb..19d70fcae2 100644 --- a/test/helpers/utils.spec.ts +++ b/test/helpers/utils.spec.ts @@ -1,6 +1,8 @@ import Chance from 'chance' +import type { PrerenderManifest } from 'next/dist/build' import { ExperimentalConfig } from 'next/dist/server/config-shared' +import { generateDynamicRewrites } from '../../packages/runtime/src/helpers/redirects' import { getCustomImageResponseHeaders, getRemotePatterns, @@ -8,6 +10,46 @@ import { redirectsForNext404Route, } from '../../packages/runtime/src/helpers/utils' +const basePrerenderManifest: PrerenderManifest = { + version: 3, + routes: {}, + dynamicRoutes: {}, + notFoundRoutes: [], +} + +const prerenderManifest: PrerenderManifest = { + ...basePrerenderManifest, + dynamicRoutes: { + '/getStaticProps/[id]': { + routeRegex: '^/getStaticProps/([^/]+?)(?:/)?$', + dataRoute: '/_next/data/build-id/getStaticProps/[id].json', + fallback: false, + dataRouteRegex: '^/_next/data/build\\-id/getStaticProps/([^/]+?)\\.json$', + }, + }, +} + +const dynamicRoutes = [ + { + page: '/getStaticProps/[id]', + regex: '^/getStaticProps/([^/]+?)(?:/)?$', + routeKeys: { + nextParamid: 'nextParamid', + }, + namedRegex: '^/getStaticProps/(?[^/]+?)(?:/)?$', + }, +] + +const route = { + dynamicRoutes, + prerenderedDynamicRoutes: prerenderManifest.dynamicRoutes, + basePath: '', + i18n: null, + buildId: 'test', + middleware: [], + is404Isr: false, +} + const chance = new Chance() describe('getCustomImageResponseHeaders', () => { @@ -132,4 +174,52 @@ describe('redirectsForNext404Route', () => { { force: false, from: '/fr/test', status: 404, to: '/server/pages/fr/404.html' }, ]) }) + + it('returns static 404 redirects when LEGACY_FALLBACK_FALSE is not set', async () => { + const expected = { + dynamicRewrites: [ + { + force: false, + from: '/_next/data/test/getStaticProps/:id.json', + status: 404, + to: '/server/pages/404.html', + }, + { + force: false, + from: '/getStaticProps/:id', + status: 404, + to: '/server/pages/404.html', + }, + ], + dynamicRoutesThatMatchMiddleware: [], + } + + expect(generateDynamicRewrites(route)).toStrictEqual(expected) + }) + + it('does not return static 404 redirects when LEGACY_FALLBACK_FALSE is set', async () => { + process.env.LEGACY_FALLBACK_FALSE = 'true' + + const expected = { + dynamicRewrites: [ + { + force: false, + from: '/_next/data/test/getStaticProps/:id.json', + status: 200, + to: '/.netlify/builders/___netlify-odb-handler', + }, + { + force: false, + from: '/getStaticProps/:id', + status: 200, + to: '/.netlify/builders/___netlify-odb-handler', + }, + ], + dynamicRoutesThatMatchMiddleware: [], + } + + expect(generateDynamicRewrites(route)).toStrictEqual(expected) + + delete process.env.LEGACY_FALLBACK_FALSE + }) })