Skip to content

fix(docs): give Fumadocs the public pathname on default-locale routes - #73

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-61-hydration-default-locale
Aug 18, 2026
Merged

fix(docs): give Fumadocs the public pathname on default-locale routes#73
os-zhuang merged 2 commits into
mainfrom
claude/issue-61-hydration-default-locale

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes#61

What was wrong

Every unprefixed docs page logged one React #418 (hydration mismatch) in the browser console; /zh-Hans/… and /ja/… were clean. Reproduced on a production build before touching anything.

The pointer in the issue comment — the hideLocale: 'default-locale' rewrite — was the right place to look, and the mechanism underneath it is this:

hideLocale: 'default-locale' keeps English out of the public URL, so /docs/quickstart is served by rewriting to the /en/docs/quickstart route. The two renderers then observe different pathnames for one page:

rendererusePathname()
build-time prerender of the route/en/docs/quickstart
browser, at the public URL/docs/quickstart

Fumadocs compares that pathname against the url of every page-tree node, and those are produced by loader({ i18n }) in the public space (/docs/quickstart, locale hidden). So on the server searchPath() looked the current page up in the tree and missed, and the page rendered as if no page were current — no active sidebar item, no expanded folder, no page title in the TOC trigger, no prev/next footer. The client, at the public URL, rendered all four. React discarded the subtree and re-rendered it.

Prefixed locales are unaffected because their internal and public paths are identical — which is exactly the asymmetry recorded on the issue.

Measured directly, same build, same page, server-rendered HTML:

EN SSR /docs/quickstart sidebar active=false TOC trigger "On this page" (fallback)
ZH SSR /zh-Hans/docs/quickstart sidebar active=true TOC trigger "快速开始" (page title)

The only difference between those two is the /en prefix the browser never sees.

The fix

FrameworkProvider is the single point at which a pathname enters Fumadocs — all 19 of its modules that need one read it through usePathname from fumadocs-core/framework, and fumadocs-core/framework/next is the only place Next's usePathname is injected. Normalising it there fixes every consumer at once.

app/[lang]/root-provider.tsx wires RootProvider to Next exactly as fumadocs-ui/provider/next does, except the usePathname handed to Fumadocs reports the public URL. The provider is assembled by hand rather than imported because fumadocs-ui/provider/next wraps RootProvider in its own FrameworkProvider, which would take precedence over one layered outside it.

toPublicPathname() mirrors Fumadocs' own hideLocale URL rules so the two stay in agreement: never keeps every prefix, default-locale hides only the default language, always hides every supported one. Only a whole leading segment that is a supported locale is removed, so a page such as /end-to-end is left alone.

Nothing is suppressed: no suppressHydrationWarning, no change to hideLocale, no consumer-side tolerance. The server now computes the same value the client does, so there is no client re-render left to hide.

Verification

Production build (pnpm --filter @objectos/docs build, then next start), driven with headless Chromium, pageerror + console captured. Both directions, same build pipeline before and after.

Before — the six unprefixed routes named on the card:

=== /docs === Minified React error #418; …args[]=text&args[]=
=== /docs/why === Minified React error #418; …args[]=text&args[]=
=== /docs/quickstart === Minified React error #418; …args[]=text&args[]=
=== /docs/configure/ai === Minified React error #418; …args[]=HTML&args[]=
=== /docs/reference/rest-api === Minified React error #418; …args[]=HTML&args[]=
=== /docs/architecture === Minified React error #418; …args[]=text&args[]=
=== /zh-Hans/docs/quickstart === CLEAN
=== /ja/docs/quickstart === CLEAN

