diff --git a/packages/next/next-server/server/next-server.ts b/packages/next/next-server/server/next-server.ts index 5f9fbd23fb65..927071d470b2 100644 --- a/packages/next/next-server/server/next-server.ts +++ b/packages/next/next-server/server/next-server.ts @@ -580,6 +580,7 @@ export default class Server { } } ;(req as any)._nextDidRewrite = true + ;(req as any)._nextRewroteUrl = newUrl return { finished: false, @@ -970,8 +971,12 @@ export default class Server { isPreviewMode = previewData !== false } - // Compute the iSSG cache key - let urlPathname = `${parseUrl(req.url || '').pathname!}` + // Compute the iSSG cache key. We use the rewroteUrl since + // pages with fallback: false are allowed to be rewritten to + // and we need to look up the path by the rewritten path + let urlPathname = (req as any)._nextRewroteUrl + ? (req as any)._nextRewroteUrl + : `${parseUrl(req.url || '').pathname!}` // remove /_next/data prefix from urlPathname so it matches // for direct page visit and /_next/data visit diff --git a/test/integration/prerender/next.config.js b/test/integration/prerender/next.config.js new file mode 100644 index 000000000000..edfaf6892fe2 --- /dev/null +++ b/test/integration/prerender/next.config.js @@ -0,0 +1,12 @@ +module.exports = { + experimental: { + rewrites() { + return [ + { + source: '/about', + destination: '/lang/en/about', + }, + ] + }, + }, +} diff --git a/test/integration/prerender/pages/lang/[lang]/about.js b/test/integration/prerender/pages/lang/[lang]/about.js new file mode 100644 index 000000000000..693a814883c7 --- /dev/null +++ b/test/integration/prerender/pages/lang/[lang]/about.js @@ -0,0 +1,12 @@ +export default ({ lang }) =>

About: {lang}

+ +export const getStaticProps = ({ params: { lang } }) => ({ + props: { + lang, + }, +}) + +export const getStaticPaths = () => ({ + paths: ['en', 'es', 'fr', 'de'].map((p) => `/lang/${p}/about`), + fallback: false, +}) diff --git a/test/integration/prerender/test/index.test.js b/test/integration/prerender/test/index.test.js index 819bae4aadea..04e89917eca1 100644 --- a/test/integration/prerender/test/index.test.js +++ b/test/integration/prerender/test/index.test.js @@ -36,6 +36,7 @@ let buildId let distPagesDir let exportDir let stderr +let origConfig const startServer = async (optEnv = {}) => { const scriptPath = join(appDir, 'server.js') @@ -130,6 +131,26 @@ const expectedManifestRoutes = () => ({ initialRevalidateSeconds: false, srcRoute: null, }, + '/lang/de/about': { + dataRoute: `/_next/data/${buildId}/lang/de/about.json`, + initialRevalidateSeconds: false, + srcRoute: '/lang/[lang]/about', + }, + '/lang/en/about': { + dataRoute: `/_next/data/${buildId}/lang/en/about.json`, + initialRevalidateSeconds: false, + srcRoute: '/lang/[lang]/about', + }, + '/lang/es/about': { + dataRoute: `/_next/data/${buildId}/lang/es/about.json`, + initialRevalidateSeconds: false, + srcRoute: '/lang/[lang]/about', + }, + '/lang/fr/about': { + dataRoute: `/_next/data/${buildId}/lang/fr/about.json`, + initialRevalidateSeconds: false, + srcRoute: '/lang/[lang]/about', + }, '/something': { dataRoute: `/_next/data/${buildId}/something.json`, initialRevalidateSeconds: false, @@ -492,6 +513,11 @@ const runTests = (dev = false, looseMode = false) => { const html = await res.text() expect(html).toMatch(/This page could not be found/) }) + + it('should allow rewriting to SSG page with fallback: false', async () => { + const html = await renderViaHTTP(appPort, '/about') + expect(html).toMatch(/About:.*?en/) + }) } if (dev) { @@ -828,6 +854,18 @@ const runTests = (dev = false, looseMode = false) => { ), page: '/default-revalidate', }, + { + namedDataRouteRegex: `^/_next/data/${escapeRegex( + buildId + )}/lang/(?[^/]+?)/about\\.json$`, + dataRouteRegex: normalizeRegEx( + `^\\/_next\\/data\\/${escapeRegex( + buildId + )}\\/lang\\/([^\\/]+?)\\/about\\.json$` + ), + page: '/lang/[lang]/about', + routeKeys: ['lang'], + }, { namedDataRouteRegex: `^/_next/data/${escapeRegex( buildId @@ -899,6 +937,14 @@ const runTests = (dev = false, looseMode = false) => { '^\\/blog\\/([^\\/]+?)\\/([^\\/]+?)(?:\\/)?$' ), }, + '/lang/[lang]/about': { + dataRoute: `/_next/data/${buildId}/lang/[lang]/about.json`, + dataRouteRegex: normalizeRegEx( + `^\\/_next\\/data\\/${escapedBuildId}\\/lang\\/([^\\/]+?)\\/about\\.json$` + ), + fallback: false, + routeRegex: normalizeRegEx('^\\/lang\\/([^\\/]+?)\\/about(?:\\/)?$'), + }, '/non-json/[p]': { dataRoute: `/_next/data/${buildId}/non-json/[p].json`, dataRouteRegex: normalizeRegEx( @@ -1039,10 +1085,9 @@ const runTests = (dev = false, looseMode = false) => { } describe('SSG Prerender', () => { - afterAll(() => fs.remove(nextConfig)) - describe('dev mode', () => { beforeAll(async () => { + origConfig = await fs.readFile(nextConfig, 'utf8') await fs.writeFile( nextConfig, ` @@ -1053,6 +1098,10 @@ describe('SSG Prerender', () => { { source: "/some-rewrite/:item", destination: "/blog/post-:item" + }, + { + source: '/about', + destination: '/lang/en/about' } ] } @@ -1069,13 +1118,17 @@ describe('SSG Prerender', () => { }) buildId = 'development' }) - afterAll(() => killApp(app)) + afterAll(async () => { + await fs.writeFile(nextConfig, origConfig) + await killApp(app) + }) runTests(true) }) describe('dev mode getStaticPaths', () => { beforeAll(async () => { + origConfig = await fs.readFile(nextConfig, 'utf8') await fs.writeFile( nextConfig, // we set cpus to 1 so that we make sure the requests @@ -1090,7 +1143,7 @@ describe('SSG Prerender', () => { }) }) afterAll(async () => { - await fs.remove(nextConfig) + await fs.writeFile(nextConfig, origConfig) await killApp(app) }) @@ -1133,6 +1186,7 @@ describe('SSG Prerender', () => { beforeAll(async () => { // remove firebase import since it breaks in legacy serverless mode origBlogPageContent = await fs.readFile(blogPagePath, 'utf8') + origConfig = await fs.readFile(nextConfig, 'utf8') await fs.writeFile( blogPagePath, @@ -1144,7 +1198,19 @@ describe('SSG Prerender', () => { await fs.writeFile( nextConfig, - `module.exports = { target: 'serverless' }`, + `module.exports = { + target: 'serverless', + experimental: { + rewrites() { + return [ + { + source: '/about', + destination: '/lang/en/about' + } + ] + } + } + }`, 'utf8' ) await fs.remove(join(appDir, '.next')) @@ -1160,6 +1226,7 @@ describe('SSG Prerender', () => { buildId = await fs.readFile(join(appDir, '.next/BUILD_ID'), 'utf8') }) afterAll(async () => { + await fs.writeFile(nextConfig, origConfig) await fs.writeFile(blogPagePath, origBlogPageContent) await killApp(app) }) @@ -1234,6 +1301,7 @@ describe('SSG Prerender', () => { return initNextServerScript(scriptPath, /ready on/i, env) } + origConfig = await fs.readFile(nextConfig, 'utf8') await fs.writeFile( nextConfig, `module.exports = { target: 'experimental-serverless-trace' }`, @@ -1249,7 +1317,10 @@ describe('SSG Prerender', () => { appPort = await findPort() app = await startServerlessEmulator(appDir, appPort, buildId) }) - afterAll(() => killApp(app)) + afterAll(async () => { + await fs.writeFile(nextConfig, origConfig) + await killApp(app) + }) runTests(false, true) }) @@ -1257,7 +1328,6 @@ describe('SSG Prerender', () => { describe('production mode', () => { let buildOutput = '' beforeAll(async () => { - await fs.remove(nextConfig) await fs.remove(join(appDir, '.next')) const { stdout } = await nextBuild(appDir, [], { stdout: true }) buildOutput = stdout @@ -1296,6 +1366,7 @@ describe('SSG Prerender', () => { beforeAll(async () => { exportDir = join(appDir, 'out') + origConfig = await fs.readFile(nextConfig, 'utf8') await fs.writeFile( nextConfig, `module.exports = { @@ -1329,8 +1400,8 @@ describe('SSG Prerender', () => { buildId = await fs.readFile(join(appDir, '.next/BUILD_ID'), 'utf8') }) afterAll(async () => { + await fs.writeFile(nextConfig, origConfig) await stopApp(app) - await fs.remove(nextConfig) for (const page of fallbackTruePages) { const pagePath = join(appDir, 'pages', page)