zfb
GitHub repository

Type to search...

to open search from anywhere

Link validation

Validate internal links and anchor fragments at build time to catch broken references before they reach production.

The linkValidation feature walks every <a href> and <img src> in the build and validates that internal links and anchor fragments resolve correctly. Broken links produce build diagnostics — warnings by default, errors when failOnBroken: true is set.

To gate CI on link integrity without editing the linkValidation block itself — useful when a preset owns it — use the top-level strictBrokenLinks config field or zfb build --strict-broken. See Strict mode below.

External URLs (http://, https://, mailto:, etc.) are silently skipped by default.

Enable

// zfb.config.ts
export default defineConfig({
  markdown: {
    features: {
      linkValidation: {},   // defaults: warn-only, skip external URLs
    },
  },
});

To make broken links fail the build:

linkValidation: { failOnBroken: true },

What is validated

  • Bare anchor fragments#section-id must match a heading ID in the current file.

  • File links without anchor./other.md must resolve to an existing file under the project root.

  • File links with anchor./other.md#section-id requires both a resolvable file and a matching heading ID in that file.

Heading IDs come from HeadingLinksPlugin, which runs earlier in the same hast phase. The cross-file heading-ID registry is populated during the build so anchor validation works across files. Headings that arrive via transclusion are included in the registry, so ./target.mdx#transcluded-heading validates correctly.

What is skipped

  • External URLs starting with http://, https://, mailto:, or tel:.

  • Links in files rendered without a BuildContext (e.g. simple in-memory pipeline calls without context).

  • Cross-file anchor links whose target file is outside the bundler's walked directories (pages, content collections, components, layouts). These degrade to existence-only validation: the file must exist on disk, but the fragment is not checked. If the target file IS in the build, the fragment is always verified.

Options

  • failOnBroken — when true, broken links emit Error diagnostics (build fails). Default: false (warnings only). It can also be turned on from outside this block — see Strict mode.

Strict mode

failOnBroken lives inside the linkValidation block, which is not always yours to edit: a preset can own that block and bake failOnBroken: false into it. zfb therefore exposes a top-level switch that overrides it, as a config field and as a pair of zfb build flags:

// zfb.config.ts
export default defineConfig({
  strictBrokenLinks: true,
});
zfb build --strict-broken      # fail this build on broken links
zfb build --no-strict-broken   # do not fail this build

See strictBrokenLinks in the config reference and the zfb build flags in the CLI reference.

Turning strict mode on for a project with no linkValidation configuration at all does not silently do nothing. It enables link validation with its default settings and makes broken links fail the build. A switch named "strict broken links" that quietly no-ops on a bare project would be a footgun, so it always has an effect.

When linkValidation is already configured, only failOnBroken is overridden. Every other option in that block — and every sibling setting under markdown and markdown.features — is left untouched.

Precedence

Three levels, highest first:

  1. An explicit CLI flag — --strict-broken or --no-strict-broken. The two conflict; passing both is an error.

  2. The strictBrokenLinks config field.

  3. The default, false.

Omitting the flag is not the same as passing --no-strict-broken: an omitted flag falls through to the config value, while --no-strict-broken explicitly overrides it. This is the same tri-state minifyHtml uses with --minify-html / --no-minify-html.

Build only — zfb dev is never affected

Strict mode is resolved inside zfb build and applies to that command alone. zfb dev never sees the strict overlay — link validation behaves there exactly as your own linkValidation configuration alone says it should. A project with no linkValidation block still has no link validation at all in dev, even with strictBrokenLinks: true set; a project that configures failOnBroken: true itself keeps that severity in dev, as it always did. The point is to gate CI, not to change the inner loop.

What --no-strict-broken does not do

--no-strict-broken disables the top-level strict overlay only. It does not downgrade a linkValidation block that independently and explicitly sets failOnBroken: true — that is a separate, more specific knob, and it stays in effect regardless of the flag:

markdown: { features: { linkValidation: { failOnBroken: true } } },

A project configured that way still fails on broken links under zfb build --no-strict-broken. To relax it, change failOnBroken itself.

zfb has a second, entirely separate broken-link mechanism: resolveMarkdownLinks's onBrokenLinks option, which reports .md/.mdx links the resolver could not rewrite. The two are not unified and are not planned to be:

This featureresolveMarkdownLinks
Config pathmarkdown.features.linkValidationresolveMarkdownLinks
Severity knobfailOnBroken, plus strictBrokenLinks / --strict-brokenonBrokenLinks: "warn" | "error" | "ignore"
Diagnostic wordingbroken link: …broken markdown link in {file}: … could not be resolved…

strictBrokenLinks and --strict-broken have no effect on onBrokenLinks. To make unresolvable markdown links fail the build too, set onBrokenLinks: "error" alongside them.

Diagnostic format

Diagnostics follow the shared BrokenLink variant in MarkdownDiagnostic:

  • severityWarning or Error depending on failOnBroken.

  • url — the raw href or src value as written by the author.

  • location.path — absolute path of the source file containing the broken link.

Behavior change in #980

Before #980, cross-file anchor fragments (./other.md#section) were validated only for existence — the fragment itself was not checked against the target file's headings. Starting with #980, the fragment is verified post-compile for every file in the build. Builds that previously passed with a broken cross-file anchor under failOnBroken: true will now fail.

Phase

Runs in two phases:

  1. Per-compile hast phaseLinkValidationPlugin validates same-file anchors (#section-id) immediately and records cross-file fragment candidates for post-compile resolution.

  2. Post-compile bundler pass — after all files have been compiled, the bundler assembles a heading map from every file's recorded headings and verifies each recorded cross-file candidate. Findings are routed through the same severity gate as other markdown diagnostics.

Revision History

CreatedUpdated