Skip to content
Blume
Esc
↑↓navigate↵open⌘Jpreview
On this page

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]: /target definition (the first one, when a label is defined twice, since that’s the one that renders); autolinks (<https://…>, in .md); and a component’s string href (<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 at https://example.com/a(b and ![](image\(1\).png) at image(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 no basePath is added (a root-relative one gains a deployment base, as every link does). An href={…} 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’s id attribute, 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’s vscode: 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:, and blog: 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 the public/ directory, a relative image embed (![](./diagram.png), 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’s href) 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 against public/. A media element’s src (<img>, <source>, <video>, or <audio>, as raw HTML in .md or an element in .mdx) is checked the same way: a relative one is found beside the page, or else where the browser requests it, against public/. A project with no public/ folder ships no public files, so an absolute asset path there is reported missing. An .html link (./setup.html, /guides/setup.html) is a file link too: Blume serves pages at their routes, never at .html URLs, so unless public/ has that file, the link is reported, with the page’s route as the fix when a page matches it.
  • Redirect targets in redirects must lead somewhere: an exact internal to to a page, a file, or another redirect, and a pattern to to a path with something served under its literal start (/v2/ in /v2/:slug*). One that leads nowhere is a warning, located at its to in blume.config.ts. A link to a redirect’s from is 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 --external checks 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 external http(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.

Last updated on

Was this page helpful?