From f94aa048ef17bba25c1eb1609e205f25bf4e684f Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Wed, 30 Jul 2025 15:56:31 +0200 Subject: [PATCH 1/3] fix: static not-found missing in prerender manifest --- packages/next/src/build/index.ts | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/packages/next/src/build/index.ts b/packages/next/src/build/index.ts index eb9f8ccac6993..58cbe62934f77 100644 --- a/packages/next/src/build/index.ts +++ b/packages/next/src/build/index.ts @@ -3423,7 +3423,7 @@ export default async function build( return staticGenerationSpan .traceChild('move-exported-app-not-found-') .traceAsyncFn(async () => { - const orig = path.join( + const underscoreNotFoundHtml = path.join( distDir, 'server', 'app', @@ -3433,9 +3433,10 @@ export default async function build( .join('pages', '404.html') .replace(/\\/g, '/') - if (existsSync(orig)) { + // If the _not-found.html exists, use it instead of the pages 404.html + if (existsSync(underscoreNotFoundHtml)) { await fs.copyFile( - orig, + underscoreNotFoundHtml, path.join(distDir, 'server', updatedRelativeDest) ) @@ -3636,6 +3637,23 @@ export default async function build( await writeManifest(pagesManifestPath, pagesManifest) }) + const hasStaticAppRouterNotFound = existsSync( + path.join(distDir, 'server', 'app', '_not-found.html') + ) + // If the _not-found.html exists, add /_not-found to the prerender manifest + if (hasStaticAppRouterNotFound) { + prerenderManifest.routes['/_not-found'] = { + initialRevalidateSeconds: false, + initialExpireSeconds: undefined, + experimentalPPR: undefined, + renderingMode: undefined, + srcRoute: null, + dataRoute: '/_not-found.rsc', + prefetchDataRoute: undefined, + allowHeader: ALLOWED_HEADERS, + } + } + // As we may have modified the dynamicRoutes, we need to sort the // dynamic routes by page. routesManifest.dynamicRoutes = sortSortableRouteObjects( From 452b72464a459f5cc88bae43389732e49c08f913 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Wed, 30 Jul 2025 16:39:40 +0200 Subject: [PATCH 2/3] fix export and update snapshot --- packages/next/src/export/index.ts | 4 ++++ test/e2e/app-dir/app-static/app-static.test.ts | 13 +++++++++++++ 2 files changed, 17 insertions(+) diff --git a/packages/next/src/export/index.ts b/packages/next/src/export/index.ts index f007d0ffc09e8..ee48b440ed3b3 100644 --- a/packages/next/src/export/index.ts +++ b/packages/next/src/export/index.ts @@ -762,6 +762,10 @@ async function exportAppImpl( if (!options.buildExport && prerenderManifest) { await Promise.all( Object.keys(prerenderManifest.routes).map(async (unnormalizedRoute) => { + // Skip handling /_not-found route, it will copy the 404.html file later + if (unnormalizedRoute === '/_not-found') { + return + } const { srcRoute } = prerenderManifest!.routes[unnormalizedRoute] const appPageName = mapAppRouteToPage.get(srcRoute || '') const pageName = appPageName || srcRoute || unnormalizedRoute diff --git a/test/e2e/app-dir/app-static/app-static.test.ts b/test/e2e/app-dir/app-static/app-static.test.ts index 09034c8b51cbd..38f8ce780d2c8 100644 --- a/test/e2e/app-dir/app-static/app-static.test.ts +++ b/test/e2e/app-dir/app-static/app-static.test.ts @@ -987,6 +987,19 @@ describe('app-dir static/dynamic handling', () => { "initialRevalidateSeconds": false, "srcRoute": "/", }, + "/_not-found": { + "allowHeader": [ + "host", + "x-matched-path", + "x-prerender-revalidate", + "x-prerender-revalidate-if-generated", + "x-next-revalidated-tags", + "x-next-revalidate-tag-token", + ], + "dataRoute": "/_not-found.rsc", + "initialRevalidateSeconds": false, + "srcRoute": null, + }, "/api/large-data": { "allowHeader": [ "host", From d7b51e24b795b6ef3cc1e77104414f16447f9106 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Wed, 30 Jul 2025 17:01:44 +0200 Subject: [PATCH 3/3] fix tests --- packages/next/src/build/index.ts | 4 ++-- .../cache-components-dynamic-imports.test.ts | 1 + .../metadata-static-generation.test.ts | 1 + .../metadata-streaming-static-generation.test.ts | 1 + 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/next/src/build/index.ts b/packages/next/src/build/index.ts index 58cbe62934f77..31bf24877d55d 100644 --- a/packages/next/src/build/index.ts +++ b/packages/next/src/build/index.ts @@ -3648,8 +3648,8 @@ export default async function build( experimentalPPR: undefined, renderingMode: undefined, srcRoute: null, - dataRoute: '/_not-found.rsc', - prefetchDataRoute: undefined, + dataRoute: null, + prefetchDataRoute: null, allowHeader: ALLOWED_HEADERS, } } diff --git a/test/e2e/app-dir/cache-components-dynamic-imports/cache-components-dynamic-imports.test.ts b/test/e2e/app-dir/cache-components-dynamic-imports/cache-components-dynamic-imports.test.ts index 9dae3d2cb18a8..7b96136984f61 100644 --- a/test/e2e/app-dir/cache-components-dynamic-imports/cache-components-dynamic-imports.test.ts +++ b/test/e2e/app-dir/cache-components-dynamic-imports/cache-components-dynamic-imports.test.ts @@ -39,6 +39,7 @@ describe('async imports in cacheComponents', () => { expect(prerenderedRoutes).toMatchInlineSnapshot(` [ + "/_not-found", "/inside-render/client/async-module", "/inside-render/client/sync-module", "/inside-render/route-handler/async-module", diff --git a/test/e2e/app-dir/metadata-static-generation/metadata-static-generation.test.ts b/test/e2e/app-dir/metadata-static-generation/metadata-static-generation.test.ts index e6765a7c12d77..172ea1df31f19 100644 --- a/test/e2e/app-dir/metadata-static-generation/metadata-static-generation.test.ts +++ b/test/e2e/app-dir/metadata-static-generation/metadata-static-generation.test.ts @@ -24,6 +24,7 @@ const isPPREnabled = process.env.__NEXT_EXPERIMENTAL_PPR === 'true' const staticRoutes = prerenderManifest.routes expect(Object.keys(staticRoutes).sort()).toEqual([ '/', + '_not-found', '/suspenseful/static', ]) }) diff --git a/test/e2e/app-dir/metadata-streaming-static-generation/metadata-streaming-static-generation.test.ts b/test/e2e/app-dir/metadata-streaming-static-generation/metadata-streaming-static-generation.test.ts index 343e1e3b21d85..c535c3774129b 100644 --- a/test/e2e/app-dir/metadata-streaming-static-generation/metadata-streaming-static-generation.test.ts +++ b/test/e2e/app-dir/metadata-streaming-static-generation/metadata-streaming-static-generation.test.ts @@ -22,6 +22,7 @@ const isPPREnabled = process.env.__NEXT_EXPERIMENTAL_PPR === 'true' const staticRoutes = prerenderManifest.routes expect(Object.keys(staticRoutes).sort()).toEqual([ '/', + '_not-found', '/slow/static', '/suspenseful/static', ])