Weekly release · #3

markdown-anchor-checker: catch broken documentation links before merging

TL;DR: Catch broken links after documentation changes with a local checker that finds missing files, renamed heading targets, and malformed anchors before review is complete.

View the repository on GitHub

  • markdown
  • documentation
  • testing
  • cli
  • open-source

Release #3 in our open-source series. Renaming a section can improve a guide and break the links pointing to it at the same time. markdown-anchor-checker checks those connections across a folder of documentation, helping reviewers find the breakage before someone follows the link during onboarding or an incident.

Why it matters

An anchor is a named location within a page, commonly generated from a heading. In guide/install.md#requirements, the file path opens the installation guide and #requirements asks the reader's browser to jump to a particular section.

If the heading changes from “Requirements” to “Before you begin,” the file still exists. A check that only confirms the page exists can pass even though the old section link no longer takes the reader where intended. Moving a guide creates a different problem: the relative path itself can stop resolving.

These changes are easy to miss in a review focused on the edited paragraph. The links needing repair may live in a README, a separate onboarding guide, or an API reference that the author did not open.

What it does

  • Finds links that lead nowhere. It reads a folder of Markdown and MDX documentation and reports links to files that do not exist and to sections whose heading was renamed or removed.
  • Names the kind of problem. Each finding has the file, line, and column, so a reviewer knows whether to fix a path, a heading, or a spelling.
  • Runs safely anywhere. It needs no credentials, makes no network requests, and does not rewrite documentation. That makes it suitable for a local review step or a continuous integration (CI) check: an automated command run when a change is proposed.
  • What it does not do: check links to other websites, or confirm that a published site's navigation works. Those need a separate check.

Who it's for

  • Documentation maintainers who reorganize a guide and need to know which links the change broke.
  • Application teams who keep documentation beside their code and want references checked with each code change.
  • Open-source maintainers reviewing contributions that rename headings or move Markdown files.

What it checks

The checker distinguishes four problems so the reviewer knows what to inspect:

Finding Example What to review
FILE_NOT_FOUND guide/old-name.md Has the page moved, or is the relative path wrong?
MISSING_HEADING install.md#gone Was the heading renamed or removed?
INVALID_ANCHOR #Requirements when #requirements exists Use the suggested spelling for the existing target.
MALFORMED_LINK config.md# or #requirements#x Repair the empty or repeated fragment separator.

Each finding includes the source file, line, column, link text, target, and message. Reports are sorted by file and position, so the same inputs produce a stable order for review.

Architecture and tech stack

The implementation discovers .md and .mdx files, parses Markdown headings and links, and builds a set of heading targets for each file. A slug is the heading's URL-friendly form: for example, “Requirements” becomes requirements. Repeated headings receive suffixes such as setup, setup-1, and setup-2.

For each local link, the checker resolves its path relative to the source document and compares its fragment with the target's heading set. Extensionless paths can resolve to .md or .mdx files. Fenced code examples and inline code are not treated as live links by the Markdown parser.

Loading diagram...
Technology Role Why it was chosen
Node.js and TypeScript Local command-line program and typed finding records Runs where documentation tooling already runs, and gives access to the remark Markdown ecosystem
unified and remark-parse Markdown parsing A real parser rather than regular expressions, so links inside code examples are not reported as broken
mdast-util-to-string and unist-util-visit Extract heading text and traverse parsed nodes The standard helpers for the syntax tree remark produces, so heading text matches what the parser read
Vitest Regression tests for checking and reporting Runs the TypeScript tests directly, including command-line runs against the example documentation

Key design decisions and trade-offs

The tool checks local content without fetching remote sites, so results do not depend on network availability. That also means external URLs need a separate check. It reports repairs for reviewers to consider rather than rewriting links and potentially choosing the wrong destination.

Heading targets follow GitHub-style rules, which makes repository documentation the main fit. A site generator with custom IDs or routing may need a different interpretation; the local report cannot establish that the rendered site works.

