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