diff --git a/content/code-security/dependabot/working-with-dependabot/dependabot-options-reference.md b/content/code-security/dependabot/working-with-dependabot/dependabot-options-reference.md
index c2350a3ec931..cb4e9b9a4008 100644
--- a/content/code-security/dependabot/working-with-dependabot/dependabot-options-reference.md
+++ b/content/code-security/dependabot/working-with-dependabot/dependabot-options-reference.md
@@ -686,6 +686,66 @@ When `target-branch` is defined:
* All pull requests for version updates are opened targeting the specified branch.
* Options defined for this `package-ecosystem` no longer apply to security updates because security updates always use the default branch for the repository.
+## `exclude-paths` {% octicon "versions" aria-label="Version updates only" height="24" %}
+
+Use to specify paths of directories and files that {% data variables.product.prodname_dependabot %} should ignore when scanning for manifests and dependencies. This option is useful when you want to prevent updates for dependencies in certain locations, such as test assets, vendored code, or specific files.
+
+{% data variables.product.prodname_dependabot %} default behavior:
+
+* All directories and files in the specified `directory` are included in the update scan unless excluded by this option.
+
+When `exclude-paths` is defined:
+
+* All files and directories matching the specified paths are ignored during update scans for the given `package-ecosystem` entry.
+
+| Parameter | Purpose |
+|-----------|---------|
+| `exclude-paths` | A list of glob patterns for files or directories to ignore. |
+
+Glob patterns are supported, such as `**` for recursive matching and `*` for single-segment wildcards. Patterns are relative to the `directory` specified for the update configuration. Each ecosystem can have its own `exclude-paths` settings.
+
+## Example
+
+```yaml copy
+version: 2
+updates:
+ - package-ecosystem: "npm"
+ directory: "/"
+ schedule:
+ interval: "daily"
+ exclude-paths:
+ - "src/test/assets"
+ - "vendor/**"
+ - "src/*.js"
+ - "src/test/helper.js"
+
+# Sample patterns that can be used-
+
+# Pattern: docs/*.json
+# Matches: docs/foo.json, docs/bar.json
+
+# Pattern: *.lock
+# Matches: Gemfile.lock, package.lock, foo.lock (in any directory)
+
+# Pattern: test/**
+# Matches: test/foo.rb, test/bar/baz.rb, test/any/depth/file.txt
+
+# Pattern: config/settings.yml
+# Matches: config/settings.yml
+
+# Pattern: **/*.md
+# Matches: README.md, docs/guide.md, any/depth/file.md
+
+# Pattern: src/*
+# Matches: src/main.rb, src/app.js
+# Does NOT match: src/utils/helper.rb
+
+# Pattern: hidden/.*
+# Matches: hidden/.env, hidden/.secret
+```
+
+In this example, {% data variables.product.prodname_dependabot %} will ignore the `src/test/assets` directory, all files under `vendor/`, all JavaScript files directly under `src/`, and the specific file `src/test/helper.js` when scanning for updates.
+
## `vendor` {% octicon "versions" aria-label="Version updates" height="24" %} {% octicon "shield-check" aria-label="Security updates" height="24" %}
Supported by: `bundler` and `gomod` only.
diff --git a/content/code-security/secret-scanning/introduction/supported-secret-scanning-patterns.md b/content/code-security/secret-scanning/introduction/supported-secret-scanning-patterns.md
index dc82c3a04ae4..5640e87cf0ec 100644
--- a/content/code-security/secret-scanning/introduction/supported-secret-scanning-patterns.md
+++ b/content/code-security/secret-scanning/introduction/supported-secret-scanning-patterns.md
@@ -76,10 +76,10 @@ In addition to these generic non-provider patterns, {% data variables.product.pr
> [!NOTE]
> Validity checks are only available to users with {% data variables.product.prodname_team %} or {% data variables.product.prodname_enterprise %} who enable the feature as part of {% data variables.product.prodname_GH_secret_protection %}.
-| Provider | Token | Partner | User | Push protection | Validity check |
-|----|:----|:----:|:----:|:----:|:----:|
+| Provider | Token | Partner | User | Push protection | Validity check | Base 64 |
+|----|:----|:----:|:----:|:----:|:----:|:----:|
{%- for entry in secretScanningData %}
-| {{ entry.provider }} | {{ entry.secretType }} | {% if entry.isPublic %}✓{% else %}✗{% endif %} | {% if entry.isPrivateWithGhas %}✓{% else %}✗{% endif %} | {% if entry.hasPushProtection %}✓{% else %}✗{% endif %} | {% if entry.hasValidityCheck %}✓{% else %}✗{% endif %} |
+| {{ entry.provider }} | {{ entry.secretType }} | {% if entry.isPublic %}✓{% else %}✗{% endif %} | {% if entry.isPrivateWithGhas %}✓{% else %}✗{% endif %} | {% if entry.hasPushProtection %}✓{% else %}✗{% endif %} | {% if entry.hasValidityCheck %}✓{% else %}✗{% endif %} | {% if entry.base64Supported %}✓{% else %}✗{% endif %} |
{%- endfor %}
{% endif %}
@@ -87,10 +87,10 @@ In addition to these generic non-provider patterns, {% data variables.product.pr
{% ifversion ghes %}
-| Provider | Token | {% data variables.product.prodname_secret_scanning_caps %} alert | Push protection | Validity check |
-|----|:----|:----:|:----:|:----:|
+| Provider | Token | {% data variables.product.prodname_secret_scanning_caps %} alert | Push protection | Validity check | Base64 |
+|----|:----|:----:|:----:|:----:|:----:|
{%- for entry in secretScanningData %}
-| {{ entry.provider }} | {{ entry.secretType }} | {% if entry.isPrivateWithGhas %}✓{% else %}✗{% endif %} | {% if entry.hasPushProtection %}✓{% else %}✗{% endif %} | {% if entry.hasValidityCheck %}✓{% else %}✗{% endif %} |
+| {{ entry.provider }} | {{ entry.secretType }} | {% if entry.isPrivateWithGhas %}✓{% else %}✗{% endif %} | {% if entry.hasPushProtection %}✓{% else %}✗{% endif %} | {% if entry.hasValidityCheck %}✓{% else %}✗{% endif %} | {% if entry.base64Supported %}✓{% else %}✗{% endif %} |
{%- endfor %}
{% endif %}
diff --git a/data/ui.yml b/data/ui.yml
index d749d965157a..a8ac2b8ac0d1 100644
--- a/data/ui.yml
+++ b/data/ui.yml
@@ -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
diff --git a/src/app/404/page.tsx b/src/app/404/page.tsx
index 20eed3fcc857..ea69f26de66f 100644
--- a/src/app/404/page.tsx
+++ b/src/app/404/page.tsx
@@ -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 (
-
-
- {/* Simple Header */}
-
-
- {/* Main Content */}
-
-
- {oopsTitle}
-
- It looks like this page doesn't exist.
-
-
- We track these errors automatically, but if the problem persists please feel free to
- contact us.
-
-
-
- Contact support
-
-
-
-
-
-
-
- )
+ return
}
diff --git a/src/app/components/AppRouterFooter.tsx b/src/app/components/AppRouterFooter.tsx
new file mode 100644
index 000000000000..ac7e40922083
--- /dev/null
+++ b/src/app/components/AppRouterFooter.tsx
@@ -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 (
+
+ {context.currentLanguage !== 'en' && {t('legal_heading')}
}
+
+ {/* Machine translation notice for non-English languages */}
+ {context.currentLanguage !== 'en' && {t('machine')}
}
+
+
+
+ )
+}
diff --git a/src/app/components/AppRouterHeader.tsx b/src/app/components/AppRouterHeader.tsx
new file mode 100644
index 000000000000..85c06fc3d467
--- /dev/null
+++ b/src/app/components/AppRouterHeader.tsx
@@ -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 (
+
+ )
+}
diff --git a/src/app/components/AppRouterLanguagesContext.tsx b/src/app/components/AppRouterLanguagesContext.tsx
new file mode 100644
index 000000000000..c33a612fe956
--- /dev/null
+++ b/src/app/components/AppRouterLanguagesContext.tsx
@@ -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
+ currentLanguage?: ClientLanguageCode
+}
+
+export const AppRouterLanguagesContext = createContext(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 (
+
+ {children}
+
+ )
+}
diff --git a/src/app/components/Client404Wrapper.tsx b/src/app/components/Client404Wrapper.tsx
new file mode 100644
index 000000000000..22df8fcf9b60
--- /dev/null
+++ b/src/app/components/Client404Wrapper.tsx
@@ -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 (
+
+
+
+
+ {/* Main Content */}
+
+
+
+
+
+ )
+}
diff --git a/src/app/components/ClientNotFoundWrapper.tsx b/src/app/components/ClientNotFoundWrapper.tsx
new file mode 100644
index 000000000000..f66dda8224af
--- /dev/null
+++ b/src/app/components/ClientNotFoundWrapper.tsx
@@ -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 (
+
+
+
+
+
+ )
+}
diff --git a/src/app/components/NotFoundContent.tsx b/src/app/components/NotFoundContent.tsx
index e70a8385c650..6a3de43559fc 100644
--- a/src/app/components/NotFoundContent.tsx
+++ b/src/app/components/NotFoundContent.tsx
@@ -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 (
-
- )
-}
-
-function SimpleFooter() {
- return (
-
- )
-}
-
-function SimpleLead({ children }: { children: React.ReactNode }) {
- return (
-
- {children}
-
- )
-}
+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 (
-
+
- {t('oops')}
- It looks like this page doesn't exist.
-
- We track these errors automatically, but if the problem persists please feel free to
- contact us.
-
+ {t('title')}
+
+ {t('message')}
+
+ {t('contact')}
- Contact support
+ {t('contact_cta')}
-
+
)
}
diff --git a/src/app/components/ServerFooter.tsx b/src/app/components/ServerFooter.tsx
new file mode 100644
index 000000000000..d4b846381a0b
--- /dev/null
+++ b/src/app/components/ServerFooter.tsx
@@ -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 (
+
+ {t('legal_heading')}
+
+ {/* Machine translation notice for non-English languages */}
+ {currentLanguage !== 'en' && {t('machine')}
}
+
+
+
+ )
+}
diff --git a/src/app/components/hooks/useAppRouterVersion.ts b/src/app/components/hooks/useAppRouterVersion.ts
new file mode 100644
index 000000000000..ef032e8d6d62
--- /dev/null
+++ b/src/app/components/hooks/useAppRouterVersion.ts
@@ -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 ?? '')
+}
diff --git a/src/app/lib/app-router-context.ts b/src/app/lib/app-router-context.ts
index 800e1f198669..1997e009ab64 100644
--- a/src/app/lib/app-router-context.ts
+++ b/src/app/lib/app-router-context.ts
@@ -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 {
- const headersList = await headers()
+/**
+ * Create App Router context from pathname
+ */
+export function createAppRouterContext(
+ pathname: string = '/',
+ fallbackLanguage?: ClientLanguageCode,
+): AppRouterContext {
+ let language = extractLanguageFromPath(pathname)
+
+ // Use fallback if provided and URL doesn't specify language
+ if (language === 'en' && fallbackLanguage && fallbackLanguage !== 'en') {
+ language = fallbackLanguage
+ }
- // 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'
+ const version = 'free-pro-team@latest'
- // Load UI data directly from data directory the same way as Pages Router does it
+ // 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 {
},
}
}
-
-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: '',
- }
-}
diff --git a/src/app/lib/constants.ts b/src/app/lib/constants.ts
new file mode 100644
index 000000000000..0864885120f1
--- /dev/null
+++ b/src/app/lib/constants.ts
@@ -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
diff --git a/src/app/lib/language-utils.ts b/src/app/lib/language-utils.ts
new file mode 100644
index 000000000000..00f77fe68571
--- /dev/null
+++ b/src/app/lib/language-utils.ts
@@ -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}`
+}
diff --git a/src/app/lib/server-context-utils.ts b/src/app/lib/server-context-utils.ts
new file mode 100644
index 000000000000..c389b14b116a
--- /dev/null
+++ b/src/app/lib/server-context-utils.ts
@@ -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 || {},
+ }
+}
diff --git a/src/app/lib/use-detect-locale.tsx b/src/app/lib/use-detect-locale.tsx
index 410c6ca026fe..8b82d19b58eb 100644
--- a/src/app/lib/use-detect-locale.tsx
+++ b/src/app/lib/use-detect-locale.tsx
@@ -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(null)
+ const [browserLanguage, setBrowserLanguage] = useState(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])
}
/**
diff --git a/src/app/lib/version-utils.ts b/src/app/lib/version-utils.ts
new file mode 100644
index 000000000000..24f85ba75ecf
--- /dev/null
+++ b/src/app/lib/version-utils.ts
@@ -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)
+}
diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx
index 0af316061fc9..4457ccaadeef 100644
--- a/src/app/not-found.tsx
+++ b/src/app/not-found.tsx
@@ -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 (
-
-
-
- )
-}
+ // Create server context using utility function
+ const appContext = createServerAppRouterContext(pathname)
-export default NotFoundPage
+ return
+}
diff --git a/src/fixtures/fixtures/data/ui.yml b/src/fixtures/fixtures/data/ui.yml
index d749d965157a..a8ac2b8ac0d1 100644
--- a/src/fixtures/fixtures/data/ui.yml
+++ b/src/fixtures/fixtures/data/ui.yml
@@ -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
diff --git a/src/frame/lib/header-utils.ts b/src/frame/lib/header-utils.ts
deleted file mode 100644
index 5ac2d7cc000e..000000000000
--- a/src/frame/lib/header-utils.ts
+++ /dev/null
@@ -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: {},
- }),
- )
- }
-}
diff --git a/src/frame/middleware/app-router-gateway.ts b/src/frame/middleware/app-router-gateway.ts
index b0f4380ea876..1c2318277b4f 100644
--- a/src/frame/middleware/app-router-gateway.ts
+++ b/src/frame/middleware/app-router-gateway.ts
@@ -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()
}
diff --git a/src/frame/middleware/render-page.ts b/src/frame/middleware/render-page.ts
index c5fe1e7c3800..2e7b9394515c 100644
--- a/src/frame/middleware/render-page.ts
+++ b/src/frame/middleware/render-page.ts
@@ -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
{% ifversion fpt %}
-| Provider | Token | Partner | User | Push protection
+| Provider | Token | Partner | User | Push protection | Base 64 |
|----|:----|:----:|:----:|:----:|
{%- for entry in secretScanningData %}
-| {{ entry.provider }} | {{ entry.secretType }} | {% if entry.isPublic %}{% octicon "check" aria-label="Supported" %}{% else %}{% octicon "x" aria-label="Unsupported" %}{% endif %} | {% if entry.isPrivateWithGhas %}{% octicon "check" aria-label="Supported" %}{% else %}{% octicon "x" aria-label="Unsupported" %}{% endif %} | {% if entry.hasPushProtection %}{% octicon "check" aria-label="Supported" %}{% else %}{% octicon "x" aria-label="Unsupported" %}{% endif %} |
+| {{ entry.provider }} | {{ entry.secretType }} | {% if entry.isPublic %}{% octicon "check" aria-label="Supported" %}{% else %}{% octicon "x" aria-label="Unsupported" %}{% endif %} | {% if entry.isPrivateWithGhas %}{% octicon "check" aria-label="Supported" %}{% else %}{% octicon "x" aria-label="Unsupported" %}{% endif %} | {% if entry.hasPushProtection %}{% octicon "check" aria-label="Supported" %}{% else %}{% octicon "x" aria-label="Unsupported" %}{% endif %} | {% if entry.base64Supported %}{% octicon "check" aria-label="Supported" %}{% else %}{% octicon "x" aria-label="Unsupported" %}{% endif %} |
{%- endfor %}
```
diff --git a/src/secret-scanning/data/public-docs-schema.ts b/src/secret-scanning/data/public-docs-schema.ts
index ff079694e865..7021ea516302 100644
--- a/src/secret-scanning/data/public-docs-schema.ts
+++ b/src/secret-scanning/data/public-docs-schema.ts
@@ -18,7 +18,9 @@ const versionsProps = Object.assign({}, (schema.properties as Record