diff --git a/packages/next/next-server/server/next-server.ts b/packages/next/next-server/server/next-server.ts
index 5f9fbd23fb652..927071d470b2e 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 0000000000000..edfaf6892fe20
--- /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 0000000000000..693a814883c73
--- /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 819bae4aadeae..04e89917eca15 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)