(The two argument shapes line up with the mechanism: top-level pages lost the TOC trigger's page title — a text node; pages inside a folder additionally lost the expanded folder and the prev/next footer — HTML structure.)

After, at d9ab7f4:

=== /docs === CLEAN
=== /docs/why === CLEAN
=== /docs/quickstart === CLEAN
=== /docs/configure/ai === CLEAN
=== /docs/reference/rest-api === CLEAN
=== /docs/architecture === CLEAN
=== /zh-Hans/docs/quickstart === CLEAN
=== /ja/docs/quickstart === CLEAN

Plus /, /privacy, /terms, /docs/deploy/docker, /de/docs/quickstart, /ko/docs, /zh-Hans/privacy — all clean. Redirects unchanged: /en/docs/quickstart → 307 /docs/quickstart, /cn/docs/quickstart → 308 /zh-Hans/docs/quickstart, / → 307 /docs.

The mechanism, not just the symptom — server-rendered HTML for the same page, before vs after:

EN SSR /docs/quickstart sidebar active false -> true
TOC trigger "On this page" (fallback) -> "Quickstart"
EN SSR /docs/configure/ai containing folder closed -> data-state="open"
ZH SSR /zh-Hans/… unchanged in every respect (already true / already the title)

Affordances exercised in the browser after the fix, on unprefixed routes: sidebar click navigates client-side (0 full page loads, next/link still wired) and the active item follows; the language switcher goes /docs/architecture/zh-Hans/docs/architecture; the theme toggle flips lightdark. Zero console errors across the whole interactive run.

Gates, at the tree of d9ab7f4:

pnpm turbo run type-check --continue 1 successful, 1 total
pnpm turbo run build 1 successful, 1 total (740/740 static pages)
pnpm turbo run test 0 successful, 0 total (no package defines a test script)
node .github/scripts/check-translations.mjs exit 0 — "✓ translations gate passed"
node .github/scripts/check-translation-ownership.mjs --files … exit 0 — 0 translation artifacts, 2 other files

Both translation scripts were run because they parse apps/docs/lib/i18n.ts and a sibling card is editing them. This change does not touch lib/i18n.ts, so the languages: [...] shape they regex over is untouched; the runs confirm it. No locale-tagged .mdx sibling is touched.

Note for upstream

Any Fumadocs site combining hideLocale with prerendered routes has this, createI18nMiddleware included — its default-locale branch performs the same rewrite, and framework/next passes Next's usePathname through unmodified. Worth reporting upstream; the fix here does not depend on that landing.


Generated by Claude Code

Every unprefixed docs page logged React #418 (hydration mismatch) in the
browser console; `/zh-Hans/…` and `/ja/…` were clean.
`hideLocale: 'default-locale'` keeps English out of the public URL, so
`/docs/quickstart` is served by rewriting to the `/en/docs/quickstart`
route. The two renderers then see different pathnames for one page: the
build-time prerender knows the internal route, the browser knows the
public URL. Fumadocs compares that pathname against page-tree `url`s,
which are in the public space, so the server looked the current page up
and missed — no active sidebar item, no expanded folder, no page title in
the TOC trigger, no prev/next footer — while the client rendered all
four. Prefixed locales are unaffected because their internal and public
paths are identical, which is exactly the recorded asymmetry.
Normalise the pathname at `FrameworkProvider`, the single point where one
enters Fumadocs, so both renderers agree on the public URL. This needs
the provider assembled by hand: `fumadocs-ui/provider/next` wraps
`RootProvider` in its own `FrameworkProvider`, which would take
precedence over one layered outside it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CJPxtTxoxTUnjNdTbiEaRa
@os-zhuang
os-zhuang merged commit 511e2e4 into mainAug 18, 2026
1 check passed
@os-zhuangClaude

Copy link
Copy Markdown
ContributorAuthor

Merged as 511e2e4 under a direct maintainer instruction, quoted verbatim for the audit trail: 「ci 绿了就合并」.

Updated from main before merging so it was re-checked against the repaired gate. The Translations job still does not run on this PR — its paths: filter does not match apps/docs/app/** — so build remains its only check, as recorded in the review.

Verified on main after landing: root-provider.tsx is in place.


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Every docs page throws React hydration error #418 in the browser console

2 participants

@os-zhuang@claude