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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/docs/app/[lang]/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { ReactNode } from 'react';
import { RootProvider } from 'fumadocs-ui/provider/next';
import { RootProvider } from '@/components/root-provider';
import { notFound } from 'next/navigation';
import { i18n, isSupportedLanguage } from '@/lib/i18n';

Expand Down
46 changes: 46 additions & 0 deletions apps/docs/components/root-provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
'use client';

import { type Framework, FrameworkProvider } from 'fumadocs-core/framework';
import { RootProvider as BaseRootProvider, type RootProviderProps } from 'fumadocs-ui/provider/base';
import Image from 'next/image';
import Link from 'next/link';
import { useParams, usePathname, useRouter } from 'next/navigation';
import { toPublicPath } from '@/lib/i18n';

/** Next's pathname, as the visitor's URL spells it — see `toPublicPath()`. */
function usePublicPathname(): string {
return toPublicPath(usePathname());
}

/**
* `fumadocs-ui/provider/next`'s `RootProvider`, with one difference: the
* pathname every fumadocs component reads is the PUBLIC path.
*
* That provider is `NextProvider` (Next's hooks, `next/link`, `next/image`)
* around the base `RootProvider`; this is the same composition with
* `usePathname` swapped for `usePublicPathname`. (The two casts: Next's
* `href`/`src` are required where fumadocs' prop types leave them optional;
* `NextProvider` hands these same two components to these same props.)
*
* Without it, a page prerendered under `app/[lang]/` renders its nav and sidebar
* against `/en/...` while every link they match is public, so the top nav's
* `Blog` (nested-url) and `Why` (url) entries never lit on a blog post, and the
* docs sidebar's current page was unmarked in the server HTML.
*
* In the browser Next already answers the public path — `proxy.ts` redirects
* any `/en/...` URL away — so the mapping changes nothing there; it only makes
* the prerender agree with what hydration reads.
*/
export function RootProvider(props: RootProviderProps) {
return (
<FrameworkProvider
usePathname={usePublicPathname}
useRouter={useRouter}
useParams={useParams}
Link={Link as Framework['Link']}
Image={Image as Framework['Image']}
>
<BaseRootProvider {...props} />
</FrameworkProvider>
);
}
21 changes: 21 additions & 0 deletions apps/docs/lib/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,24 @@ export const i18n = defineI18n({
export function isSupportedLanguage(value: string): boolean {
return (i18n.languages as readonly string[]).includes(value);
}

/**
* The public URL path for a route path: the hidden default-locale prefix
* removed (`/en/blog/x` → `/blog/x`, `/en` → `/`); any other path unchanged.
*
* Every page lives under `app/[lang]/`, so a page prerendered at build time is
* rendered at its internal path — `/en/blog/x` — and that is what Next's
* `usePathname()` answers during the prerender. A visitor never sees that path:
* `proxy.ts` rewrites `/blog/x` to it and redirects `/en/...` away, and the
* browser-side `usePathname()` answers the public `/blog/x`. Every URL fumadocs
* compares a pathname against (nav links, the docs page tree) is public too, so
* the server-side comparison missed on every prerendered page and hydration
* never repainted the result. This is the same mapping as the proxy's
* `/en/...` redirect, read from the same config.
*/
export function toPublicPath(pathname: string): string {
if (i18n.hideLocale !== 'default-locale') return pathname;
const prefix = `/${i18n.defaultLanguage}`;
if (pathname === prefix) return '/';
return pathname.startsWith(`${prefix}/`) ? pathname.slice(prefix.length) : pathname;
}
2 changes: 2 additions & 0 deletions apps/docs/lib/layout.shared.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export function baseOptions(): BaseLayoutProps {
},
// Every layout spreads baseOptions() — DocsLayout (docs sidebar), and HomeLayout on
// the home page and on /blog — so these entries are the nav's links on all of them.
// `active` is matched against the PUBLIC pathname (`/blog/x`, never the `/en/blog/x`
// a post is prerendered under) — `components/root-provider.tsx` supplies it.
links: [
{ text: 'Why', url: ESSAY_URL, active: 'url' },
{ text: 'Blog', url: '/blog', active: 'nested-url' },
Expand Down
Loading