Uh oh!
There was an error while loading. Please reload this page.
Resolve Markdown document links from source paths - #137
Resolve Markdown document links from source paths#137guanzhousongmicrosoft wants to merge 3 commits into
Conversation
Signed-off-by: Guanzhou Song <guanzhousong@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Two gaps in the resolver, both found in review. A cross-section link naming api-reference resolved to a route that does not exist. content.config.json publishes the docs repository's api-reference/ folder at /docs/reference, and the source paths callers pass are already in website space, so a link written the only way an author can write it - ../api-reference/operators/aggregation/$limit.md, against the folder they can see - resolved to /docs/api-reference/... and 404'd. That is the one folder whose published name differs; every other section keeps its own, so the mapping is a single entry rather than a copy of the config. The resolved route also skipped NEXT_BASE_PATH. Markdown links render as plain anchors rather than next/link, so nothing applies the base path for us, and a subpath deployment would have emitted /docs/... - correct only at the domain root. withBasePath now wraps the path, leaving the query and fragment after it. Navbar already does this from a client component, so this follows the pattern the codebase settled on rather than introducing one. Covered both: the mapping from an article page and from inside the reference section, a section that is not remapped, and the base path across a plain route, a mapped route, one carrying a query and fragment, and links the resolver declines to touch. The base path tests re-import the module after stubbing the environment, since sitePath reads it once at module scope. npm install fails against the registry proxy on this machine, so the vitest suite is left to CI. The resolver itself was exercised directly under node with type stripping, over every case above, with and without a base path configured.
guanzhousongmicrosoft
commented
Aug 4, 2026
Both review findings confirmed and fixed in 1. Cross-section Worth noting why this is the only affected mapping: 2. Coverage — eight new cases: the mapping from an article page and from inside the reference section, a section that is not remapped (guarding against over-eager rewriting), and the base path across a plain route, a mapped route, one carrying a query and fragment, and links the resolver declines to touch. The base-path tests re-import the module after stubbing the environment, since On verification, honestly: |
The previous fix was wrong in a way its tests could not see. Markdown is a client component, so markdownLinks ships in the browser bundle, and NEXT_BASE_PATH is not NEXT_PUBLIC_-prefixed: Next strips it from that bundle. Under a configured base path the static render - which runs in node, where the variable exists - emitted /preview/docs/..., and the same code after hydration emitted /docs/..., so the href changed under the reader and React had a mismatch to reconcile. Nothing in the exported HTML shows this, because the export is produced by the server-side render that reads the variable correctly. The tests passed for the same reason: vitest runs in node. The resolver is pure again and returns a base-path-relative route. The anchor renderer now uses next/link for a rewritten href, which is by definition an internal document route, and next/link applies the basePath from next.config on both the server and the client. Hrefs the resolver leaves alone still render as plain anchors, so external links, fragments and hand-written absolute paths behave exactly as before. Coverage follows the same reasoning. The base-path tests now assert the route is unchanged whether or not NEXT_BASE_PATH is set, and a source guard asserts neither client-reachable module mentions NEXT_BASE_PATH, sitePath or withBasePath. A build-level check would not have caught this and would not catch a regression: the markup on disk is correct in both the broken and the fixed version. Proving the browser behaviour would need hydration testing, which the project has no harness for today - jsdom and testing-library are not dependencies - so the guard is placed where the mistake is made instead. Note Navbar has the same latent defect: it calls withBasePath at module scope in a client component, for the blogs anchor and the logo image. Left alone here, since it is a different surface and this branch should not grow to cover it.
guanzhousongmicrosoft
commented
Aug 4, 2026
You are right, and my previous fix was wrong in a way its own tests could not detect. Corrected in The mechanism. Two things made this easy to miss, and both are worth recording:
I also cited The fix. The resolver is pure again and returns a base-path-relative route. The anchor renderer uses Coverage, following the same reasoning rather than the original request:
On "browser/build-level coverage" specifically: build-level cannot catch this, for the reason above. Browser-level would need hydration testing, and the project has no harness for it — Related, not fixed here. Verified locally as far as the environment allows: the resolver returns identical routes with and without |
guanzhousongmicrosoft
commented
Aug 4, 2026
Reopened as #141 from a personal fork, so the branch and the pull request are owned by the same account that authors the commits. The tree is identical — the same three commits, unchanged. The review discussion above stays here rather than moving: the Closing in favour of #141. |
withBasePath reads NEXT_BASE_PATH, which is a private build variable, so Next strips it from the browser bundle. Navbar is a client component and calls it at module scope for the blogs anchor and the logo image: under a configured base path the static render produced /preview/blogs/, and the same code after hydration produced /blogs/, changing the href under the reader and leaving React a mismatch to reconcile. Nothing on disk shows this. The export is written by the server-side render, which reads the variable correctly, so the emitted markup is right in both the broken and the fixed build - only a real browser disagrees. That is what made it survive review twice. next.config now republishes the normalized value as NEXT_PUBLIC_BASE_PATH, which Next inlines into both bundles, and sitePath prefers it while keeping the private variable as a fallback for server-only callers and standalone scripts that never see the republished one. Deployments still set the single variable they already set, and every existing caller is fixed without being touched, including any added later. Covered with the environment stubbed both ways: the public variable, the private fallback, agreement between them, slash normalization, an empty republished value meaning no base path, and relative and absolute URLs left alone. Exercised directly under node across all four combinations of the two variables. The vitest suite is left to CI, since npm install fails against the registry proxy on this machine. Found while fixing the same defect in the Markdown link resolver (#137), which solves it differently - that path is an internal route, so it can hand the prefixing to next/link and read no environment at all.
Summary
.mdlinks against the Markdown source file instead of the rendered page URLindex.mdtargets with trailing-slash website routes while preserving queries and fragmentsThis removes the need to rewrite every cross-document Markdown link as an absolute
documentdb.ioURL and fixes the long-term rendering gap tracked in the docs repository.Fixesdocumentdb/docs#38
Verification
npm test(112 tests)npm run linttsc --noEmitnpm run build:next