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.
The 6 checks
- Broken local file link.
[Missing](missing.md)points at a file that doesn't exist. Same for the image- the fileimages/no-alt.pngisn't there either. Both are critical: a reader clicking them gets nothing. - 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. - 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. - Empty link target.
[Empty]()has no target at all. It looks like a link and goes nowhere. - Empty link text.
[](setup.md)points at a real file but has no text, so a reader sees nothing to click. - 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
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.
Comments
No comments yet. Start the discussion.