Repository navigation
Every docs page throws React hydration error #418 in the browser console #61
Description
Activity
Sharper diagnosis, measured while working #60 — the error is asymmetric by locale
Adding this here rather than filing a second card; it narrows the same defect rather than reporting a new one.
Prefixed-locale routes throw nothing. Only the default-locale (unprefixed) pages throw React #418.
route shape console /docs,/docs/why,/docs/quickstart,/docs/configure/ai,/docs/reference/rest-apiReact #418 hydration mismatch /zh-Hans/…,/ja/…clean ⇒ That asymmetry points at the
hideLocale: 'default-locale'middleware rewrite inlib/i18n.ts/middleware.tsas the place to look: the default locale is the one whose URL is rewritten rather than carried, so the server and client can disagree about which route they are rendering.⚠️ This is a lead, not a root cause — nobody has yet opened the middleware and confirmed the mechanism. ⛔ Do not treat the pointer as the diagnosis.Reproduced at the head of the #60 branch on pages that branch does not touch, and previously on the Deploy pages from the #59 rewrite. Pre-existing, and unrelated to either docs change.
⇒ Worth noting for whoever picks this up: the unprefixed routes are the ones most readers land on, so the blast radius is the default experience rather than an edge case.
Generated by Claude Code
Claim: PM loop round 3
Session:session_01CJPxtTxoxTUnjNdTbiEaRa
Branch:claude/issue-61-hydration-default-locale
Worktree:objectos-issue-61
Domain: n/a — objectos has no lane or seat (objectstack-ai/objectstack#9551 still open). Dispatch authorized by the maintainer under the standing 维护者直派通道, 原话: 「现在允许你派 objectos 的活」. The authorization covers dispatch; it does not settle the seat question.
File surface:apps/docs/middleware.ts,apps/docs/lib/i18n.ts,apps/docs/app/[lang]/**,apps/docs/lib/layout.shared.tsx(stop on breach; explain in the report)
Container & model: M,mode:subagent,model: opus
Clause-②: no — no contract surface;packages/speclives in another repository and nothing here changes accept/reject behaviour.
Serial constraints cleared: #64 (content/docs/architecture.mdx) and #67 (.github/scripts/,docs/TRANSLATION.md) are in the same batch with disjoint file surfaces. One read-coupling, declared rather than assumed disjoint:apps/docs/lib/i18n.tsis parsed by.github/scripts/check-translations.mjsandcheck-translation-ownership.mjs(regex overlanguages: [...]), which #67 also edits. Both dispatches carry the warning; the file-surface non-overlap does not cover it.First-touch grading
Graded
finding→ queue. Grading has never happened on this card; it is done here rather than at filing time, per protocol.Promoted because it has a named landing point, a reproduction path, and a measured blast radius: the asymmetry recorded in the comment above puts the fault on the unprefixed routes, which are the ones most readers land on. "Nothing is visibly broken" is why it could sit here forever, not a reason it should.
Premise re-verified on
origin/mainbefore dispatchapps/docs/middleware.tshas not changed sincebb8ab38, which predates this card — the finding is not stale.- The
hideLocale: 'default-locale'branch is still present and still doesNextResponse.rewrite(url)from an unprefixed path to/${defaultLanguage}${pathname}, which is consistent with the recorded asymmetry.
That consistency is not a diagnosis. It is carried to the dev as a mechanism assumption to be tested, exactly as the comment above frames it.
Generated by Claude Code
{ "issue": 61, "status": "done", "branch": "claude/issue-61-hydration-default-locale", "pr": "https://github.com/objectstack-ai/objectos/pull/73", "premise_still_valid": true, "summary": "Reproduced on a production build, then fixed at the mechanism. The PM pointer (hideLocale 'default-locale' rewrite) was correct as a pointer; the mechanism under it is that `usePathname()` reports the INTERNAL route to the build-time prerender (`/en/docs/quickstart`) and the PUBLIC URL to the browser (`/docs/quickstart`), while Fumadocs compares it against page-tree urls that are always in the public space. The server's `searchPath()` therefore missed the current page and rendered no active sidebar item, no expanded folder, no page title in the TOC trigger and no prev/next footer; the client rendered all four. Fix normalises the pathname at `FrameworkProvider` — the single injection point through which all 19 Fumadocs modules read a pathname — via a new `apps/docs/app/[lang]/root-provider.tsx` that wires RootProvider to Next exactly as `fumadocs-ui/provider/next` does, minus the raw pathname. No suppressHydrationWarning, no hideLocale change, no consumer-side tolerance. Two deviations to flag, both non-blocking: (a) the two new/changed files sit at `apps/docs/app/[lang]/**`, inside the declared file surface, but `root-provider.tsx` is a NEW file rather than an edit of a named one — colocated there deliberately so nothing lands outside the declared surface, and in particular so `apps/docs/lib/i18n.ts` (the #67 read-coupling) stays byte-identical; (b) the PR-body footer uses the session-URL form of the Claude Code link rather than the bare form the dispatch specified, per the standing clause that the bare form is dropped by GitHub on every subsequent body edit.", "tests": "All gates run at the tree of commit d9ab7f4 (worktree /home/user/objectos-issue-61, since removed). BEFORE (production build, `next start`, headless Chromium, pageerror+console captured): /docs, /docs/why, /docs/quickstart, /docs/architecture -> 'Minified React error #418; ...args[]=text&args[]='; /docs/configure/ai, /docs/reference/rest-api -> same with args[]=HTML; /zh-Hans/docs/quickstart and /ja/docs/quickstart -> CLEAN. AFTER: all eight CLEAN, plus /, /privacy, /terms, /docs/deploy/docker, /de/docs/quickstart, /ko/docs, /zh-Hans/privacy all CLEAN. Mechanism verified in the server-rendered HTML, not only the console: EN SSR /docs/quickstart sidebar data-active false->true and TOC trigger 'On this page' (fallback) -> 'Quickstart' (real title); EN SSR /docs/configure/ai containing folder closed -> data-state=\"open\"; ZH SSR /zh-Hans/docs/quickstart unchanged in every respect (already true, already the title) — that ZH/EN SSR asymmetry before the fix is the direct evidence for the pathname diagnosis. Structural SSR-vs-DOM diff on the fixed EN route reduces to the same benign post-hydration residue the clean ZH control has (DOCTYPE, cmd->Ctrl key hint, TOC scroll tracker, next-route-announcer). Redirects unchanged: /en/docs/quickstart -> 307 /docs/quickstart, /cn/docs/quickstart -> 308 /zh-Hans/docs/quickstart, / -> 307 /docs. Browser affordances after fix: sidebar click = client-side nav (0 full page loads) with active state following, language switcher /docs/architecture -> /zh-Hans/docs/architecture, theme toggle light -> dark, zero console errors across the whole interactive run. CI gate set: `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 — see #72). Read-coupling scripts both run and reported: `node .github/scripts/check-translations.mjs` exit 0, '✓ translations gate passed'; `node .github/scripts/check-translation-ownership.mjs --files <changed> --actor os-zhuang` exit 0, '⚠ TRANSLATION_BOT_LOGIN is not set — ownership is not enforced yet. This PR touches 0 translation artifact(s) and 2 other file(s).' Control-byte scan of both changed files: clean. No changeset (this repo has none). No locale-tagged mdx sibling touched. NOTE: the first type-check run failed (TS2322 on the Link/Image Framework slots — Next's components require href/src where Fumadocs types them optional); bridged with two named casts to `Framework['Link']`/`Framework['Image']`, which is what `fumadocs-ui/provider/next` does at runtime, then green.", "open_questions": [], "out_of_scope_findings": [ "not filed — duplicate of existing #72 (`finding`): `pnpm turbo run test` matches zero scripts and exits 0, so the CI test job cannot go red. Independently confirmed by this run's '0 successful, 0 total'.", "not filed — duplicate of existing #68: check-translation-ownership.mjs prints '⚠ TRANSLATION_BOT_LOGIN is not set — ownership is not enforced yet' and returns success, observed while running it for the declared read-coupling.", "not filed — upstream, not objectos: any Fumadocs site combining hideLocale with prerendered routes has this defect. Its own createI18nMiddleware performs the same default-locale rewrite and framework/next passes Next's usePathname through unmodified. Recorded in the PR body; worth an upstream report, and the fix here does not depend on one." ] }
Generated by Claude Code
ACCEPT — #73.
Reviewed against GitHub,
origin/mainand the vendor sources innode_modules, not against the report's account of itself.The diagnosis is a mechanism, and it is load-bearing
The dispatch carried the
hideLocalepointer explicitly labelled not a diagnosis. The PR treated it that way: it confirmed the pointer, then found what is under it —usePathname()reports the internal route to the build-time prerender (/en/docs/quickstart) and the public URL to the browser (/docs/quickstart), while every page-treeurlproduced byloader({ i18n })is in the public space. The server's tree lookup misses, so it renders no active sidebar item, no expanded folder, no TOC page title, no prev/next; the client renders all four; React discards the subtree.Two things make that a real diagnosis rather than a plausible story, and both are independently checkable:
- the ZH/EN SSR asymmetry before the fix — same build, same page,
data-activefalse on EN and true on ZH — which no explanation other than the pathname can produce; - the two React error argument shapes lining up with what each page loses: top-level pages lose the TOC trigger's title (a
textnode), pages inside a folder additionally lose the expanded folder and prev/next (HTMLstructure). That correlation was not needed to fix anything, which is what makes it good evidence.
The two things I flagged for review before the report arrived
The hand-assembled provider is correct, and forking the vendor wiring was the right call. Verified in
node_modules:fumadocs-ui/dist/provider/next.jswrapsRootProvider(from./base.js) insideNextProvider. So aFrameworkProviderlayered outside the importedRootProviderwould be overridden by the inner one — the stated reason for not importing it is the actual behaviour, not a guess.fumadocs-ui/dist/provider/base.jscontains zero references toFrameworkProvider, sobase+ an own provider reproduces the vendor's composition faithfully.fumadocs-core/dist/framework/next.jspasses exactlyusePathname, useRouter, useParams, Link: CustomLink ?? Link, Image: CustomImage ?? Image.root-provider.tsxmirrors that argument for argument, substitutingusePublicPathname. The delta against the vendor is one function.
The
Link/Imagecasts are not papering over a defect. The vendor hands Next's ownLinkandImageto those same slots at runtime, so the casts state a contract fumadocs already relies on; the disagreement is between declarations (Next requireshref/src, Fumadocs types them optional) and it is in the caller's favour. Accepted as written.Other checks
Item Reading Scope 2 files, both under apps/docs/app/[lang]/**— inside the declared surface.lib/i18n.tsbyte-identical, so the declared read-coupling with #67 did not fireNothing suppressed No suppressHydrationWarning, nohideLocalechange, no consumer-side tolerance — the 裁决 heldtoPublicPathnameedge casesReads hideLocale/defaultLanguagefromlib/i18n.tsat runtime rather than hardcoding; strips only a whole leading segment that is a supported locale (/end-to-endsurvives);/en→/;/zh-Hans/…untouched underdefault-localeVerification Both directions on the same build pipeline — six unprefixed routes red→clean, two prefixed routes clean→clean, plus seven more routes and three redirect shapes CI buildgreen ond9ab7f4Two things recorded rather than blocking
The
Translationsworkflow did not run on this PR at all — itspaths:filter coverscontent/docs/**,lib/i18n.tsand the scripts, and this PR touches none of them. That is the filter working as designed, and the dev ran both scripts locally and reported their output instead. Worth knowing before anyone makes that job a required check: a PR outside those paths would wait on a check that never starts.A maintenance hazard, named because it will not announce itself: this mirrors a vendor composition. A future
fumadocs-ui/fumadocs-coreupgrade that changes whatNextProviderpasses toFrameworkProviderwill not break the build here — it will silently stop mirroring. The header comment explains the why, which is the part that survives; whoever bumps fumadocs should re-readframework/next.jsagainstroot-provider.tsx. Not a blocker, and the alternative (patching every consumer) is worse.Out-of-scope findings
Correctly filed nothing new: two were identified as duplicates of #72 and #68 and left alone, which is the right call — a duplicate card costs more than it records. The third is upstream, not this repository: any Fumadocs site combining
hideLocalewith prerendered routes has this,createI18nMiddlewareincluded. Recorded in the PR body; worth reporting upstream, and this fix does not depend on that landing.Landing
Flipping to ready. Not merging — objectos has no merge queue, and merging outside one is not a path this protocol grants a PM seat (objectstack-ai/objectstack#9551).
Generated by Claude Code
- the ZH/EN SSR asymmetry before the fix — same build, same page,
Observation, filed unassigned. Measured while browser-verifying the Deploy rewrite (#59); it is not caused by that change.
What was measured
Production build of
apps/docs(pnpm run build, thennext start), driven with headless Chromium. Every page navigated logs exactly onepageerror:Pages checked, all identical:
/docs/architecture,/docs/quickstart,/docs/deploy/docker(rewritten),/docs/deploy/air-gapped(rewritten),/zh-Hans/docs/deploy/air-gapped. Untouched pages and rewritten pages behave the same, which is what makes it a pre-existing site-wide condition rather than a content defect.Error 418 is a hydration mismatch: the server-rendered HTML does not match what the client renders on first pass, so React discards the server markup for that subtree and re-renders it on the client.
Why it is worth a look
Nothing is visibly broken, so this could sit here indefinitely. But it is worth closing because a standing hydration error costs a client re-render on every page load, and — more expensively for whoever debugs the next real UI bug — it makes the console noisy enough that a genuine error blends in.
Likely candidates, in order: a theme or locale value read at render time, or a timestamp / locale-formatted string rendered without being pinned to a deterministic value. A non-minified dev build names the offending subtree directly.