Lint Markdown internal links

Use Remark. It’s the only linter I’m aware of that can check relative links across files.

I assume you already have node and npm installed. The versions I have currently are:

$ npm --version
7.19.1
$ node --version
v18.14.2

Use the following components:

  • remark inspects and changes markdown via plugins.
  • remarklint collects linting plugins for remark.
  • remark-validate-links checks that markdown links and images point to existing local files and headings in a Git repo.

Basic Linting

A quickstart from the remarklint documentation.

Set up:

cd "$(mktemp --dir)"

npm install remark-cli remark-preset-lint-consistent remark-preset-lint-recommended

mkdir doc

cat > doc/example.md <<"EOF"
1) Hello, _Jupiter_ and *Neptune*!
EOF

npm exec -- remark doc/ --use remark-preset-lint-consistent --use remark-preset-lint-recommended

Output:

doc/example.md
   1:1-1:35  warning  Marker style should be `.`               ordered-list-marker-style  remark-lint
        1:4  warning  Incorrect list-item indent: add 1 space  list-item-indent           remark-lint
  1:25-1:34  warning  Emphasis should use `_` as a marker      emphasis-marker            remark-lint

⚠ 3 warnings

Structural integrity checking

Or relative link validation. A quick start from the remark-validate-links documentation.

npm install remark-validate-links

cat > doc/example-link.md <<"EOF"
# Alpha

Links are checked:

This [exists](#alpha).
This [one does not](#does-not).

# Bravo

Headings in `readme.md` are [checked](readme.md#no-such-heading).
And [missing files are reported](missing-example.js).

Definitions are also checked:

[alpha]: #alpha
[charlie]: #charlie

References w/o definitions are not checked: [delta]
EOF

# remark-validate-links fails unless it runs in a git repo with an origin remote.
# The README shows how avoid that by setting `options.repository`. I don't know
# how to set that via npm CLI.
git init .
git add remote origin localhost

npm exec -- remark doc/ --use remark-preset-lint-consistent --use remark-preset-lint-recommended --use remark-validate-links

Output:

doc/example-link.md
     6:6-6:31  warning  Link to unknown heading: `does-not`                        missing-heading            remark-validate-links
  10:29-10:65  warning  Link to unknown file: `readme.md`                          missing-file               remark-validate-links
  10:29-10:65  warning  Link to unknown heading in `readme.md`: `no-such-heading`  missing-heading-in-file    remark-validate-links
   11:5-11:53  warning  Link to unknown file: `missing-example.js`                 missing-file               remark-validate-links
   15:1-15:16  warning  Found unused definition                                    no-unused-definitions      remark-lint
   16:1-16:20  warning  Found unused definition                                    no-unused-definitions      remark-lint
   16:1-16:20  warning  Link to unknown heading: `charlie`                         missing-heading            remark-validate-links
  18:45-18:52  warning  Found reference to undefined definition                    no-undefined-references    remark-lint

doc/example.md
     1:1-1:35  warning  Marker style should be `.`                                 ordered-list-marker-style  remark-lint
          1:4  warning  Incorrect list-item indent: add 1 space                    list-item-indent           remark-lint
    1:25-1:34  warning  Emphasis should use `_` as a marker                        emphasis-marker            remark-lint

⚠ 11 warnings

From here, it’s an exercise for the reader to integrate this into pre-commit and other development workflows.