Linting Markdown files with markdownlint

Christian Emmer
Christian Emmer
Apr 5, 2023 · 2 min read
Linting Markdown files with markdownlint

Markdown's syntax is easy to learn, and even though the syntax is forgiving, linting can help you avoid unexpected issues.

markdownlint and its CLI tool markdownlint-cli is the most common tool used for linting Markdown files. As of writing, markdownlint validates Markdown files against a list of 53 rules . markdownlint-cli can be installed and used locally, but it's also easy to integrate into CI/CD pipelines.

See "Common Markdown Mistakes" for a list of the most common Markdown syntax mistakes I see people make.

Common Markdown Mistakes
Common Markdown Mistakes
Nov 9, 2020 · 4 min read

Markdown provides simple syntax for writing structure documents, but most written markdown would not pass a linter check. Here's a list of 11 common syntax mistakes and how to fix them.

Usage

markdownlint-cli has instructions for how to install via npm and Homebrew , but I'll focus on running it via Docker for OS portability. You can run markdownlint-cli in a container to lint Markdown files in your current directory like this:

docker run --volume "$PWD:/workdir" \
  ghcr.io/igorshubovych/markdownlint-cli:latest \
  "**/*.md"

If you want to disable certain markdownlint rules, you can do so like this:

docker run --volume "$PWD:/workdir" \
  ghcr.io/igorshubovych/markdownlint-cli:latest \
  --disable MD013 MD033 MD041 -- \
  "**/*.md"

Note the required -- which terminates the list of space-separated rule names.

Example output

Let's define a markdown file sample.md with the contents:

#Sample
This is a sample Markdown file.


It has a number of formatting issues.

###  Sub-heading

Here's the example output from markdownlint-cli:

$ docker run --volume "$PWD:/workdir" ghcr.io/igorshubovych/markdownlint-cli:latest "**/*.md"
sample.md:1:1 MD018/no-missing-space-atx No space after hash on atx style heading [Context: "#Sample"]
sample.md:1 MD041/first-line-heading/first-line-h1 First line in a file should be a top-level heading [Context: "#Sample"]
sample.md:4 MD012/no-multiple-blanks Multiple consecutive blank lines [Expected: 1; Actual: 2]
sample.md:7:1 MD019/no-multiple-space-atx Multiple spaces after hash on atx style heading [Context: "###  Sub-heading"]

Adding to GitHub Actions

Linters become a lot more powerful when you add them to your CI pipelines. A linter without enforcement will surely be ignored over time.

Here's a sample of how you could add markdownlint-cli to a GitHub Actions .github/workflows/test.yml (feel free to change the filename):

name: Project CI

on:
  pull_request:
    types:
      - opened
      - synchronize  # HEAD has changed, e.g. a push happened
      - reopened

jobs:
  markdown-lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - run: docker run --volume "$PWD:/workdir" ghcr.io/igorshubovych/markdownlint-cli:latest "**/*.md"

Then you can add a branch protection rule to prevent pull request merges without certain passing jobs.