How to find broken Markdown links before you publish
Most broken Markdown links don't fail your build. They fail after you publish, when a reader clicks "Setup" and lands on a 404, or follows an anchor to a section that doesn't exist.
Here's how to catch them before publish — the six checks I run, with a real example.
The example: a guide with everything wrong
This is a small, realistic guide. It renders fine. Every link below is a problem:
# Guide
See [Missing](missing.md), [Bad Anchor](#no-such-section), [Ref][undefined-ref],
[Empty]() and [](setup.md) for problems.

Inline code is ignored: `[Fake](fake.md)`.
## Real Section
Content here.
markdown
The 6 checks
1. Broken local file link. [Missing](missing.md) points at a file that doesn't exist. Same for the image  — the file images/no-alt.png isn't there either. Both are critical: a reader clicking them gets nothing.
2. Broken anchor. [Bad Anchor](#no-such-section) points at #no-such-section, but the file has no such heading. The only real heading anchor is #real-section.
3. Undefined reference. [Ref][undefined-ref] uses a reference-style link, but there is no [undefined-ref]: ... definition anywhere in the file. It renders as plain text, not a link.
4. Empty link target. [Empty]() has no target at all. It looks like a link and goes nowhere.
5. Empty link text. [](setup.md) points at a real file but has no text, so a reader sees nothing to click.
6. Image without alt text.  has no alt text. Screen readers skip it, and if the image fails to load, nobody knows what was there.
Note what the checker deliberately ignores: the link inside the fenced code block and the one in inline code are not real links, so they are skipped — no false positives from your examples.
The real run
I ran a checker over that file. Verbatim output:
$ python3 scripts/md_link_check.py check --path references/broken.md
FAIL: 7 finding(s), 4 at or above critical, 0 external link(s) not fetched
[critical] broken-local-link (broken.md:3): link target file does not exist: missing.md
[critical] broken-anchor (broken.md:3): anchor not found in this file: #no-such-section
[critical] undefined-reference (broken.md:3): reference link has no definition: undefined-ref
[warning] empty-link-target (broken.md:4): link has an empty target
[warning] empty-link-text (broken.md:4): link has no text
[warning] missing-alt-text (broken.md:6): image has no alt text
[critical] broken-local-link (broken.md:6): link target file does not exist: images/no-alt.png
markdown
Exit code 1 — it fails the gate. Every finding comes with a file and line number, so you know exactly where to fix.
The fixed version
Same intent, all seven findings gone:
# Guide
See [Setup](setup.md), [Real Section](#real-section), [Ref][guide-ref], and [Advanced](setup.md) for details.

## Real Section
Content here.
I verified this passes:
$ python3 scripts/md_link_check.py check --path fixed.md
PASS: 0 finding(s), 0 at or above critical, 0 external link(s) not fetched
Exit code 0. The fixes are boring on purpose: point at files that exist, anchors that exist, define your references, give every link text and a target, and write alt text for images.
Making it routine
Check before every publish, not when a reader reports a dead link:
python3 scripts/md_link_check.py check --path docs
- Point it at a single file or a whole directory — it checks every Markdown file recursively.
- Default
--fail-on criticalmeans a critical finding blocks the gate while warnings stay advisory. Use--fail-on warningfor a stricter gate. - Add
--json-out report.json --md-out report.mdif you want a report file for CI. - External links are counted, never fetched: the checker tells you how many it saw (
1 external link(s) not fetched) without hitting the network, so it's fast enough to run on every commit.
What this doesn't do
Honest limits: it doesn't fetch or verify external URLs, doesn't check setext-style headings, and doesn't anchor-check pure-anchor reference definitions. It's a local-link check — the cheap gate before the expensive checks.
The tool
The checker above is Markdown Link Checker — 6 rules, offline, $9.99 once, no subscription. If you'd rather not check every link by hand before publish, it's there.
Top comments (0)