Repository navigation
docs: fix the site's broken internal links and check every built link - #591
Merged
Merged
Conversation
Relative `.md` links in the docs tree are now rewritten to page routes on the rendered site (`src/lib/relative-md-links.ts`), so `rpc-tools.md` on /reference/public-api/ links /reference/rpc-tools/ instead of a 404 one directory too deep. The llms bundles inherit the fix. The raw twins keep their relative links, which resolve there. The JSON-LD graph names the root twin as `index.md` (it named the dotfile `.md`, a 404), and the 404 page no longer names a social card the build never writes. `tests/built-links.test.ts` resolves every link on every built page, twin, llms bundle, JSON-LD graph and sitemap against dist/ the way a browser does, fragments included, and the links validator now fails the build on any relative link that reaches it.
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.
What and why
The published site had 444 broken internal links (143 distinct targets), all from relative
.mdlinks in the docs tree. For example,[daemon API reference](rpc-tools.md)on https://laithalsaadoon.github.io/microvms-agentd/reference/public-api/ fetched/microvms-agentd/reference/public-api/rpc-tools.md, a 404. Sätteri emits a link target as written, and each rendered page is served one directory deeper than its source (reference/public-api.mdat/reference/public-api/), so every relative.mdlink on a rendered page resolved one level too deep: 148 links on 29 pages.starlight-llms-txtflattens the rendered pages intollms-full.txtandllms-small.txt, which carried the same 148 links each, resolved from the site root: 144 landed beside the bundle and 152 climbed above the base with../. The raw.mdtwins were correct, because there each relative target sits beside its source.No gate resolved these.
starlight-links-validatorwas configured to skip every.mdlink and every relative link, and the config comment that justified it said Astro rewrites a relative link to a route in the rendered page. It does not.tests/raw-route-links.test.tsresolves links in the twins only.The fix
site/src/lib/relative-md-links.tsis a Sätteri mdastlinkvisitor. It resolves a relative.mdtarget against the source document's path undersrc/content/docsand rewrites it to the target page's root-relative route, keeping the fragment:rpc-tools.mdbecomes/reference/rpc-tools/.starlightBasePath(), whose Sätteri pass is pushed after the configured plugins, then adds the base. A relative.mdtarget with no file under the content root fails the build with the document and line, and so does one whose path uses characters outside[a-z0-9_-], because Astro slugs those with github-slugger and the lowercased path would no longer be the route. Every page in the tree meets that rule. The twins never pass through this processor and keep their relative links.site/src/lib/jsonld.tsbuilds the page's MarkdownencodingURL withrawMarkdownUrl, the helper the<head>alternate link already uses. It built the URL by hand and named the root twin with Starlight's dotfile spelling,/microvms-agentd/.md(live: 404), while the site publishes it asindex.md(live: 200).site/src/components/Head.astroemits theog:imageandtwitter:imagetags only on a page that has a card. The card route renders one card perdocscollection entry, keyed byogId, and Starlight's built-in 404 page is outside that collection, so it named/og/404.png, which the build never wrote (live: 404).The checks
site/tests/built-links.test.ts(default vitest tier, so CI's required "build and gate the docs site" job runs it in "gate the built surface", and the deploy job publishes only an artifact that passed it) resolves every link the way a browser does, against the URL each file is served at:hrefandsrcon every built HTML page (<script src>included), and every<meta content>URL on the site's origin;urland@idon the site's origin in each page's JSON-LD graph (an@idfragment such as#articlenames a node, so only its page is checked);hreforsrcin raw HTML), with fenced code and code spans removed first;<loc>;A protocol-relative link (
//x/) fails on any surface, because a browser reads its first segment as a host; it is what joining an empty base onto a leading slash produces. A target passes when GitHub Pages would serve a file for it (x/index.htmlatx/,x/fromx,x.htmlatx). External links stay with lychee (mise run docs:linksand the weeklylinks.yml). One link is allowed by name: the 404 page's canonical URL andog:url, which Starlight sets to/404/, a route with no file because Pages serves404.htmlat every missing path. A relative link on the 404 page fails its own case, because it would resolve against whichever missing URL was requested.starlight-links-validatornow applieserrorOnRelativeLinks(its default), and its exclusion covers root-relative.mdtwin links only, so a relative.mdlink the rewrite missed failsastro build.Evidence
/reference/public-api/now carrieshref="/microvms-agentd/reference/rpc-tools/"three times and no relative href;llms-full.txtcarries/microvms-agentd/reference/rpc-tools/; the twinreference/public-api.mdstill linksrpc-tools.md, which resolves to/microvms-agentd/reference/rpc-tools.md..mdfiles outsidesite/authored/reports 0 errors, and online with.config/lychee.toml0 errors (55 OK, 5 redirects). The 174 offline errors insite/authored/are site routes such as/learn/tutorial/install, whichtests/authored-links.test.tsresolves against the published pages and which resolve on the built site.mise run docs:checkon this tree: EXIT=0 in 176 s; biome and cspell clean,astro check0 errors, vitestTest Files 15 passed (15)andTests 223 passed (223)(main: 216, and this file adds 7). The rendered-pages case took 1.7 s at a load average of 70 on 16 CPUs. An earlier run under that load, before the case read each page once, timed out at vitest's 5-second default there and inagent-surface.test.ts's existing "emits no protocol-relative href anywhere"; the rendered-pages case now states a 60-second timeout.mise run check, run bare: EXIT=0 in 590 s, 69test result:lines, 1799 passed, 0 failed,[security] advisories ok, bans ok, licenses ok, sources ok,Prose: 0 errors, 4111 warnings(main: 4111, so no Vale ceiling moves). It ran before the last edits tosite/src/lib/andsite/tests/, which none of its tasks read (its lint is Rust and Python, and Vale reads.mdand.mdx).<script src>(1,211 references on 249 pages) and treated//links as external, plus smaller gaps (malformed percent-encoding, footnotes read as definitions, the bare base, autolinks and raw HTML in twins, JSON-LD). Each is fixed above, and the JSON-LD case then found the.mdencoding URL.mise run pr-body:check -- --body <this body>: EXIT=0.Guards
The registry's suites are rust, script and bindings, and a scratch worktree has no
site/node_modules, so these site guards were broken by hand, asdts-consumer.tomlandparity-wiring.tomlrecord for the other site tests. Each was restored and the tree passes again (see Evidence).built-links.test.ts's JSON-LD case: withjsonld.tsas on main, it failed onindex.html -> https://laithalsaadoon.github.io/microvms-agentd/.md resolves to /microvms-agentd/.md, which was not built.built-links.test.ts: withrelativeMdLinks(CONTENT_ROOT)removed fromastro.config.tsand the validator set back toexclude: ({ link }) => link.endsWith(".md")anderrorOnRelativeLinks: false,astro buildpassed and the test failed two cases: 148 offenders on rendered pages (among themreference/public-api/index.html -> rpc-tools.md resolves to /microvms-agentd/reference/public-api/rpc-tools.md, which was not built) and 296 in the llms bundles. With the originalHead.astro, it failed the rendered-pages case on404.html -> …/og/404.png, twice (og:imageandtwitter:image).mise run docs:buildfailed withFound 148 invalid links in 29 files, each markedrelative link.](rpc-tools.md)changed to](rpc-toolz.md)in the syncedsrc/content/docs/reference/public-api.md,astro buildfailed withdocs-relative-md-links: …/reference/public-api.md:11: \rpc-toolz.md` resolves to `reference/rpc-toolz.md`, which is not a page.docs:syncwould refuse the same link indocs/` first.Follow-ups
Platform claims
Scope
(see
docs/STRATEGY.md).