From f6d525847e4f41d243c7353685b16d772b8f5f43 Mon Sep 17 00:00:00 2001
From: Zack Tanner <1939140+ztanner@users.noreply.github.com>
Date: Mon, 7 Apr 2025 10:22:25 -0700
Subject: [PATCH] fix: not-found from app router should work & be prioritized
when pages i18n is used
---
packages/next/src/build/index.ts | 10 +++++
.../app/app-dir/[[...slug]]/page.tsx | 44 +++++++++++++++++++
.../not-found-with-pages-i18n/app/layout.tsx | 16 +++++++
.../app/not-found.tsx | 10 +++++
.../not-found-with-pages-i18n/next.config.js | 12 +++++
.../not-found-with-pages.test.ts | 31 +++++++++++++
.../not-found-with-pages-i18n/pages/404.tsx | 24 ++++++++++
.../pages/[[...slug]].tsx | 39 ++++++++++++++++
8 files changed, 186 insertions(+)
create mode 100644 test/e2e/app-dir/not-found-with-pages-i18n/app/app-dir/[[...slug]]/page.tsx
create mode 100644 test/e2e/app-dir/not-found-with-pages-i18n/app/layout.tsx
create mode 100644 test/e2e/app-dir/not-found-with-pages-i18n/app/not-found.tsx
create mode 100644 test/e2e/app-dir/not-found-with-pages-i18n/next.config.js
create mode 100644 test/e2e/app-dir/not-found-with-pages-i18n/not-found-with-pages.test.ts
create mode 100644 test/e2e/app-dir/not-found-with-pages-i18n/pages/404.tsx
create mode 100644 test/e2e/app-dir/not-found-with-pages-i18n/pages/[[...slug]].tsx
diff --git a/packages/next/src/build/index.ts b/packages/next/src/build/index.ts
index 9d85556ab49c0..f7cbf822fff76 100644
--- a/packages/next/src/build/index.ts
+++ b/packages/next/src/build/index.ts
@@ -3301,6 +3301,16 @@ export default async function build(
orig,
path.join(distDir, 'server', updatedRelativeDest)
)
+
+ // since the app router not found is prioritized over pages router,
+ // we have to ensure the app router entries are available for all locales
+ if (i18n) {
+ for (const locale of i18n.locales) {
+ const curPath = `/${locale}/404`
+ pagesManifest[curPath] = updatedRelativeDest
+ }
+ }
+
pagesManifest['/404'] = updatedRelativeDest
}
})
diff --git a/test/e2e/app-dir/not-found-with-pages-i18n/app/app-dir/[[...slug]]/page.tsx b/test/e2e/app-dir/not-found-with-pages-i18n/app/app-dir/[[...slug]]/page.tsx
new file mode 100644
index 0000000000000..84e51b866c775
--- /dev/null
+++ b/test/e2e/app-dir/not-found-with-pages-i18n/app/app-dir/[[...slug]]/page.tsx
@@ -0,0 +1,44 @@
+import { notFound } from 'next/navigation'
+
+export async function generateStaticParams() {
+ return []
+}
+
+async function validateSlug(slug: string[]) {
+ try {
+ const isValidPath =
+ slug.length === 1 && (slug[0] === 'about' || slug[0] === 'contact')
+
+ if (!isValidPath) {
+ return false
+ }
+
+ return true
+ } catch (error) {
+ throw error
+ }
+}
+
+export default async function CatchAll({
+ params,
+}: {
+ params: Promise<{ slug: string[] }>
+}) {
+ const { slug } = await params
+ const slugArray = Array.isArray(slug) ? slug : [slug]
+
+ // Validate the slug
+ const isValid = await validateSlug(slugArray)
+
+ // If not valid, show 404
+ if (!isValid) {
+ notFound()
+ }
+
+ return (
+
+
Catch All
+
This is a catch all page added to the APP router
+
+ )
+}
diff --git a/test/e2e/app-dir/not-found-with-pages-i18n/app/layout.tsx b/test/e2e/app-dir/not-found-with-pages-i18n/app/layout.tsx
new file mode 100644
index 0000000000000..a14e64fcd5e33
--- /dev/null
+++ b/test/e2e/app-dir/not-found-with-pages-i18n/app/layout.tsx
@@ -0,0 +1,16 @@
+export const metadata = {
+ title: 'Next.js',
+ description: 'Generated by Next.js',
+}
+
+export default function RootLayout({
+ children,
+}: {
+ children: React.ReactNode
+}) {
+ return (
+
+ {children}
+
+ )
+}
diff --git a/test/e2e/app-dir/not-found-with-pages-i18n/app/not-found.tsx b/test/e2e/app-dir/not-found-with-pages-i18n/app/not-found.tsx
new file mode 100644
index 0000000000000..9e2f20f4cca7c
--- /dev/null
+++ b/test/e2e/app-dir/not-found-with-pages-i18n/app/not-found.tsx
@@ -0,0 +1,10 @@
+import React from 'react'
+
+const NotFound = () => (
+
+
APP ROUTER - 404 PAGE
+
This page is using the APP ROUTER
+
+)
+
+export default NotFound
diff --git a/test/e2e/app-dir/not-found-with-pages-i18n/next.config.js b/test/e2e/app-dir/not-found-with-pages-i18n/next.config.js
new file mode 100644
index 0000000000000..ee7663cef2d2b
--- /dev/null
+++ b/test/e2e/app-dir/not-found-with-pages-i18n/next.config.js
@@ -0,0 +1,12 @@
+/**
+ * @type {import('next').NextConfig}
+ */
+const nextConfig = {
+ i18n: {
+ locales: ['en-GB', 'en'],
+ defaultLocale: 'en',
+ localeDetection: false,
+ },
+}
+
+module.exports = nextConfig
diff --git a/test/e2e/app-dir/not-found-with-pages-i18n/not-found-with-pages.test.ts b/test/e2e/app-dir/not-found-with-pages-i18n/not-found-with-pages.test.ts
new file mode 100644
index 0000000000000..5b6a635bd3d6b
--- /dev/null
+++ b/test/e2e/app-dir/not-found-with-pages-i18n/not-found-with-pages.test.ts
@@ -0,0 +1,31 @@
+import { nextTestSetup } from 'e2e-utils'
+
+describe('not-found-with-pages', () => {
+ const { next, isNextStart } = nextTestSetup({
+ files: __dirname,
+ })
+
+ if (isNextStart) {
+ it('should write all locales to the pages manifest', async () => {
+ const pagesManifest = JSON.parse(
+ await next.readFile('.next/server/pages-manifest.json')
+ )
+
+ expect(pagesManifest['/404']).toBe('pages/404.html')
+ expect(pagesManifest['/en/404']).toBe('pages/404.html')
+ expect(pagesManifest['/en-GB/404']).toBe('pages/404.html')
+ })
+ }
+
+ it('should prefer the app router 404 over the pages router 404 when both are present', async () => {
+ const browser = await next.browser('/app-dir/foo')
+ expect(await browser.elementByCss('h1').text()).toBe(
+ 'APP ROUTER - 404 PAGE'
+ )
+
+ await browser.loadPage(next.url)
+ expect(await browser.elementByCss('h1').text()).toBe(
+ 'APP ROUTER - 404 PAGE'
+ )
+ })
+})
diff --git a/test/e2e/app-dir/not-found-with-pages-i18n/pages/404.tsx b/test/e2e/app-dir/not-found-with-pages-i18n/pages/404.tsx
new file mode 100644
index 0000000000000..be5a6a9368ef2
--- /dev/null
+++ b/test/e2e/app-dir/not-found-with-pages-i18n/pages/404.tsx
@@ -0,0 +1,24 @@
+import React from 'react'
+import { GetStaticProps } from 'next'
+
+interface NotFoundProps {
+ message: string
+}
+
+const NotFound = ({ message }: NotFoundProps) => (
+
+
PAGES ROUTER - 404 PAGE
+
This page is using the PAGES ROUTER
+
{message}
+
+)
+
+export const getStaticProps: GetStaticProps = async () => {
+ return {
+ props: {
+ message: 'Custom message fetched at build time',
+ },
+ }
+}
+
+export default NotFound
diff --git a/test/e2e/app-dir/not-found-with-pages-i18n/pages/[[...slug]].tsx b/test/e2e/app-dir/not-found-with-pages-i18n/pages/[[...slug]].tsx
new file mode 100644
index 0000000000000..2da5fd5f318d5
--- /dev/null
+++ b/test/e2e/app-dir/not-found-with-pages-i18n/pages/[[...slug]].tsx
@@ -0,0 +1,39 @@
+import React from 'react'
+
+export const getStaticProps = ({ params }: { params: { slug: string[] } }) => {
+ try {
+ const slugArray = Array.isArray(params.slug) ? params.slug : [params.slug]
+
+ const isValidPath =
+ slugArray.length === 1 &&
+ (slugArray[0] === 'about' || slugArray[0] === 'contact')
+
+ if (!isValidPath) {
+ return {
+ notFound: true,
+ }
+ }
+
+ return {
+ props: {
+ slug: params.slug,
+ },
+ }
+ } catch (error) {
+ throw error
+ }
+}
+
+export const getStaticPaths = async () => ({
+ paths: [],
+ fallback: 'blocking',
+})
+
+const CatchAll = () => (
+
+
Catch All
+
This is a catch all page added to the pages router
+
+)
+
+export default CatchAll