Repository navigation
fix(docs): light the top nav on a blog post — fumadocs matches against the public path, not the /en prerender path - #23028
Merged
objectstack-fleet[bot] merged 2 commits intoOct 12, 2026
Conversation
…ht on a blog post Posts prerender under app/[lang]/ at /en/blog/SLUG, and Next's usePathname() answers that internal path during the prerender while every nav link (and the docs page tree) is public. The browser-side pathname is already public, so hydration read a match the server HTML never rendered and React kept the server's data-active=false. The root provider is now fumadocs' NextProvider composition with usePathname mapped through toPublicPath() (the hidden default-locale prefix removed, the proxy's own /en redirect mapping), so the prerender agrees with hydration. Claude-Session: https://claude.ai/code/session_012yK1ddAdAqfzwigRHZq3r2 Co-authored-by: Claude <noreply@anthropic.com>
…props Claude-Session: https://claude.ai/code/session_012yK1ddAdAqfzwigRHZq3r2 Co-authored-by: Claude <noreply@anthropic.com>
objectstack-fleet
Bot
deleted the
claude/issue-22977-docs-nav-active-blog
branch
October 12, 2026 11:29
This was referenced Oct 12, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #22977
Clause-②: no
What changed
The docs site's root provider now hands fumadocs the public pathname.
apps/docs/app/[lang]/layout.tsxusedRootProviderfromfumadocs-ui/provider/next, which isNextProvider(Next'susePathname/useRouter/useParams,next/link,next/image) around the baseRootProvider. The newapps/docs/components/root-provider.tsxis that same composition. It changes one thing:usePathnameis mapped throughtoPublicPath()(new, inapps/docs/lib/i18n.ts), which drops the hidden default-locale prefix (/en/blog/SLUGbecomes/blog/SLUG,/enbecomes/). Every other path is returned unchanged. This is the mappingproxy.tsalready applies when it redirects/en/..., and it reads the samei18nconfig.lib/layout.shared.tsxgains a two-line comment pointing the nav entries at it. Four files, +70 / -1, all inapps/docs. No dependency change and no fumadocs upgrade.Why: the mechanism, measured
.next/prerender-manifest.jsonlists the posts as/en/blog/SLUG, because every page lives underapp/[lang]/. During that prerender, Next'susePathname()answers/en/blog/SLUG.isLinkItemActive→isActive(link.url, pathname, nested)(dist/utils/urls.js) compares that against the publicurls,/blogand/blog/the-ontology-is-the-software, so it never matches.canonicalUrlfromlocation.href(create-initial-router-state.js), so the client'susePathname()already answers the public/blog/SLUG.data-active=true, but React 19 does not patch an attribute that mismatches during hydration. The DOM keeps the server'sfalse./blogis not ingenerateStaticParams. It renders on demand with the request path, and that is why the control lit.On the client,
toPublicPathis the identity:proxy.tsredirects every/en/...URL away, so a browser pathname never carries the prefix. The change only makes the server prerender agree with what hydration already reads. The whole-page diff below measures this.Route chosen: match against the public path
The other route was to prerender the posts under the public path. That means moving the blog route out of
app/[lang]/and exempting/blog/*from the proxy's rewrite. It would undo the i18n routing thatproxy.tskeeps on purpose, so that a future language needs only config and content. It would also fix only the blog. The docs pages are prerendered the same way (/en/docs/...) and carry the same defect, measured below. Matching against the public path fixes both at the one place fumadocs reads the pathname, and it changes no route.Measurements: before (main) and after (this branch)
Both builds use
pnpm --filter @objectstack/docs build(the CIBuild Docscommand), served withnext starton a random port:9432bac07, built in a detached comparison worktree.7324c3cb2.Each page was read twice:
fetch.networkidle+ 1.5 s).Values are the
data-activeof the nav anchors./blog/context-window-is-the-constraint/blog/metadata-driven-architecture/blog/protocol-first-development/blog/the-ontology-is-the-software(the essay)/blog(control)/(control, no false positive)/docs/build-without-code(control)Bloglights on the essay as well asWhy, becauseBlogisnested-urland the essay sits under/blog. That is what the card's "Blog on every post via nested-url" asks for.A second run against a fresh
next startof the main build gave a byte-identical reading.Every other nav item, and every other pathname reader
The nav array is
Why(url),Blog(nested-url) and the appended GitHub icon item. The GitHub item'surlis absolute (https://github.com/objectstack-ai/objectstack), so it can never equal a pathname. It readsdata-active=falseon every page in both builds.Because the swap is at the provider, every fumadocs pathname reader sees it, not only the nav. So the check went wider than the nav array. I diffed, across 9 pages (
/,/blog, the four posts,/docs,/docs/build-without-code,/docs/upgrading), the multiset of every anchor'shrefand of everydata-activeanchor, in the server HTML and in the hydrated DOM, main against fix. The complete diff:/,/blog: no difference, server or hydrated.data-activeflips in the table above. Hrefs are unchanged.data-active=true(/docs/build-without-code,/docs/upgrading);/docs/getting-started·/docs/capabilities/request-template, and/docs/api/wire-format·/docs/protocol-upgrade).Same defect, docs half
The docs pages are prerendered under
/en/docs/..., so they had the same defect:So the hydrated docs page looked right, after a recoverable error and a re-render. The blog's attribute-only mismatch raised no error and was never repainted. This PR adds no code for the docs half: it is fixed by the same
usePathnamemapping. That falls inside the claim's declaredapps/docs/**surface, and the hydrated docs DOM is unchanged.Verification
All at
7324c3cb2:pnpm --filter @objectstack/docs typecheck(fumadocs-mdx && next typegen && tsc --noEmit): exit 0. The first run went red on the provider'sLink/Imageprop types, which proves the new file is in the program. Fixed in the second commit.pnpm --filter @objectstack/docs build: exit 0, on both main and this branch.node scripts/pm/dispatch-gates.mjs --commandson this diff gave 27 commands: the dispatch list pluspnpm check:docs-locale-catch-all, whichlib/i18n.ts/app/[lang]/layout.tsxpull in. All 27 exit 0.--ranreconciliation: "27 derived famil(ies) accounted for — 27 run, 0 NOT-MEASURED (a DERIVED zero …)".eslint --no-inline-config --format jsonreports 4 files, 0 errors, 0 warnings. The narrowing is proven three ways:ESLint.isPathIgnoredis false andcalculateConfigForFileresolves for all four, so they are in the linted population;parserOptions.projectineslint.config.mjs;calculateConfigForFilereports none), so this diff cannot move any untouched file's verdict.apps/docshas no test runner, and the card rules out a new gate. The pin is the before/after measurement above.Build Docsin CI: not measured here; CI runs it.Changeset
None.
@objectstack/docsis"private": trueand publishes nothing, so the PR carriesskip-changeset.Acceptance notes
toPublicPath()andproxy.ts's/en/...redirect are two spellings of one mapping, both read fromlib/i18n.ts's config. The proxy was left untouched to keep this diff off its matcher surface. Folding the redirect ontotoPublicPath()would leave one spelling. Noted, not filed; carrier: none.Generated by Claude Code