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.

Update 2023-05-26

YOu need to npm init the directory otherwise the packages are actually installed in the home folder. And then not usable as exec!

If you follow the remark quickstart after npm init, you get this error: “Cannot use import statement outside a module.” https://stackoverflow.com/questions/58384179/syntaxerror-cannot-use-import-statement-outside-a-module

It happens because remark is an “ESM only” package.

Someone tries to explain what that means to CommonJS users:

Pure ESM package https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c