Skip to content

fix: ensure app router not found works when deployed with pages i18n config #77905

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions packages/next/src/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
})
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<div>
<h1>Catch All</h1>
<p>This is a catch all page added to the APP router</p>
</div>
)
}
16 changes: 16 additions & 0 deletions test/e2e/app-dir/not-found-with-pages-i18n/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export const metadata = {
title: 'Next.js',
description: 'Generated by Next.js',
}

export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}
10 changes: 10 additions & 0 deletions test/e2e/app-dir/not-found-with-pages-i18n/app/not-found.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import React from 'react'

const NotFound = () => (
<div>
<h1>APP ROUTER - 404 PAGE</h1>
<p>This page is using the APP ROUTER</p>
</div>
)

export default NotFound
12 changes: 12 additions & 0 deletions test/e2e/app-dir/not-found-with-pages-i18n/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {
i18n: {
locales: ['en-GB', 'en'],
defaultLocale: 'en',
localeDetection: false,
},
}

module.exports = nextConfig
Original file line number Diff line number Diff line change
@@ -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'
)
})
})
24 changes: 24 additions & 0 deletions test/e2e/app-dir/not-found-with-pages-i18n/pages/404.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from 'react'
import { GetStaticProps } from 'next'

interface NotFoundProps {
message: string
}

const NotFound = ({ message }: NotFoundProps) => (
<div>
<h1>PAGES ROUTER - 404 PAGE</h1>
<p>This page is using the PAGES ROUTER</p>
<p>{message}</p>
</div>
)

export const getStaticProps: GetStaticProps<NotFoundProps> = async () => {
return {
props: {
message: 'Custom message fetched at build time',
},
}
}

export default NotFound
39 changes: 39 additions & 0 deletions test/e2e/app-dir/not-found-with-pages-i18n/pages/[[...slug]].tsx
Original file line number Diff line number Diff line change
@@ -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 = () => (
<div>
<h1>Catch All</h1>
<p>This is a catch all page added to the pages router</p>
</div>
)

export default CatchAll
Loading