diff --git a/docs/01-app/03-api-reference/07-adapters/03-api-reference.mdx b/docs/01-app/03-api-reference/07-adapters/03-api-reference.mdx index b1a4b5516fa7..925e3b10d2d2 100644 --- a/docs/01-app/03-api-reference/07-adapters/03-api-reference.mdx +++ b/docs/01-app/03-api-reference/07-adapters/03-api-reference.mdx @@ -23,6 +23,7 @@ Called after the build process completes with detailed information about routes **Parameters:** - `context.routing`: Object containing Next.js routing phases and metadata + - `routing.version`: Version of the routing contract. Version `2` keeps dynamic Pages API routes in the canonical `/api` namespace. Manifests without a version use the legacy version `1` semantics. - `routing.beforeMiddleware`: Routes executed before middleware (includes header and redirect handling) - `routing.middlewareMatchers`: Middleware matcher definitions for this build, used to decide whether middleware should be invoked for a given request - `routing.beforeFiles`: Rewrite routes checked before filesystem route matching diff --git a/packages/next/src/build/adapter/build-complete.ts b/packages/next/src/build/adapter/build-complete.ts index ec5af1cdd40a..617a77631fba 100644 --- a/packages/next/src/build/adapter/build-complete.ts +++ b/packages/next/src/build/adapter/build-complete.ts @@ -9,6 +9,7 @@ import { interopDefault } from '../../lib/interop-default' import type { RouteHas } from '../../lib/load-custom-routes' import { recursiveReadDir } from '../../lib/recursive-readdir' import { isDynamicRoute } from '../../shared/lib/router/utils' +import { isAPIRoute } from '../../lib/is-api-route' import type { Revalidate } from '../../server/lib/cache-control' import type { NextConfigComplete } from '../../server/config-shared' import { @@ -520,6 +521,11 @@ export interface NextAdapter { ) => Promise | NextConfigComplete onBuildComplete?: (ctx: { routing: { + /** + * Version of the routing manifest contract. Unversioned manifests use + * the legacy version 1 semantics. + */ + version: 2 beforeMiddleware: Array /** * middlewareMatchers are the middleware matcher definitions emitted by @@ -2125,7 +2131,7 @@ export async function handleBuildComplete({ route.page ) - const shouldLocalize = Boolean(config.i18n) + const shouldLocalize = Boolean(config.i18n && !isAPIRoute(route.page)) const routeRegex = getNamedRouteRegex(route.page, { prefixRouteKeys: true, @@ -2423,6 +2429,7 @@ export async function handleBuildComplete({ await adapterMod.onBuildComplete({ routing: { + version: 2, beforeMiddleware: [...headers, ...redirects], middlewareMatchers: outputs.middleware?.config.matchers?.map((matcher) => ({ diff --git a/test/production/adapter-config-i18n/adapter-config-i18n.test.ts b/test/production/adapter-config-i18n/adapter-config-i18n.test.ts index 8341ac5725ea..a4a719bc311c 100644 --- a/test/production/adapter-config-i18n/adapter-config-i18n.test.ts +++ b/test/production/adapter-config-i18n/adapter-config-i18n.test.ts @@ -6,11 +6,7 @@ describe('adapter config with i18n routes', () => { files: __dirname, }) - // Dynamic Pages API routes are localized like any other dynamic route. The - // Vercel adapter's i18n rules prefix the locale onto `/api/...` too, so the - // matcher has to expect it — if the two disagree the matcher is unreachable - // and requests fall through to `/{locale}/404`. See vercel/next.js#96935. - it('localizes dynamic Pages API routes', async () => { + it('keeps dynamic Pages API routes in the canonical namespace', async () => { const { outputs, routing }: Parameters[0] = await next.readJSON('build-complete.json') @@ -24,11 +20,12 @@ describe('adapter config with i18n routes', () => { (route) => route.source === '/blog/[slug]' ) + expect(routing.version).toBe(2) expect(apiOutput).toBeDefined() expect(apiRoute).toBeDefined() - expect(apiRoute?.sourceRegex).toContain('nextLocale') + expect(apiRoute?.sourceRegex).not.toContain('nextLocale') expect(apiRoute?.destination).toBe( - '/$nextLocale/api/proxy/[[...slug]]?nxtPslug=$nxtPslug' + '/api/proxy/[[...slug]]?nxtPslug=$nxtPslug' ) expect(pageRoute).toBeDefined() diff --git a/test/production/adapter-config/adapter-config-export.test.ts b/test/production/adapter-config/adapter-config-export.test.ts index 69be96ba4e53..263ec99ea659 100644 --- a/test/production/adapter-config/adapter-config-export.test.ts +++ b/test/production/adapter-config/adapter-config-export.test.ts @@ -83,6 +83,7 @@ describe('adapter-config export', () => { } expect(routing).toEqual({ + version: 2, beforeMiddleware: expect.toBeArray(), middlewareMatchers: expect.toBeArray(), beforeFiles: expect.toBeArray(), diff --git a/test/production/adapter-config/adapter-config.test.ts b/test/production/adapter-config/adapter-config.test.ts index 27253300a6e3..f4d5e0531017 100644 --- a/test/production/adapter-config/adapter-config.test.ts +++ b/test/production/adapter-config/adapter-config.test.ts @@ -451,6 +451,7 @@ describe('adapter-config', () => { } expect(routing).toEqual({ + version: 2, beforeMiddleware: expect.toBeArray(), middlewareMatchers: expect.toBeArray(), beforeFiles: expect.toBeArray(),