App router 404 localization (#57230)
This commit is contained in:
@@ -343,3 +343,9 @@ cookbook_landing:
|
||||
search_articles: Search articles
|
||||
category: Category
|
||||
complexity: Complexity
|
||||
|
||||
not_found:
|
||||
title: Ooops!
|
||||
message: It looks like this page doesn't exist.
|
||||
contact: We track these errors automatically, but if the problem persists please feel free to contact us.
|
||||
contact_cta: Contact support
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { getAppRouterContext } from '@/app/lib/app-router-context'
|
||||
import { AppRouterMainContextProvider } from '@/app/components/AppRouterMainContext'
|
||||
import { translate } from '@/languages/lib/translation-utils'
|
||||
import { CommentDiscussionIcon, MarkGithubIcon } from '@primer/octicons-react'
|
||||
import { Client404Wrapper } from '@/app/components/Client404Wrapper'
|
||||
import { createServerAppRouterContext } from '@/app/lib/server-context-utils'
|
||||
import { headers } from 'next/headers'
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
@@ -12,94 +11,10 @@ export const metadata: Metadata = {
|
||||
}
|
||||
|
||||
export default async function Page404() {
|
||||
// Get context with UI data
|
||||
const appContext = await getAppRouterContext()
|
||||
const headersList = await headers()
|
||||
const pathname = headersList.get('x-pathname') || '/404'
|
||||
|
||||
const siteTitle = translate(appContext.site.data.ui, 'header.github_docs', 'GitHub Docs')
|
||||
const oopsTitle = translate(appContext.site.data.ui, 'meta.oops', 'Ooops!')
|
||||
const appContext = createServerAppRouterContext(pathname)
|
||||
|
||||
return (
|
||||
<AppRouterMainContextProvider context={appContext}>
|
||||
<div className="min-h-screen d-flex flex-column">
|
||||
{/* Simple Header */}
|
||||
<div className="border-bottom color-border-muted no-print">
|
||||
<header className="container-xl p-responsive py-3 position-relative d-flex width-full">
|
||||
<div className="d-flex flex-1 flex-items-center">
|
||||
<a
|
||||
href={`/${appContext.currentLanguage}`}
|
||||
className="color-fg-default no-underline d-flex flex-items-center"
|
||||
>
|
||||
<MarkGithubIcon size={32} className="mr-2" />
|
||||
<span className="f4 text-bold">{siteTitle}</span>
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="container-xl p-responsive py-6 width-full flex-1">
|
||||
<article className="col-md-10 col-lg-7 mx-auto">
|
||||
<h1>{oopsTitle}</h1>
|
||||
<div className="f2 color-fg-muted mb-3" data-container="lead">
|
||||
It looks like this page doesn't exist.
|
||||
</div>
|
||||
<p className="f3">
|
||||
We track these errors automatically, but if the problem persists please feel free to
|
||||
contact us.
|
||||
</p>
|
||||
<a id="support" href="https://support.github.com" className="btn btn-outline mt-2">
|
||||
<CommentDiscussionIcon size="small" className="octicon mr-1" />
|
||||
Contact support
|
||||
</a>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<footer className="py-6">
|
||||
<div className="container-xl px-3 px-md-6">
|
||||
<ul className="d-flex flex-wrap list-style-none">
|
||||
<li className="d-flex mr-xl-3 color-fg-muted">
|
||||
<span>© 2025 GitHub, Inc.</span>
|
||||
</li>
|
||||
<li className="ml-3">
|
||||
<a
|
||||
className="text-underline"
|
||||
href="/site-policy/github-terms/github-terms-of-service"
|
||||
>
|
||||
Terms
|
||||
</a>
|
||||
</li>
|
||||
<li className="ml-3">
|
||||
<a
|
||||
className="text-underline"
|
||||
href="/site-policy/privacy-policies/github-privacy-statement"
|
||||
>
|
||||
Privacy
|
||||
</a>
|
||||
</li>
|
||||
<li className="ml-3">
|
||||
<a className="text-underline" href="https://www.githubstatus.com/">
|
||||
Status
|
||||
</a>
|
||||
</li>
|
||||
<li className="ml-3">
|
||||
<a className="text-underline" href="https://github.com/pricing">
|
||||
Pricing
|
||||
</a>
|
||||
</li>
|
||||
<li className="ml-3">
|
||||
<a className="text-underline" href="https://services.github.com/">
|
||||
Expert services
|
||||
</a>
|
||||
</li>
|
||||
<li className="ml-3">
|
||||
<a className="text-underline" href="https://github.blog/">
|
||||
Blog
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</AppRouterMainContextProvider>
|
||||
)
|
||||
return <Client404Wrapper appContext={appContext} />
|
||||
}
|
||||
|
||||
83
src/app/components/AppRouterFooter.tsx
Normal file
83
src/app/components/AppRouterFooter.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
'use client'
|
||||
|
||||
import { useAppRouterMainContext } from '@/app/components/AppRouterMainContext'
|
||||
import { createTranslationFunctions } from '@/languages/lib/translation-utils'
|
||||
import { LinkExternalIcon } from '@primer/octicons-react'
|
||||
|
||||
export function AppRouterFooter() {
|
||||
const context = useAppRouterMainContext()
|
||||
|
||||
const { t } = createTranslationFunctions(context.site.data.ui, 'footer')
|
||||
|
||||
return (
|
||||
<section className="container-xl px-3 mt-6 pb-8 px-md-6 color-fg-muted">
|
||||
{context.currentLanguage !== 'en' && <h2 className="f4 mb-2 col-12">{t('legal_heading')}</h2>}
|
||||
|
||||
{/* Machine translation notice for non-English languages */}
|
||||
{context.currentLanguage !== 'en' && <p>{t('machine')}</p>}
|
||||
|
||||
<ul className="d-flex flex-wrap list-style-none">
|
||||
<li className="mr-3">© {new Date().getFullYear()} GitHub, Inc.</li>
|
||||
|
||||
{/* German-specific Impressum link (legally required) */}
|
||||
{context.currentLanguage === 'de' && (
|
||||
<li className="mr-3">
|
||||
<a
|
||||
className="text-underline"
|
||||
href="https://aka.ms/impressum_de"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
{t('imprint')}
|
||||
</a>
|
||||
<LinkExternalIcon aria-label="(external site)" size={12} />
|
||||
</li>
|
||||
)}
|
||||
|
||||
<li className="mr-3">
|
||||
<a
|
||||
className="text-underline"
|
||||
href={`/${context.currentLanguage}/site-policy/github-terms/github-terms-of-service`}
|
||||
>
|
||||
{t('terms')}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li className="mr-3">
|
||||
<a
|
||||
className={`text-underline ${
|
||||
context.currentLanguage === 'ko' ? 'color-fg-attention text-bold' : ''
|
||||
}`}
|
||||
href={`/${context.currentLanguage}/site-policy/privacy-policies/github-privacy-statement`}
|
||||
>
|
||||
{t('privacy')}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li className="mr-3">
|
||||
<a className="text-underline" href="https://www.githubstatus.com/">
|
||||
{t('status')}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li className="mr-3">
|
||||
<a className="text-underline" href="https://github.com/pricing">
|
||||
{t('pricing')}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li className="mr-3">
|
||||
<a className="text-underline" href="https://services.github.com">
|
||||
{t('expert_services')}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li className="mr-3">
|
||||
<a className="text-underline" href="https://github.blog">
|
||||
{t('blog')}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
29
src/app/components/AppRouterHeader.tsx
Normal file
29
src/app/components/AppRouterHeader.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
'use client'
|
||||
|
||||
import { MarkGithubIcon } from '@primer/octicons-react'
|
||||
import { useAppRouterMainContext } from '@/app/components/AppRouterMainContext'
|
||||
import { createTranslationFunctions } from '@/languages/lib/translation-utils'
|
||||
|
||||
export function AppRouterHeader() {
|
||||
const context = useAppRouterMainContext()
|
||||
|
||||
const { t } = createTranslationFunctions(context.site.data.ui, 'header')
|
||||
|
||||
const siteTitle = t('github_docs')
|
||||
|
||||
return (
|
||||
<div className="border-bottom color-border-muted no-print">
|
||||
<header className="container-xl p-responsive py-3 position-relative d-flex width-full">
|
||||
<div className="d-flex flex-1 flex-items-center">
|
||||
<a
|
||||
href={`/${context.currentLanguage}`}
|
||||
className="color-fg-default no-underline d-flex flex-items-center"
|
||||
>
|
||||
<MarkGithubIcon size={32} className="mr-2" />
|
||||
<span className="f4 text-bold">{siteTitle}</span>
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
54
src/app/components/AppRouterLanguagesContext.tsx
Normal file
54
src/app/components/AppRouterLanguagesContext.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
'use client'
|
||||
|
||||
import { createContext, useContext } from 'react'
|
||||
import { clientLanguages, type ClientLanguageCode } from '@/languages/lib/client-languages'
|
||||
|
||||
export type AppRouterLanguageItem = {
|
||||
name: string
|
||||
nativeName?: string
|
||||
code: string
|
||||
hreflang?: string
|
||||
}
|
||||
|
||||
export type AppRouterLanguagesContextT = {
|
||||
languages: Record<string, AppRouterLanguageItem>
|
||||
currentLanguage?: ClientLanguageCode
|
||||
}
|
||||
|
||||
export const AppRouterLanguagesContext = createContext<AppRouterLanguagesContextT | null>(null)
|
||||
|
||||
export const useAppRouterLanguages = (): AppRouterLanguagesContextT => {
|
||||
const context = useContext(AppRouterLanguagesContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'"useAppRouterLanguages" may only be used inside "AppRouterLanguagesContext.Provider"',
|
||||
)
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
/**
|
||||
* Provider component for App Router language context
|
||||
*/
|
||||
interface AppRouterLanguagesProviderProps {
|
||||
children: React.ReactNode
|
||||
currentLanguage?: ClientLanguageCode
|
||||
}
|
||||
|
||||
export function AppRouterLanguagesProvider({
|
||||
children,
|
||||
currentLanguage,
|
||||
}: AppRouterLanguagesProviderProps) {
|
||||
const value: AppRouterLanguagesContextT = {
|
||||
languages: clientLanguages,
|
||||
currentLanguage,
|
||||
}
|
||||
|
||||
return (
|
||||
<AppRouterLanguagesContext.Provider value={value}>
|
||||
{children}
|
||||
</AppRouterLanguagesContext.Provider>
|
||||
)
|
||||
}
|
||||
41
src/app/components/Client404Wrapper.tsx
Normal file
41
src/app/components/Client404Wrapper.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
'use client'
|
||||
|
||||
import { AppRouterFooter } from '@/app/components/AppRouterFooter'
|
||||
import { AppRouterHeader } from '@/app/components/AppRouterHeader'
|
||||
import { AppRouterLanguagesProvider } from '@/app/components/AppRouterLanguagesContext'
|
||||
import { AppRouterMainContextProvider } from '@/app/components/AppRouterMainContext'
|
||||
import type { ServerAppRouterContext } from '@/app/lib/server-context-utils'
|
||||
import { createTranslationFunctions } from '@/languages/lib/translation-utils'
|
||||
import { CommentDiscussionIcon } from '@primer/octicons-react'
|
||||
|
||||
interface Client404WrapperProps {
|
||||
appContext: ServerAppRouterContext
|
||||
}
|
||||
|
||||
export function Client404Wrapper({ appContext }: Client404WrapperProps) {
|
||||
const { t } = createTranslationFunctions(appContext.site.data.ui, 'not_found')
|
||||
return (
|
||||
<AppRouterLanguagesProvider currentLanguage={appContext.currentLanguage}>
|
||||
<AppRouterMainContextProvider context={appContext}>
|
||||
<div className="min-h-screen d-flex flex-column">
|
||||
<AppRouterHeader />
|
||||
{/* Main Content */}
|
||||
<div className="container-xl p-responsive py-6 width-full flex-1">
|
||||
<article className="col-md-10 col-lg-7 mx-auto">
|
||||
<h1>{t('title')}</h1>
|
||||
<div className="f2 color-fg-muted mb-3" data-container="lead">
|
||||
{t('message')}
|
||||
</div>
|
||||
<p className="f3">{t('contact')}</p>
|
||||
<a id="support" href="https://support.github.com" className="btn btn-outline mt-2">
|
||||
<CommentDiscussionIcon size="small" className="octicon mr-1" />
|
||||
{t('contact_cta')}
|
||||
</a>
|
||||
</article>
|
||||
</div>
|
||||
<AppRouterFooter />
|
||||
</div>
|
||||
</AppRouterMainContextProvider>
|
||||
</AppRouterLanguagesProvider>
|
||||
)
|
||||
}
|
||||
20
src/app/components/ClientNotFoundWrapper.tsx
Normal file
20
src/app/components/ClientNotFoundWrapper.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
'use client'
|
||||
|
||||
import { AppRouterMainContextProvider } from '@/app/components/AppRouterMainContext'
|
||||
import { AppRouterLanguagesProvider } from '@/app/components/AppRouterLanguagesContext'
|
||||
import { NotFoundContent } from '@/app/components/NotFoundContent'
|
||||
import type { ServerAppRouterContext } from '@/app/lib/server-context-utils'
|
||||
|
||||
interface ClientNotFoundWrapperProps {
|
||||
appContext: ServerAppRouterContext
|
||||
}
|
||||
|
||||
export function ClientNotFoundWrapper({ appContext }: ClientNotFoundWrapperProps) {
|
||||
return (
|
||||
<AppRouterLanguagesProvider currentLanguage={appContext.currentLanguage}>
|
||||
<AppRouterMainContextProvider context={appContext}>
|
||||
<NotFoundContent />
|
||||
</AppRouterMainContextProvider>
|
||||
</AppRouterLanguagesProvider>
|
||||
)
|
||||
}
|
||||
@@ -1,119 +1,33 @@
|
||||
'use client'
|
||||
import { AppRouterHeader } from '@/app/components/AppRouterHeader'
|
||||
import { useAppRouterMainContext } from '@/app/components/AppRouterMainContext'
|
||||
import { ServerFooter } from '@/app/components/ServerFooter'
|
||||
import { createTranslationFunctions } from '@/languages/lib/translation-utils'
|
||||
import { CommentDiscussionIcon, MarkGithubIcon } from '@primer/octicons-react'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
function SimpleHeader() {
|
||||
const context = useAppRouterMainContext()
|
||||
|
||||
const { t } = useMemo(
|
||||
() => createTranslationFunctions(context.site.data.ui, ['header']),
|
||||
[context.site.data.ui],
|
||||
)
|
||||
|
||||
const siteTitle = t('github_docs')
|
||||
|
||||
return (
|
||||
<div className="border-bottom color-border-muted no-print">
|
||||
<header className="container-xl p-responsive py-3 position-relative d-flex width-full">
|
||||
<div className="d-flex flex-1 flex-items-center">
|
||||
<a
|
||||
href={`/${context.currentLanguage}`}
|
||||
className="color-fg-default no-underline d-flex flex-items-center"
|
||||
>
|
||||
<MarkGithubIcon size={32} className="mr-2" />
|
||||
<span className="f4 text-bold">{siteTitle}</span>
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SimpleFooter() {
|
||||
return (
|
||||
<footer className="py-6">
|
||||
<div className="container-xl px-3 px-md-6">
|
||||
<ul className="d-flex flex-wrap list-style-none">
|
||||
<li className="d-flex mr-xl-3 color-fg-muted">
|
||||
<span>© {new Date().getFullYear()} GitHub, Inc.</span>
|
||||
</li>
|
||||
<li className="ml-3">
|
||||
<a className="text-underline" href="/site-policy/github-terms/github-terms-of-service">
|
||||
Terms
|
||||
</a>
|
||||
</li>
|
||||
<li className="ml-3">
|
||||
<a
|
||||
className="text-underline"
|
||||
href="/site-policy/privacy-policies/github-privacy-statement"
|
||||
>
|
||||
Privacy
|
||||
</a>
|
||||
</li>
|
||||
<li className="ml-3">
|
||||
<a className="text-underline" href="https://www.githubstatus.com/">
|
||||
Status
|
||||
</a>
|
||||
</li>
|
||||
<li className="ml-3">
|
||||
<a className="text-underline" href="https://github.com/pricing">
|
||||
Pricing
|
||||
</a>
|
||||
</li>
|
||||
<li className="ml-3">
|
||||
<a className="text-underline" href="https://services.github.com/">
|
||||
Expert services
|
||||
</a>
|
||||
</li>
|
||||
<li className="ml-3">
|
||||
<a className="text-underline" href="https://github.blog/">
|
||||
Blog
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
|
||||
function SimpleLead({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="f2 color-fg-muted mb-3" data-container="lead">
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
import { CommentDiscussionIcon } from '@primer/octicons-react'
|
||||
|
||||
export function NotFoundContent() {
|
||||
const context = useAppRouterMainContext()
|
||||
|
||||
const { t } = useMemo(
|
||||
() => createTranslationFunctions(context.site.data.ui, ['meta']),
|
||||
[context.site.data.ui],
|
||||
)
|
||||
const { t } = createTranslationFunctions(context.site.data.ui, 'not_found')
|
||||
|
||||
return (
|
||||
<div className="min-h-screen d-flex flex-column">
|
||||
<SimpleHeader />
|
||||
<AppRouterHeader />
|
||||
|
||||
<div className="container-xl p-responsive py-6 width-full flex-1">
|
||||
<article className="col-md-10 col-lg-7 mx-auto">
|
||||
<h1>{t('oops')}</h1>
|
||||
<SimpleLead>It looks like this page doesn't exist.</SimpleLead>
|
||||
<p className="f3">
|
||||
We track these errors automatically, but if the problem persists please feel free to
|
||||
contact us.
|
||||
</p>
|
||||
<h1>{t('title')}</h1>
|
||||
<div className="f2 color-fg-muted mb-3" data-container="lead">
|
||||
{t('message')}
|
||||
</div>
|
||||
<p className="f3">{t('contact')}</p>
|
||||
<a id="support" href="https://support.github.com" className="btn btn-outline mt-2">
|
||||
<CommentDiscussionIcon size="small" className="octicon mr-1" />
|
||||
Contact support
|
||||
{t('contact_cta')}
|
||||
</a>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<SimpleFooter />
|
||||
<ServerFooter currentLanguage={context.currentLanguage} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
86
src/app/components/ServerFooter.tsx
Normal file
86
src/app/components/ServerFooter.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import { getUIDataMerged } from '@/data-directory/lib/get-data'
|
||||
import { createTranslationFunctions } from '@/languages/lib/translation-utils'
|
||||
import { LinkExternalIcon } from '@primer/octicons-react'
|
||||
import type { ClientLanguageCode } from '@/languages/lib/client-languages'
|
||||
|
||||
interface ServerFooterProps {
|
||||
currentLanguage: ClientLanguageCode
|
||||
}
|
||||
|
||||
export function ServerFooter({ currentLanguage }: ServerFooterProps) {
|
||||
// Load translations on server-side - this ensures all footer translations work
|
||||
const uiData = getUIDataMerged(currentLanguage)
|
||||
const { t } = createTranslationFunctions(uiData, 'footer')
|
||||
|
||||
return (
|
||||
<section className="container-xl px-3 mt-6 pb-8 px-md-6 color-fg-muted">
|
||||
<h2 className="f4 mb-2 col-12">{t('legal_heading')}</h2>
|
||||
|
||||
{/* Machine translation notice for non-English languages */}
|
||||
{currentLanguage !== 'en' && <p>{t('machine')}</p>}
|
||||
|
||||
<ul className="d-flex flex-wrap list-style-none">
|
||||
<li className="mr-3">© {new Date().getFullYear()} GitHub, Inc.</li>
|
||||
|
||||
{/* German-specific Impressum link (legally required) */}
|
||||
{currentLanguage === 'de' && (
|
||||
<li className="mr-3">
|
||||
<a
|
||||
className="text-underline"
|
||||
href="https://aka.ms/impressum_de"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
{t('imprint')}
|
||||
</a>
|
||||
<LinkExternalIcon aria-label="(external site)" size={12} />
|
||||
</li>
|
||||
)}
|
||||
|
||||
<li className="mr-3">
|
||||
<a
|
||||
className="text-underline"
|
||||
href={`/${currentLanguage}/site-policy/github-terms/github-terms-of-service`}
|
||||
>
|
||||
{t('terms')}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li className="mr-3">
|
||||
<a
|
||||
className={`text-underline ${
|
||||
currentLanguage === 'ko' ? 'color-fg-attention text-bold' : ''
|
||||
}`}
|
||||
href={`/${currentLanguage}/site-policy/privacy-policies/github-privacy-statement`}
|
||||
>
|
||||
{t('privacy')}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li className="mr-3">
|
||||
<a className="text-underline" href="https://www.githubstatus.com/">
|
||||
{t('status')}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li className="mr-3">
|
||||
<a className="text-underline" href="https://github.com/pricing">
|
||||
{t('pricing')}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li className="mr-3">
|
||||
<a className="text-underline" href="https://services.github.com">
|
||||
{t('expert_services')}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li className="mr-3">
|
||||
<a className="text-underline" href="https://github.blog">
|
||||
{t('blog')}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
12
src/app/components/hooks/useAppRouterVersion.ts
Normal file
12
src/app/components/hooks/useAppRouterVersion.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
'use client'
|
||||
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { getVersionInfoFromPath } from '@/app/lib/version-utils'
|
||||
|
||||
/**
|
||||
* App Router compatible version hook
|
||||
*/
|
||||
export function useAppRouterVersion() {
|
||||
const pathname = usePathname()
|
||||
return getVersionInfoFromPath(pathname ?? '')
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import { headers } from 'next/headers'
|
||||
import { translate } from '@/languages/lib/translation-utils'
|
||||
import { clientLanguageKeys } from '@/languages/lib/client-languages'
|
||||
import { getUIDataMerged } from '@/data-directory/lib/get-data'
|
||||
import { type ClientLanguageCode } from '@/languages/lib/client-languages'
|
||||
import { translate } from '@/languages/lib/translation-utils'
|
||||
import { extractLanguageFromPath } from '@/app/lib/language-utils'
|
||||
|
||||
export interface AppRouterContext {
|
||||
currentLanguage: string
|
||||
currentLanguage: ClientLanguageCode
|
||||
currentVersion: string
|
||||
sitename: string
|
||||
site: {
|
||||
@@ -14,18 +14,24 @@ export interface AppRouterContext {
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAppRouterContext(): Promise<AppRouterContext> {
|
||||
const headersList = await headers()
|
||||
/**
|
||||
* Create App Router context from pathname
|
||||
*/
|
||||
export function createAppRouterContext(
|
||||
pathname: string = '/',
|
||||
fallbackLanguage?: ClientLanguageCode,
|
||||
): AppRouterContext {
|
||||
let language = extractLanguageFromPath(pathname)
|
||||
|
||||
// Get language and version from headers or fallbacks
|
||||
const language =
|
||||
headersList.get('x-docs-language') ||
|
||||
extractLanguageFromHeader(headersList.get('accept-language') || 'en')
|
||||
const version = headersList.get('x-docs-version') || 'free-pro-team@latest'
|
||||
// Use fallback if provided and URL doesn't specify language
|
||||
if (language === 'en' && fallbackLanguage && fallbackLanguage !== 'en') {
|
||||
language = fallbackLanguage
|
||||
}
|
||||
|
||||
// Load UI data directly from data directory the same way as Pages Router does it
|
||||
const version = 'free-pro-team@latest'
|
||||
|
||||
// Load UI data directly from data directory
|
||||
const uiData = getUIDataMerged(language)
|
||||
|
||||
const siteName = translate(uiData, 'header.github_docs', 'GitHub Docs')
|
||||
|
||||
return {
|
||||
@@ -39,49 +45,3 @@ export async function getAppRouterContext(): Promise<AppRouterContext> {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function extractLanguageFromHeader(acceptLanguage: string): string {
|
||||
// Fastly's custom VCL forces Accept-Language header to contain
|
||||
// one of our supported language codes, so complex parsing isn't needed
|
||||
const language = acceptLanguage.trim()
|
||||
return clientLanguageKeys.includes(language) ? language : 'en'
|
||||
}
|
||||
|
||||
// Helper to create minimal MainContext-compatible object
|
||||
export function createAppRouterMainContext(appContext: AppRouterContext): any {
|
||||
return {
|
||||
currentLanguage: appContext.currentLanguage,
|
||||
currentVersion: appContext.currentVersion,
|
||||
data: {
|
||||
ui: appContext.site.data.ui,
|
||||
reusables: {},
|
||||
variables: {
|
||||
release_candidate: { version: null },
|
||||
},
|
||||
},
|
||||
allVersions: {},
|
||||
breadcrumbs: {},
|
||||
communityRedirect: {},
|
||||
currentPathWithoutLanguage: '',
|
||||
currentProduct: null,
|
||||
currentProductName: '',
|
||||
currentProductTree: null,
|
||||
enterpriseServerReleases: {
|
||||
isOldestReleaseDeprecated: false,
|
||||
oldestSupported: '',
|
||||
nextDeprecationDate: '',
|
||||
supported: [],
|
||||
},
|
||||
enterpriseServerVersions: [],
|
||||
error: '',
|
||||
featureFlags: {},
|
||||
fullUrl: '',
|
||||
isHomepageVersion: false,
|
||||
nonEnterpriseDefaultVersion: 'free-pro-team@latest',
|
||||
page: null,
|
||||
relativePath: null,
|
||||
sidebarTree: null,
|
||||
status: 404,
|
||||
xHost: '',
|
||||
}
|
||||
}
|
||||
|
||||
18
src/app/lib/constants.ts
Normal file
18
src/app/lib/constants.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* App Router compatible constants
|
||||
* These can be safely imported by both server and client components
|
||||
*/
|
||||
|
||||
export const DEFAULT_VERSION = 'free-pro-team@latest'
|
||||
|
||||
// Version utility functions that don't use router hooks
|
||||
export function getVersionInfo(currentVersion: string = DEFAULT_VERSION) {
|
||||
return {
|
||||
currentVersion,
|
||||
isEnterprise: currentVersion.includes('enterprise'),
|
||||
isEnterpriseCloud: currentVersion.includes('cloud'),
|
||||
isEnterpriseServer: currentVersion.includes('enterprise-server'),
|
||||
}
|
||||
}
|
||||
|
||||
export type VersionInfo = ReturnType<typeof getVersionInfo>
|
||||
51
src/app/lib/language-utils.ts
Normal file
51
src/app/lib/language-utils.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { clientLanguageKeys, type ClientLanguageCode } from '@/languages/lib/client-languages'
|
||||
|
||||
/**
|
||||
* Extract language from URL path
|
||||
* Handles paths like /en/something, /es/articles, etc.
|
||||
*/
|
||||
export function extractLanguageFromPath(path: string): ClientLanguageCode {
|
||||
try {
|
||||
const pathSegments = path.split('/')
|
||||
const firstSegment = pathSegments[1]
|
||||
|
||||
if (firstSegment && clientLanguageKeys.includes(firstSegment)) {
|
||||
return firstSegment as ClientLanguageCode
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to extract language from path:', error)
|
||||
}
|
||||
return 'en'
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a path contains a language prefix
|
||||
*/
|
||||
export function hasLanguagePrefix(path: string): boolean {
|
||||
const pathSegments = path.split('/')
|
||||
const firstSegment = pathSegments[1]
|
||||
return Boolean(firstSegment && clientLanguageKeys.includes(firstSegment))
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove language prefix from path
|
||||
* e.g., /es/articles/example -> /articles/example
|
||||
*/
|
||||
export function stripLanguagePrefix(path: string): string {
|
||||
if (hasLanguagePrefix(path)) {
|
||||
const pathSegments = path.split('/')
|
||||
return '/' + pathSegments.slice(2).join('/')
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
/**
|
||||
* Add language prefix to path if it doesn't have one
|
||||
* e.g., /articles/example + 'es' -> /es/articles/example
|
||||
*/
|
||||
export function addLanguagePrefix(path: string, language: ClientLanguageCode): string {
|
||||
if (hasLanguagePrefix(path)) {
|
||||
return path
|
||||
}
|
||||
return `/${language}${path === '/' ? '' : path}`
|
||||
}
|
||||
45
src/app/lib/server-context-utils.ts
Normal file
45
src/app/lib/server-context-utils.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { extractLanguageFromPath } from '@/app/lib/language-utils'
|
||||
import { extractVersionFromPath } from '@/app/lib/version-utils'
|
||||
import { getUIDataMerged } from '@/data-directory/lib/get-data'
|
||||
import { type ClientLanguageCode } from '@/languages/lib/client-languages'
|
||||
import { createTranslationFunctions, translate } from '@/languages/lib/translation-utils'
|
||||
|
||||
export interface ServerAppRouterContext {
|
||||
currentLanguage: ClientLanguageCode
|
||||
currentVersion: string
|
||||
sitename: string
|
||||
site: { data: { ui: any } }
|
||||
}
|
||||
|
||||
/**
|
||||
* Server-side context creation using filesystem data
|
||||
* Use in server components where filesystem access is available
|
||||
*/
|
||||
export function createServerAppRouterContext(pathname: string): ServerAppRouterContext {
|
||||
const language = extractLanguageFromPath(pathname)
|
||||
const currentVersion = extractVersionFromPath(pathname)
|
||||
|
||||
const uiData = getUIDataMerged(language)
|
||||
const siteName = translate(uiData, 'header.github_docs', 'GitHub Docs')
|
||||
|
||||
return {
|
||||
currentLanguage: language,
|
||||
currentVersion,
|
||||
sitename: siteName,
|
||||
site: { data: { ui: uiData } },
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create server-side footer with translations
|
||||
*/
|
||||
export function createServerFooterContent(language: ClientLanguageCode) {
|
||||
const uiData = getUIDataMerged(language)
|
||||
const { t } = createTranslationFunctions(uiData, 'footer')
|
||||
|
||||
return {
|
||||
t,
|
||||
language,
|
||||
footerData: uiData.footer || {},
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
'use client'
|
||||
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { useMemo } from 'react'
|
||||
import { useMemo, useEffect, useState } from 'react'
|
||||
import { clientLanguageKeys, type ClientLanguageCode } from '@/languages/lib/client-languages'
|
||||
import Cookies from '@/frame/components/lib/cookies'
|
||||
import { USER_LANGUAGE_COOKIE_NAME } from '@/frame/lib/constants'
|
||||
|
||||
/**
|
||||
* Validates if a string is a supported locale using client languages
|
||||
@@ -16,8 +18,32 @@ function isValidLocale(locale: string): locale is ClientLanguageCode {
|
||||
*/
|
||||
export function useDetectLocale(): ClientLanguageCode {
|
||||
const pathname = usePathname()
|
||||
const [cookieLanguage, setCookieLanguage] = useState<ClientLanguageCode | null>(null)
|
||||
const [browserLanguage, setBrowserLanguage] = useState<ClientLanguageCode | null>(null)
|
||||
|
||||
// Read cookie and browser language on client-side mount
|
||||
useEffect(() => {
|
||||
const userLanguageCookie = Cookies.get(USER_LANGUAGE_COOKIE_NAME)
|
||||
if (userLanguageCookie && isValidLocale(userLanguageCookie)) {
|
||||
setCookieLanguage(userLanguageCookie)
|
||||
}
|
||||
|
||||
// Get language from browser as fallback
|
||||
if (navigator?.language) {
|
||||
const browserLocale = navigator.language.split('-')[0]
|
||||
if (isValidLocale(browserLocale)) {
|
||||
setBrowserLanguage(browserLocale)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
return useMemo(() => {
|
||||
// Priority order:
|
||||
// 1. URL path
|
||||
// 2. User language cookie
|
||||
// 3. Browser language
|
||||
// 4. Default to English
|
||||
|
||||
// Extract locale from pathname (e.g., /es/search -> 'es')
|
||||
if (pathname) {
|
||||
const pathSegments = pathname.split('/')
|
||||
@@ -28,16 +54,18 @@ export function useDetectLocale(): ClientLanguageCode {
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to browser locale if available
|
||||
if (typeof window !== 'undefined' && window.navigator?.language) {
|
||||
const browserLocale = window.navigator.language.split('-')[0]
|
||||
if (isValidLocale(browserLocale)) {
|
||||
return browserLocale
|
||||
}
|
||||
// Use cookie language if available
|
||||
if (cookieLanguage) {
|
||||
return cookieLanguage
|
||||
}
|
||||
|
||||
// Use browser language if available
|
||||
if (browserLanguage) {
|
||||
return browserLanguage
|
||||
}
|
||||
|
||||
return 'en'
|
||||
}, [pathname])
|
||||
}, [pathname, cookieLanguage, browserLanguage])
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
26
src/app/lib/version-utils.ts
Normal file
26
src/app/lib/version-utils.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { DEFAULT_VERSION, getVersionInfo } from '@/app/lib/constants'
|
||||
|
||||
/**
|
||||
* Extract version from pathname (works in both server and client)
|
||||
*/
|
||||
export function extractVersionFromPath(pathname: string): string {
|
||||
const pathSegments = pathname.split('/').filter(Boolean)
|
||||
|
||||
const versionSegment = pathSegments.find(
|
||||
(segment) =>
|
||||
segment.includes('enterprise-server') ||
|
||||
segment.includes('enterprise-cloud') ||
|
||||
segment === 'enterprise' ||
|
||||
segment === 'free-pro-team@latest',
|
||||
)
|
||||
|
||||
return versionSegment || DEFAULT_VERSION
|
||||
}
|
||||
|
||||
/**
|
||||
* Get version info from pathname (works in both server and client)
|
||||
*/
|
||||
export function getVersionInfoFromPath(pathname: string) {
|
||||
const currentVersion = extractVersionFromPath(pathname)
|
||||
return getVersionInfo(currentVersion)
|
||||
}
|
||||
@@ -1,27 +1,21 @@
|
||||
import { AppRouterMainContextProvider } from '@/app/components/AppRouterMainContext'
|
||||
import { NotFoundContent } from '@/app/components/NotFoundContent'
|
||||
import { getAppRouterContext } from '@/app/lib/app-router-context'
|
||||
import { ClientNotFoundWrapper } from '@/app/components/ClientNotFoundWrapper'
|
||||
import { createServerAppRouterContext } from '@/app/lib/server-context-utils'
|
||||
import { headers } from 'next/headers'
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
// Force this page to be dynamic so it can access headers()
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: '404 - Page not found',
|
||||
other: {
|
||||
status: '404',
|
||||
},
|
||||
other: { status: '404' },
|
||||
}
|
||||
|
||||
async function NotFoundPage() {
|
||||
// Get context from headers set by gateway middleware
|
||||
const appContext = await getAppRouterContext()
|
||||
export default async function NotFoundPage() {
|
||||
const headersList = await headers()
|
||||
const pathname = headersList.get('x-pathname') || '/'
|
||||
|
||||
return (
|
||||
<AppRouterMainContextProvider context={appContext}>
|
||||
<NotFoundContent />
|
||||
</AppRouterMainContextProvider>
|
||||
)
|
||||
// Create server context using utility function
|
||||
const appContext = createServerAppRouterContext(pathname)
|
||||
|
||||
return <ClientNotFoundWrapper appContext={appContext} />
|
||||
}
|
||||
|
||||
export default NotFoundPage
|
||||
|
||||
@@ -343,3 +343,9 @@ cookbook_landing:
|
||||
search_articles: Search articles
|
||||
category: Category
|
||||
complexity: Complexity
|
||||
|
||||
not_found:
|
||||
title: Ooops!
|
||||
message: It looks like this page doesn't exist.
|
||||
contact: We track these errors automatically, but if the problem persists please feel free to contact us.
|
||||
contact_cta: Contact support
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
/**
|
||||
* Utilities for safely serializing data for HTTP headers
|
||||
*/
|
||||
|
||||
export interface MinimalUIData {
|
||||
header: {
|
||||
github_docs?: string
|
||||
}
|
||||
footer?: any
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely serialize data to a base64-encoded JSON string for HTTP headers
|
||||
* Handle encoding issues and provide fallbacks for serialization errors
|
||||
*/
|
||||
export function safeStringifyForHeader(data: any): string {
|
||||
try {
|
||||
const jsonString = JSON.stringify(data)
|
||||
// Encode to base64 to avoid header character issues
|
||||
return Buffer.from(jsonString, 'utf8').toString('base64')
|
||||
} catch (e) {
|
||||
console.warn('Failed to stringify data for header:', e)
|
||||
// Return minimal fallback as base64
|
||||
const fallback = JSON.stringify({
|
||||
header: { github_docs: 'GitHub Docs' },
|
||||
footer: {},
|
||||
})
|
||||
return Buffer.from(fallback, 'utf8').toString('base64')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create minimal UI data from Express context
|
||||
* Provide consistent fallbacks for missing data
|
||||
*/
|
||||
export function createMinimalUIData(context?: any): MinimalUIData {
|
||||
if (!context?.site?.data?.ui) {
|
||||
return {
|
||||
header: { github_docs: 'GitHub Docs' },
|
||||
footer: {},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
header: context.site.data.ui.header || { github_docs: 'GitHub Docs' },
|
||||
footer: context.site.data.ui.footer || {},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set context headers for App Router from Express context
|
||||
* Preserve headers from Fastly
|
||||
*/
|
||||
export function setAppRouterContextHeaders(
|
||||
req: any,
|
||||
res: any,
|
||||
preserveFastlyHeaders: boolean = true,
|
||||
): void {
|
||||
if (req.context) {
|
||||
// Only set language header if Fastly hasn't already set it (or if not preserving)
|
||||
if (!preserveFastlyHeaders || !req.headers['x-docs-language']) {
|
||||
res.setHeader('x-docs-language', req.context.currentLanguage || 'en')
|
||||
}
|
||||
|
||||
// Only set version header if Fastly hasn't already set it (or if not preserving)
|
||||
if (!preserveFastlyHeaders || !req.headers['x-docs-version']) {
|
||||
res.setHeader('x-docs-version', req.context.currentVersion || 'free-pro-team@latest')
|
||||
}
|
||||
|
||||
const minimalUI = createMinimalUIData(req.context)
|
||||
res.setHeader('x-docs-ui-data', safeStringifyForHeader(minimalUI))
|
||||
} else {
|
||||
// Fallback when no Express context is available
|
||||
res.setHeader('x-docs-language', 'en')
|
||||
res.setHeader('x-docs-version', 'free-pro-team@latest')
|
||||
res.setHeader(
|
||||
'x-docs-ui-data',
|
||||
safeStringifyForHeader({
|
||||
header: { github_docs: 'GitHub Docs' },
|
||||
footer: {},
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -5,34 +5,31 @@ import {
|
||||
shouldUseAppRouter,
|
||||
stripLocalePrefix,
|
||||
} from '@/app/lib/routing-patterns'
|
||||
import { defaultCacheControl } from './cache-control'
|
||||
import type { ExtendedRequest } from '@/types'
|
||||
import type { NextFunction, Response } from 'express'
|
||||
import { setAppRouterContextHeaders } from '../lib/header-utils'
|
||||
import { defaultCacheControl } from './cache-control'
|
||||
import { nextApp } from './next'
|
||||
|
||||
export default function appRouterGateway(req: ExtendedRequest, res: Response, next: NextFunction) {
|
||||
const path = req.path || req.url
|
||||
const strippedPath = stripLocalePrefix(path)
|
||||
|
||||
// Only intercept GET and HEAD requests, and prioritize /empty-categories paths
|
||||
// Only intercept GET and HEAD requests
|
||||
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
||||
return next()
|
||||
}
|
||||
|
||||
// Special case: Always intercept /empty-categories paths regardless of method
|
||||
// Special case: Always intercept /empty-categories paths
|
||||
if (strippedPath.endsWith('/empty-categories')) {
|
||||
// Skip the normal exclusion logic and go straight to App Router routing
|
||||
const pageFound = !!(req.context && req.context.page)
|
||||
|
||||
if (shouldUseAppRouter(path, pageFound)) {
|
||||
// Set the URL to trigger App Router's not-found.tsx since /empty-categories should 404
|
||||
req.url = '/404'
|
||||
res.status(404)
|
||||
defaultCacheControl(res)
|
||||
|
||||
// Set context headers for App Router (don't preserve Fastly since this is internal routing)
|
||||
setAppRouterContextHeaders(req, res, false)
|
||||
// Only pass the original pathname - no other headers needed
|
||||
res.setHeader('x-pathname', req.path)
|
||||
|
||||
res.locals = res.locals || {}
|
||||
res.locals.handledByAppRouter = true
|
||||
@@ -41,11 +38,6 @@ export default function appRouterGateway(req: ExtendedRequest, res: Response, ne
|
||||
}
|
||||
|
||||
// Don't route static assets, API routes, valid versioned docs paths, or junk paths to App Router
|
||||
// Let them be handled by Pages Router middleware (shielding, API handlers, etc.)
|
||||
// However, invalid versioned paths (like paths with /ANY/ or bogus versions) should go to App Router for 404
|
||||
// EXCEPTION: /empty-categories paths should always go to App Router for proper 404 handling
|
||||
const strippedPathForExclusion = stripLocalePrefix(path)
|
||||
|
||||
if (
|
||||
path.startsWith('/_next/') ||
|
||||
path.startsWith('/_build') ||
|
||||
@@ -56,7 +48,7 @@ export default function appRouterGateway(req: ExtendedRequest, res: Response, ne
|
||||
isJunkPath(path) ||
|
||||
(isVersionedPath(path) &&
|
||||
!isInvalidVersionedPath(path) &&
|
||||
!strippedPathForExclusion.endsWith('/empty-categories')) ||
|
||||
!strippedPath.endsWith('/empty-categories')) ||
|
||||
path.includes('.css') ||
|
||||
path.includes('.js') ||
|
||||
path.includes('.map') ||
|
||||
@@ -71,22 +63,17 @@ export default function appRouterGateway(req: ExtendedRequest, res: Response, ne
|
||||
return next()
|
||||
}
|
||||
|
||||
// Check if a page was found by the findPage middleware
|
||||
const pageFound = !!(req.context && req.context.page)
|
||||
|
||||
if (shouldUseAppRouter(path, pageFound)) {
|
||||
console.log(`[INFO] Using App Router for path: ${path} (pageFound: ${!!pageFound})`)
|
||||
|
||||
// Strip locale prefix for App Router routing
|
||||
const strippedPath = stripLocalePrefix(path)
|
||||
|
||||
// For 404 routes (either explicit or missing pages), always route to our 404 page
|
||||
// For 404 routes, always route to our 404 page
|
||||
if (strippedPath === '/404' || strippedPath === '/_not-found' || !pageFound) {
|
||||
// Set the URL to trigger App Router's not-found.tsx
|
||||
req.url = '/404' // Use a real App Router page route
|
||||
req.url = '/404'
|
||||
res.status(404)
|
||||
|
||||
// Set proper cache headers for 404 responses to match Pages Router behavior
|
||||
defaultCacheControl(res)
|
||||
} else {
|
||||
// For other App Router routes, use the stripped path
|
||||
@@ -94,22 +81,16 @@ export default function appRouterGateway(req: ExtendedRequest, res: Response, ne
|
||||
req.url = strippedPath + originalUrl.substring(req.path.length)
|
||||
}
|
||||
|
||||
// Set context headers for App Router (preserve Fastly headers)
|
||||
setAppRouterContextHeaders(req, res, true)
|
||||
|
||||
// Use Next.js App Router to handle this request
|
||||
// The App Router will use the appropriate page.tsx or not-found.tsx
|
||||
// IMPORTANT: Don't call next() - this terminates the Express middleware chain
|
||||
// Only pass pathname for App Router context creation
|
||||
res.setHeader('x-pathname', req.path)
|
||||
|
||||
// Mark response as handled to prevent further middleware processing
|
||||
res.locals = res.locals || {}
|
||||
res.locals.handledByAppRouter = true
|
||||
|
||||
// Use the Next.js request handler and DO NOT call next()
|
||||
return nextApp.getRequestHandler()(req, res)
|
||||
}
|
||||
|
||||
console.log(`[INFO] Using Pages Router for path: ${path}`)
|
||||
// Continue with Pages Router pipeline
|
||||
return next()
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import statsd from '@/observability/lib/statsd'
|
||||
import type { ExtendedRequest } from '@/types'
|
||||
import { allVersions } from '@/versions/lib/all-versions'
|
||||
import { minimumNotFoundHtml } from '../lib/constants'
|
||||
import { setAppRouterContextHeaders } from '../lib/header-utils'
|
||||
import { defaultCacheControl } from './cache-control'
|
||||
import { isConnectionDropped } from './halt-on-dropped-connection'
|
||||
import { nextHandleRequest } from './next'
|
||||
@@ -47,7 +46,7 @@ async function buildMiniTocItems(req: ExtendedRequest): Promise<string | undefin
|
||||
export default async function renderPage(req: ExtendedRequest, res: Response) {
|
||||
// Skip if App Router has already handled this request
|
||||
if (res.locals?.handledByAppRouter) {
|
||||
return // Request already handled by App Router
|
||||
return
|
||||
}
|
||||
|
||||
const { context } = req
|
||||
@@ -75,9 +74,7 @@ export default async function renderPage(req: ExtendedRequest, res: Response) {
|
||||
return res.status(404).type('html').send(minimumNotFoundHtml)
|
||||
}
|
||||
|
||||
// The rest is "unhandled" requests where we don't have the page
|
||||
// but the URL looks like a real page.
|
||||
|
||||
// For App Router migration: All language-prefixed 404s should use App Router
|
||||
statsd.increment(STATSD_KEY_404, 1, [
|
||||
`url:${req.url}`,
|
||||
`path:${req.path}`,
|
||||
@@ -86,15 +83,14 @@ export default async function renderPage(req: ExtendedRequest, res: Response) {
|
||||
|
||||
defaultCacheControl(res)
|
||||
|
||||
// For App Router migration: All language-prefixed 404s should use App Router
|
||||
// Create a mock request that will be handled by App Router
|
||||
const mockReq = Object.create(req)
|
||||
mockReq.url = '/404'
|
||||
mockReq.path = '/404'
|
||||
mockReq.method = 'GET'
|
||||
|
||||
// Set context headers for App Router (preserves Fastly headers)
|
||||
setAppRouterContextHeaders(req, res, true)
|
||||
// Only pass pathname
|
||||
res.setHeader('x-pathname', req.path)
|
||||
|
||||
// Import nextApp and handle directly
|
||||
const { nextApp } = await import('./next')
|
||||
|
||||
Reference in New Issue
Block a user