DEV Community

Capafy Factory
Capafy Factory

Posted on Edited on

How to find broken Markdown links before you publish

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.

![](images/no-alt.png)

Enter fullscreen mode Exit fullscreen mode


md
Not a real link


Inline code is ignored: `[Fake](fake.md)`.

## Real Section

Content here.
Enter fullscreen mode Exit fullscreen mode


markdown

The 6 checks

1. Broken local file link. [Missing](missing.md) points at a file that doesn't exist. Same for the image ![](images/no-alt.png) — 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. ![](images/no-alt.png) 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
Enter fullscreen mode Exit fullscreen mode


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.

![Diagram](images/diagram.png)



## Real Section

Content here.
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
  • Point it at a single file or a whole directory — it checks every Markdown file recursively.
  • Default --fail-on critical means a critical finding blocks the gate while warnings stay advisory. Use --fail-on warning for a stricter gate.
  • Add --json-out report.json --md-out report.md if 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)