Under the hood

The source separates a missing heading from a spelling mismatch: if the exact fragment is absent but its slug matches, the report suggests that spelling. It does not guess a replacement for a heading that has actually disappeared.

Sharp edges and benchmarks

The release passed its TypeScript build, 24 automated tests, and the TypeScript check used by its lint script. Tests cover slug generation and duplicate headings, link classes, finding positions, discovery exclusions, terminal and JSON reports, and exit codes. A test compares the example's terminal report with the checked-in expected output.

The five-file fixture is synthetic documentation, not a production benchmark. Large-repository performance was not measured. GitHub-style slug behavior is tested for the repository's cases; full parity with GitHub's live renderer has not been established.

Quick start

Requires Node.js 20 or later. Install from the public source repository:

git clone https://github.com/jare20895/markdown-anchor-checker
cd markdown-anchor-checker
npm ci
npm run build
node dist/bin.js examples/docs

The fixture contains intentional errors. An excerpt of the actual report shows both a suggested repair and a missing file:

guide/install.md:10  [INVALID_ANCHOR]  #Requirements  →  "bad heading link"  "#Requirements" is not a GitHub anchor; use "#requirements".
README.md:9  [FILE_NOT_FOUND]  guide/old-name.md  →  "moved page"  no file at "guide/old-name.md".
Scanned 5 files, 10 broken links in 3 files.

That command exits with status 1 because it found broken links; this is the expected demonstration result. Change the wrong-case fragment to #requirements to resolve that specific finding. For a moved page, update the path after confirming the intended destination. The tool reports the problem but leaves that editorial decision to you.

To check your own documentation, keep the terminal in the checker repository and pass the documentation directory:

node dist/bin.js /absolute/path/to/your-project/docs
node dist/bin.js /absolute/path/to/your-project/docs --json

The JSON report contains a scanned-file count and an array of errors. It can be saved as a build artifact or consumed by another tool. Source installation is the verified route for this release; these commands do not assume npm registry publication.

Add it to review and CI

Run the scan after edits and before merging. In CI, build a pinned checkout of the checker, then invoke its dist/bin.js against the documentation checkout. The release was published at commit 7aeb86d3bffd90f29b5caab9011e7edca63cdccd, which can be used to pin that dependency.

The exit codes give the job a clear result: 0 means no findings among the links it checks, 1 means broken links, and 2 means invalid usage or an unreadable input directory. A status of 2 needs a job-configuration fix; it should not be treated as evidence that the docs are clean.

Choose the scan root deliberately. Scanning only one guide folder will not discover links written in sibling folders, even though the checker can load a relative link's target outside the scan root. To review a documentation reorganization, scan the common parent containing the pages that may link to the moved material.

Path to production

First, run it against the full documentation tree and review findings against the rendered site, especially custom IDs and MDX pages. Then pin the tool's release commit in CI and preserve the report when a scan fails.

The repository's known issues identify concrete improvements: reading .gitignore, collecting HTML and custom heading anchors, and testing MDX-specific syntax. Measure it on your own repository before relying on a particular build-time budget.

Limits, plainly

A clean result also has limits:

  • External URLs and site-root paths are skipped. The checker cannot tell you whether a remote page or a deployed route works.
  • Directories are accepted without checking an index page, including any fragment on a directory link.
  • HTML anchors and custom heading IDs are unsupported. Sites that depend on them may receive findings that require manual review.
  • MDX is parsed as plain Markdown; headings or links within JSX may be missed.
  • The checker uses a fixed exclusion list and does not read .gitignore. It skips hidden names, node_modules, dist, and build.
  • Unicode normalization is not applied before slugging, so visually identical text may not resolve identically.

Use the checker to review local documentation links, and verify rendered navigation separately when your publishing system adds its own routing or anchor rules. The code is MIT licensed; the repository includes the full command reference, validation evidence, and known issues.