Skip to content

Commit 7d8b9cf

Browse files
wiinciCopilot
andauthored
feat: Implement App Router integration and 404 handling (#56915)
Co-authored-by: Copilot <[email protected]>
1 parent 1f7fff8 commit 7d8b9cf

27 files changed

+1653
-164
lines changed

src/app/404/page.tsx

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { getAppRouterContext } from '@/app/lib/app-router-context'
2+
import { AppRouterMainContextProvider } from '@/app/components/AppRouterMainContext'
3+
import { translate } from '@/languages/lib/translation-utils'
4+
import { CommentDiscussionIcon, MarkGithubIcon } from '@primer/octicons-react'
5+
import type { Metadata } from 'next'
6+
7+
export const dynamic = 'force-dynamic'
8+
9+
export const metadata: Metadata = {
10+
title: '404 - Page not found',
11+
other: { status: '404' },
12+
}
13+
14+
export default async function Page404() {
15+
// Get context with UI data
16+
const appContext = await getAppRouterContext()
17+
18+
const siteTitle = translate(appContext.site.data.ui, 'header.github_docs', 'GitHub Docs')
19+
const oopsTitle = translate(appContext.site.data.ui, 'meta.oops', 'Ooops!')
20+
21+
return (
22+
<AppRouterMainContextProvider context={appContext}>
23+
<div className="min-h-screen d-flex flex-column">
24+
{/* Simple Header */}
25+
<div className="border-bottom color-border-muted no-print">
26+
<header className="container-xl p-responsive py-3 position-relative d-flex width-full">
27+
<div className="d-flex flex-1 flex-items-center">
28+
<a
29+
href={`/${appContext.currentLanguage}`}
30+
className="color-fg-default no-underline d-flex flex-items-center"
31+
>
32+
<MarkGithubIcon size={32} className="mr-2" />
33+
<span className="f4 text-bold">{siteTitle}</span>
34+
</a>
35+
</div>
36+
</header>
37+
</div>
38+
39+
{/* Main Content */}
40+
<div className="container-xl p-responsive py-6 width-full flex-1">
41+
<article className="col-md-10 col-lg-7 mx-auto">
42+
<h1>{oopsTitle}</h1>
43+
<div className="f2 color-fg-muted mb-3" data-container="lead">
44+
It looks like this page doesn't exist.
45+
</div>
46+
<p className="f3">
47+
We track these errors automatically, but if the problem persists please feel free to
48+
contact us.
49+
</p>
50+
<a id="support" href="https://support.github.com" className="btn btn-outline mt-2">
51+
<CommentDiscussionIcon size="small" className="octicon mr-1" />
52+
Contact support
53+
</a>
54+
</article>
55+
</div>
56+
57+
<footer className="py-6">
58+
<div className="container-xl px-3 px-md-6">
59+
<ul className="d-flex flex-wrap list-style-none">
60+
<li className="d-flex mr-xl-3 color-fg-muted">
61+
<span>© 2025 GitHub, Inc.</span>
62+
</li>
63+
<li className="ml-3">
64+
<a
65+
className="text-underline"
66+
href="/site-policy/github-terms/github-terms-of-service"
67+
>
68+
Terms
69+
</a>
70+
</li>
71+
<li className="ml-3">
72+
<a
73+
className="text-underline"
74+
href="/site-policy/privacy-policies/github-privacy-statement"
75+
>
76+
Privacy
77+
</a>
78+
</li>
79+
<li className="ml-3">
80+
<a className="text-underline" href="https://www.githubstatus.com/">
81+
Status
82+
</a>
83+
</li>
84+
<li className="ml-3">
85+
<a className="text-underline" href="https://github.com/pricing">
86+
Pricing
87+
</a>
88+
</li>
89+
<li className="ml-3">
90+
<a className="text-underline" href="https://services.github.com/">
91+
Expert services
92+
</a>
93+
</li>
94+
<li className="ml-3">
95+
<a className="text-underline" href="https://github.blog/">
96+
Blog
97+
</a>
98+
</li>
99+
</ul>
100+
</div>
101+
</footer>
102+
</div>
103+
</AppRouterMainContextProvider>
104+
)
105+
}

src/app/_not-found/page.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { notFound } from 'next/navigation'
2+
3+
// This page handles internal /_not-found redirects from Express middleware
4+
export default function InternalNotFound() {
5+
// This will trigger Next.js to render the not-found.tsx page
6+
notFound()
7+
}

src/app/client-layout.tsx

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
'use client'
2+
3+
import { ThemeProvider } from '@primer/react'
4+
import { useEffect, useMemo, useState } from 'react'
5+
6+
import { LocaleProvider } from '@/app/lib/locale-context'
7+
import { useDetectLocale } from '@/app/lib/use-detect-locale'
8+
import { useTheme } from '@/color-schemes/components/useTheme'
9+
import { initializeEvents } from '@/events/components/events'
10+
import { CTAPopoverProvider } from '@/frame/components/context/CTAContext'
11+
import { SharedUIContextProvider } from '@/frame/components/context/SharedUIContext'
12+
import { LanguagesContext, LanguagesContextT } from '@/languages/components/LanguagesContext'
13+
import { clientLanguages, type ClientLanguageCode } from '@/languages/lib/client-languages'
14+
import { MainContextProvider } from '@/app/components/MainContextProvider'
15+
import { createMinimalMainContext } from '@/app/lib/main-context-adapter'
16+
import type { AppRouterContext } from '@/app/lib/app-router-context'
17+
18+
interface ClientLayoutProps {
19+
readonly children: React.ReactNode
20+
readonly appContext?: AppRouterContext
21+
readonly pageData?: {
22+
title?: string
23+
fullTitle?: string
24+
introPlainText?: string
25+
topics?: string[]
26+
documentType?: string
27+
type?: string
28+
hidden?: boolean
29+
}
30+
}
31+
32+
export function ClientLayout({ children, appContext, pageData }: ClientLayoutProps): JSX.Element {
33+
const { theme } = useTheme()
34+
const locale: ClientLanguageCode = useDetectLocale()
35+
const [isInitialized, setIsInitialized] = useState(false)
36+
const [initializationError, setInitializationError] = useState<Error | null>(null)
37+
38+
const languagesContext: LanguagesContextT = useMemo(
39+
() => ({
40+
languages: clientLanguages,
41+
}),
42+
[],
43+
)
44+
45+
// Create MainContext-compatible data for App Router
46+
const mainContext = useMemo(
47+
() => createMinimalMainContext(pageData, appContext),
48+
[pageData, appContext],
49+
)
50+
51+
useEffect(() => {
52+
const initializeTheme = async (): Promise<void> => {
53+
try {
54+
const html = document.documentElement
55+
56+
if (theme.css?.colorMode) {
57+
html.setAttribute('data-color-mode', theme.css.colorMode)
58+
}
59+
60+
if (theme.css?.darkTheme) {
61+
html.setAttribute('data-dark-theme', theme.css.darkTheme)
62+
}
63+
64+
if (theme.css?.lightTheme) {
65+
html.setAttribute('data-light-theme', theme.css.lightTheme)
66+
}
67+
68+
if (!isInitialized) {
69+
await initializeEvents()
70+
setIsInitialized(true)
71+
}
72+
} catch (error) {
73+
console.error('Failed to initialize theme:', error)
74+
setInitializationError(error as Error)
75+
}
76+
}
77+
78+
initializeTheme()
79+
}, [theme, isInitialized])
80+
81+
if (initializationError) {
82+
return (
83+
<div
84+
role="alert"
85+
className="min-h-screen flex items-center justify-center bg-canvas-default p-4"
86+
>
87+
<div className="max-w-md text-center">
88+
<h2 className="text-xl font-semibold mb-4 text-danger-fg">Something went wrong</h2>
89+
<p className="text-fg-muted mb-4">Please try refreshing the page.</p>
90+
<button
91+
onClick={() => {
92+
setInitializationError(null)
93+
setIsInitialized(false)
94+
}}
95+
className="btn btn-primary"
96+
type="button"
97+
aria-label="Try again"
98+
>
99+
Try again
100+
</button>
101+
</div>
102+
</div>
103+
)
104+
}
105+
106+
return (
107+
<LocaleProvider locale={locale}>
108+
<LanguagesContext.Provider value={languagesContext}>
109+
<MainContextProvider value={mainContext}>
110+
<ThemeProvider
111+
colorMode={theme.component.colorMode}
112+
dayScheme={theme.component.dayScheme}
113+
nightScheme={theme.component.nightScheme}
114+
preventSSRMismatch
115+
>
116+
<SharedUIContextProvider>
117+
<CTAPopoverProvider>
118+
<div className="min-h-screen flex flex-col">{children}</div>
119+
</CTAPopoverProvider>
120+
</SharedUIContextProvider>
121+
</ThemeProvider>
122+
</MainContextProvider>
123+
</LanguagesContext.Provider>
124+
</LocaleProvider>
125+
)
126+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
'use client'
2+
import type { AppRouterContext } from '@/app/lib/app-router-context'
3+
import type { MainContextT } from '@/frame/components/context/MainContext'
4+
import { adaptAppRouterContextToMainContext } from '@/app/lib/main-context-adapter'
5+
import { createContext, ReactNode, useContext, useMemo } from 'react'
6+
7+
export const AppRouterMainContext = createContext<AppRouterContext | null>(null)
8+
9+
// Provides MainContext-compatible data
10+
export const AppRouterCompatMainContext = createContext<MainContextT | null>(null)
11+
12+
export function AppRouterMainContextProvider({
13+
children,
14+
context,
15+
}: {
16+
children: ReactNode
17+
context: AppRouterContext
18+
}) {
19+
// Create a MainContext-compatible version for existing components
20+
const mainContextCompat = useMemo(() => adaptAppRouterContextToMainContext(context), [context])
21+
22+
return (
23+
<AppRouterMainContext.Provider value={context}>
24+
<AppRouterCompatMainContext.Provider value={mainContextCompat}>
25+
{children}
26+
</AppRouterCompatMainContext.Provider>
27+
</AppRouterMainContext.Provider>
28+
)
29+
}
30+
31+
export function useAppRouterMainContext(): AppRouterContext {
32+
const context = useContext(AppRouterMainContext)
33+
34+
if (!context) {
35+
throw new Error('useAppRouterMainContext must be used within AppRouterMainContextProvider')
36+
}
37+
38+
return context
39+
}
40+
41+
// Hook for components that need MainContext compatibility
42+
export function useAppRouterCompatMainContext(): MainContextT {
43+
const context = useContext(AppRouterCompatMainContext)
44+
45+
if (!context) {
46+
throw new Error(
47+
'useAppRouterCompatMainContext must be used within AppRouterMainContextProvider',
48+
)
49+
}
50+
51+
return context
52+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
'use client'
2+
3+
import type { ReactNode } from 'react'
4+
import { MainContext, type MainContextT } from '@/frame/components/context/MainContext'
5+
6+
interface MainContextProviderProps {
7+
children: ReactNode
8+
value: MainContextT
9+
}
10+
11+
/**
12+
* App Router compatible MainContext provider
13+
* This allows reusing existing components that depend on MainContext
14+
*/
15+
export function MainContextProvider({ children, value }: MainContextProviderProps) {
16+
return <MainContext.Provider value={value}>{children}</MainContext.Provider>
17+
}

0 commit comments

Comments
 (0)