Validate
blume validate checks every link in your content — pages, anchors, assets, and optionally external URLs — and fails CI when one is broken.
blume validate reads your content the way a build does and checks every link it finds. It doesn’t generate or build anything, so it’s the fast gate to put in front of blume build:
blume validate
- Internal page links (
/guides/intro,./sibling) must resolve to a real page — broken ones are reported as errors. Every link form Markdown and MDX render is read: inline links, including one whose label wraps onto the next line; reference-style links, checked at their[label]: /targetdefinition (the first one, when a label is defined twice, since that’s the one that renders); autolinks (<https://…>, in.md); and a component’s stringhref(<Card href="./install">). A root-relative link to a Markdown file in your content (/guides/setup.md) is checked at the page that file publishes, where the build points it; one naming no file there is checked as the page’s Markdown copy. A link’s target is read the way the renderer reads it, so[x](<https://example.com/a(b>)points athttps://example.com/a(band.png)atimage(1).png, with any spaces around it and a title in any of its quotes ("…",'…', or(…)) after it. Links in code (fenced, including a fence inside a block quote, or inline) and in comments (<!-- … -->in.md,{/* … */}in.mdx) never render, so they aren’t checked. A lowercase HTML<a href>to a page ships as written, so it’s checked the way a browser reads it: a relative path resolves against the page’s URL, and nobasePathis added (a root-relative one gains a deploymentbase, as every link does). Anhref={…}expression isn’t checked. - Anchor links (
#section,/guides/intro#setup) must match an anchor on the target page — a heading’s id (generated or pinned), a raw HTML element’sidattribute, or an<a name>— with misses reported as warnings. Ids inside code blocks, inline code, HTML comments, and<Prompt>blocks don’t count. - Other link schemes (
mailto:,tel:, an app’svscode:deep link) aren’t checked: whether a reader’s browser or an installed app handles one can’t be known. ReadMe’s own link schemes are the exception:doc:,ref:,page:,changelog:, andblog:only resolve on ReadMe and ship as dead links anywhere else, so each is reported as a warning. Link the page by its path instead. - Asset links are checked where the file lives: an absolute path (
/logo.png) against thepublic/directory, a relative image embed (, or a card’s<Card img="./cover.png">) against the page’s own folder. A relative link to any other file ([spec](./spec.pdf), or an<a href>or a component’shref) is checked against the page’s own folder too, since Blume publishes a file there with the page; one that names no file beside the page resolves as a site route againstpublic/. A media element’ssrc(<img>,<source>,<video>, or<audio>, as raw HTML in.mdor an element in.mdx) is checked the same way: a relative one is found beside the page, or else where the browser requests it, againstpublic/. A project with nopublic/folder ships no public files, so an absolute asset path there is reported missing. An.htmllink (./setup.html,/guides/setup.html) is a file link too: Blume serves pages at their routes, never at.htmlURLs, so unlesspublic/has that file, the link is reported, with the page’s route as the fix when a page matches it. - Redirect targets in
redirectsmust lead somewhere: an exact internaltoto a page, a file, or another redirect, and a patterntoto a path with something served under its literal start (/v2/in/v2/:slug*). One that leads nowhere is a warning, located at itstoinblume.config.ts. A link to a redirect’sfromis valid either way. External targets aren’t checked. - External links are only checked with
--external(off by default since it requires the network). A dead link is an error: a 404 or 410, or a host name that doesn’t resolve. Anything else that fails is a warning, since it’s more often the other site or the network than the link: any other error status (a 401 or 403 from a site that turns bots away, a 429 rate limit, a 5xx outage), a refused or dropped connection, or no answer within 10 seconds. To leave out URLs that can’t be checked from where the command runs, like a placeholder domain, a local server, or a site that turns bots away, pass--ignore. A bare URL that GitHub-flavored Markdown links on its own, with no link syntax around it, isn’t read;blume audit --externalchecks it in the built site.
What counts as a page
Links resolve against everything the built site serves, not just Markdown pages: custom .astro pages, API reference pages, the generated changelog index, every configured redirect, and, on a multi-language site, an untranslated page’s fallback URL under each locale. The files Blume writes beside your pages count too, while the config has them on: /llms.txt and /llms-full.txt, the sitemap, /robots.txt, the RSS feeds, /openapi.json, /agent-readability.json, /skill.md, and the .well-known discovery files such as /.well-known/agent-skills/index.json. So does a folder in public/ with an index.html, at /demo or /demo/ for public/demo/index.html, as a static host serves it. A page that fails to load, with invalid front matter or .mdx that doesn’t parse (BLUME_MDX_SYNTAX), is reported alongside the link findings, because a page that never loads is a link-validation blind spot rather than a clean result.
Flags
--external— also check externalhttp(s)links over the network.--ignore <glob>— with--external, don’t request an external link whose URL matches the glob, and report nothing for it. The glob is matched against the link’s full URL, scheme included:*matches within one path segment and**across segments, so--ignore "https://api.acme.example/**"skips every link to that host and--ignore "http://localhost:*/**"every local server. Repeat the flag for more patterns, and quote each one so your shell doesn’t expand it. Internal links are always checked: they resolve against your own pages, so fix a broken one or add a redirect.--strict— exit non-zero on warnings too. Info-level notes stay advisory.--json— emit the diagnostics as JSON on stdout instead of the terminal report.
Diagnostics
| Code | Severity | Meaning |
|---|---|---|
BLUME_BROKEN_LINK |
error | An internal link points at a route no page serves. When it’s a relative link to a file that exists but whose name, or a folder’s, starts with _, the fix says so: that file is a partial, which isn’t published. |
BLUME_BROKEN_ANCHOR |
warning | The page exists but has no anchor matching the fragment. |
BLUME_BROKEN_ASSET |
warning | An absolute path isn’t in public/, a relative image embed isn’t beside the page, an .html link names no file in public/, or a media src names a file the site doesn’t serve at that URL. |
BLUME_BROKEN_REDIRECT |
warning | A redirect’s internal to leads to no page, file, or other redirect. |
BLUME_UNSUPPORTED_LINK_SCHEME |
warning | A link uses one of ReadMe’s link schemes (doc:, ref:, page:, changelog:, blog:), which browsers can’t follow. |
BLUME_DEAD_LINK |
error or warning | An external link (with --external) returned 404/410 or its host doesn’t resolve (error), or it got any other error status, no connection, or no answer in time (warning). |
The exit code is the contract: errors exit non-zero, and --strict makes warnings do the same.
JSON output
With --json, blume validate (and blume doctor) write one object to stdout: a diagnostics list, each entry carrying its code, severity, message, the file relative to the project root with line and column when the finding has a location, a suggestion, and a docsUrl pointing at the page that explains the code, plus a summary of counts per severity. The exit code is unchanged, so the same invocation gates CI and feeds an editor integration.