Daniel Doubrovkine bio photo

Daniel Doubrovkine

aka dB., @Microsoft, ex-@ShopifyEng, @OpenSearchProj, @awscloud, former CTO @artsy, +@vestris, NYC

Email Twitter LinkedIn Github Strava
Creative Commons License

After RuboCop spelling checks for Ruby and Dylint spelling checks for Rust, here’s the version that doesn’t require either language’s toolchain.

Introducing markdownlint-rule-spellcheck, a custom markdownlint rule powered by CSpell. Released 0.1.0 checks Markdown prose, suggests corrections, and ships American English through its dependencies. No separate dictionary download or system spelling package is required.

Getting Started

Requires Node.js 22.18 or later. Install the rule and the command-line runner:

npm install --save-dev markdownlint-cli2 markdownlint-rule-spellcheck@0.1.0

Add .markdownlint-cli2.jsonc:

{
  "customRules": ["markdownlint-rule-spellcheck"]
}

Given example.md:

# Say helo to the world

Run:

npx markdownlint-cli2 example.md

The spelling diagnostic includes the original line and column:

example.md:1:7 error spellcheck Prose words should be spelled correctly [Misspelled word: "helo". Suggestions: hello, helio, halo.]

Headings, paragraphs, lists, tables, link labels, and image descriptions are checked. Code, link destinations, URLs, and email addresses are skipped. Liquid expressions and tags are skipped too, which matters for Jekyll sites like this one.

Accepting Project Vocabulary

Technical documentation contains legitimate words that aren’t in an English dictionary. Collect candidates, review them, and add the accepted words:

npx markdownlint-rule-spellcheck collect "**/*.md" --output .spellcheck.txt
# Edit .spellcheck.txt: remove actual typos.
npx markdownlint-rule-spellcheck add ./.spellcheck.txt
npx markdownlint-cli2 "**/*.md"

The add command updates .markdownlint.json, preserving existing settings. Its dictionary configuration looks like this:

{
  "spellcheck": {
    "dictionaries": {
      "en": ["@cspell/dict-en_us", "./.spellcheck.txt"]
    }
  }
}

Corrections and Front Matter

Corrections are opt-in. Set "autocorrect": true inside spellcheck, then run:

npx markdownlint-cli2 --fix "**/*.md"

The first suggestion must be one edit away: an insertion, deletion, substitution, or adjacent transposition. Lowercase and title case can be corrected; ALL CAPS, mixed case, and components of technical tokens remain alone. Review the diff. A correctly spelled word can still be the wrong word.

YAML front matter strings are checked by default, with nested field selection in .markdownlint.json:

{
  "spellcheck": {
    "markdown": {
      "frontMatter": {
        "include": ["title", "description", "authors.name"],
        "exclude": ["authors.url"]
      }
    }
  }
}

There’s a setup wrinkle. Markdownlint normally removes front matter before custom rules see the document. To check it, add "frontMatter": "a^" to .markdownlint-cli2.jsonc. That’s a regular expression that never matches, leaving the header available to the rule. Other Markdown rules will see it too and may need configuration. Alternatively, set spellcheck.markdown.frontMatter to false and keep the normal header handling.

I also use markdownlint-rule-single-line-paragraphs to keep each prose paragraph on one source line. Let the editor wrap the display instead of inserting arbitrary line breaks into the file.

Install it:

npm install --save-dev markdownlint-rule-single-line-paragraphs

Register both rules in .markdownlint-cli2.jsonc:

{
  "customRules": [
    "markdownlint-rule-spellcheck",
    "markdownlint-rule-single-line-paragraphs"
  ],
  "config": {
    "MD013": false
  }
}

Disabling MD013 removes the line-length limit; the custom rule enforces single-line paragraphs. It isn’t a rule for wrapping at 80 characters. It’s the opposite. This blog already uses it, and the spelling rule’s own documentation uses both.