Files
next.js/test/e2e/i18n-beforefiles-rewrite/i18n-beforefiles-rewrite.test.ts
Tim NeutkensandGitHub c7a5a0954d Revert "Revert "refactor: write pages router export files directly to server/pages/"" (#93409)
Re-apply #92735

Before re-landing we need to investigate why the pages router middleware
i18n test fails when deployed.

Update: Investigation/fix:
https://github.com/vercel/next.js/pull/92735#issuecomment-4242978559

Reverts vercel/next.js#92733

---

Cursor plan based on the investigation Claude did:

# Fix i18n export orphan HTML files in server/pages/

## Problem

When i18n is configured, the export path map in `build/index.ts` creates
locale-prefixed entries for every page (e.g., `/en/home/a`,
`/fr/home/a`), but only deletes the non-locale entry from `defaultMap`
for SSG pages. For non-SSG static pages, the non-locale entry (e.g.,
`/home/a`) remains, causing the export worker to render and write orphan
HTML files directly to `server/pages/`.

These orphan files interfere with routing on deployment platforms (e.g.,
a `[teamId]/[slug].html` orphan matches rewritten paths that should
404).

## Root Cause

In [packages/next/src/build/index.ts](packages/next/src/build/index.ts)
at line 2988:

```2988:2991:packages/next/src/build/index.ts
                  if (isSsg) {
                    // remove non-locale prefixed variant from defaultMap
                    delete defaultMap[page]
                  }
```

The `if (isSsg)` guard means non-SSG pages keep their non-locale entry,
which gets rendered and written to `server/pages/` as an orphan file.

## Fix

**Single-line change** in
[packages/next/src/build/index.ts](packages/next/src/build/index.ts):
Remove the `if (isSsg)` guard so `delete defaultMap[page]` always runs
when i18n is enabled.

The locale-prefixed entries are already created in the loop above (line
2981), and they capture the original `page` value via
`defaultMap[page]?.page || page`. The non-locale entry is not needed
after that -- in the old staging-directory flow (`moveExportedPage`), it
was rendered but never moved to the final output directory.

**Safety verification:**

- `updatePagesManifestForExportedPage` (line 3754) is called based on
`combinedPages`, not the export map -- it still correctly replaces
non-locale manifest entries with locale-prefixed ones
- `pageDuration` tracking (line 3745) may return `undefined` for the
non-locale path, but this is typed `number | undefined` and used with
`|| 0` fallback at line 375 of `build/utils.ts` -- purely cosmetic
- SSG pages already have this exact behavior (delete from defaultMap),
so this change makes non-SSG consistent with SSG
- The `ssgPageDurations` invariant check (line 3737-3738) only runs for
SSG pages, which are unaffected

## Test

Add assertions to the existing i18n integration test at
[test/integration/i18n-support/test/index.test.ts](test/integration/i18n-support/test/index.test.ts)
to verify that non-locale-prefixed HTML orphans do not exist in
`server/pages/` after build. The test already has production mode
assertions checking locale-prefixed file existence and manifest
correctness (lines 81-99).
2026-05-26 16:50:38 +02:00

42 lines
1.4 KiB
TypeScript

import { nextTestSetup, isNextDev } from 'e2e-utils'
;(isNextDev ? describe.skip : describe)('i18n-beforefiles-rewrite', () => {
const { next } = nextTestSetup({
files: __dirname,
})
const locales = ['en', 'fr']
const autoStaticRoutes = [
{ pathname: '', content: 'index' },
{ pathname: '/home/a', content: 'Home A' },
{ pathname: '/home/b', content: 'Home B' },
{ pathname: '/dynamic/static', content: 'static route' },
{ pathname: '/dynamic/first', content: 'dynamic route' },
{ pathname: '/team/slug', content: '/[teamId]/[slug]' },
]
it('should serve locale-prefixed auto-static pages', async () => {
for (const { pathname, content } of autoStaticRoutes) {
for (const locale of locales) {
const url = `/${locale}${pathname}`
const res = await next.fetch(url)
const html = await res.text()
expect({ url, status: res.status }).toEqual({ url, status: 200 })
expect(html).toContain(content)
}
}
})
it.each(['', '/en', '/fr'])(
'should 404 for %s/rewrite-before-files',
async (localePrefix) => {
// beforeFiles rewrites /rewrite-before-files to /somewhere.
// /somewhere does not match any page, so this should 404.
// Before the fix, orphan dynamic HTML output could incorrectly match.
const res = await next.fetch(`${localePrefix}/rewrite-before-files`)
expect(res.status).toBe(404)
}
)
})