@eslint/markdown: linting the JavaScript inside your Markdown
Lint JavaScript code blocks in Markdown documents
At a glance
- What is it?
- The official ESLint language plugin for Markdown parses .md files as source, lints the JS, JSX and TypeScript inside fenced code blocks, and ships rules for Markdown structure itself. It works as an ESLint v9 language plugin with a flat config, and it is MIT licensed and published to npm and JSR.
- Who is it for?
- The plugin fits a team whose documentation is full of JavaScript examples that nobody checks, since it makes those examples first class lint targets under the same ESLint version and the same rules as the application code. It does not fit a documentation site whose code samples are deliberately invalid, and it does not cover the rendering of the prose itself.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 2 days ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What the Markdown plugin actually does
@eslint/markdown is the official ESLint language plugin for Markdown. The one line description is that it lints Markdown with ESLint, as well as JS, JSX, TypeScript and more inside Markdown.
That has two halves. The first is that the Markdown file itself becomes a source file ESLint understands, which is what makes Markdown rules possible at all. ESLint's normal model is one file parsed by one parser with one set of rules, and a Markdown document is a document with prose, headings, links, definitions and embedded code in it. The plugin supplies a language definition so that ESLint can treat a .md file the way it treats a .js file.
The second half is the embedded code. A fenced code block tagged as JavaScript is linted with the same ESLint rules that apply to a .js file, so an example in your documentation is held to the same standard as the code it documents. The screenshot in the README shows a JavaScript snippet in a Markdown editor with red squiggly underlines and a tooltip explaining the problem, which is the whole point of the project in one image.
The audience is teams where the documentation contains a lot of executable-looking code, which is most teams with a component library, an API reference, or a README full of usage examples. The cost of not linting those examples is that a broken example sits in the docs until a user copies it and it fails.
Installing the plugin alongside ESLint v9
The compatibility requirement is specific. Install the plugin alongside ESLint v9.15.0 or greater, and type compatibility is guaranteed with ESLint v9.39.0 or greater.
The plugin is installed as a dev dependency, and the documentation shows the equivalent command for each package manager:
npm install eslint -D
# or
yarn add eslint -D
# or
pnpm install eslint -D
# or
bun add eslint -Dand then the plugin itself with the same four variants:
npm install @eslint/markdown -D
# or
yarn add @eslint/markdown -D
# or
pnpm install @eslint/markdown -D
# or
bun add @eslint/markdown -DDeno is handled separately, through the JSR registry rather than npm:
deno add jsr:@eslint/markdownThe package manifest confirms the shape of the distribution. The name is @eslint/markdown, the version is 8.0.3, the description is The official ESLint language plugin for Markdown, the licence is MIT, and the type is module. The published files field contains only dist, with main pointing at dist/index.js and types at dist/index.d.ts. There is a jsr.json at the repository root for the Deno distribution, and the workspaces field points at examples/* so the example projects are part of the same monorepo.
Two configurations and the flat config that turns them on
The plugin ships two named configurations, and they do different things.
The recommended configuration lints all .md files with the recommended rules and assumes CommonMark format. The processor configuration enables extracting code blocks from all .md files so the code blocks can be individually linted.
The distinction is between linting the document and linting the code in it. That is the kind of split that matters, because a Markdown file with prose is not a JavaScript file, and the language plugin gives you the prose rules, while the processor is what actually hands the fenced blocks to the JavaScript rules.
The flat config that wires it up is short:
// eslint.config.js
import { defineConfig } from "eslint/config";
import markdown from "@eslint/markdown";
export default defineConfig([
{
files: ["**/*.md"],
plugins: {
markdown,
},
extends: ["markdown/recommended"],
},
// your other configs here
]);The files pattern scopes the block to Markdown, the plugin is registered under the name markdown, and extends pulls in the recommended configuration. And you can add rules on top of the recommended set the same way:
// eslint.config.js
import { defineConfig } from "eslint/config";
import markdown from "@eslint/markdown";
export default defineConfig([
{
plugins: {
markdown,
},
extends: ["markdown/recommended"],
rules: {
"markdown/no-html": "error",
},
},
// your other configs here
]);Note that the second example has no files pattern, so it applies to whatever the rest of your config already covers.
The rule set: what is on by default and what is not
The rules are listed in a table in the README, and the Recommended column tells you which are on when you use the recommended configuration.
The ones that are recommended include fenced-code-language, which requires a language for fenced code blocks, and heading-increment, which enforces that heading levels increase by one at a time. On the structure side there is no-multiple-h1, no-missing-atx-heading-space, which disallows headings without a space after the hash characters, and no-missing-link-fragments, which disallows link fragments that do not reference valid headings.
The reference rules are about links and definitions: no-missing-label-refs and no-invalid-label-refs, no-reference-like-urls, which disallows URLs that match defined reference identifiers, and no-unused-definitions, no-duplicate-definitions and no-empty-definitions. There is also no-reversed-media-syntax for reversed link and image syntax, no-space-in-emphasis for spaces around emphasis markers, and no-empty-links and no-empty-images.
Three rules are explicitly not recommended, and they are the ones most teams will want. no-html disallows HTML tags and is off by default, which is right for a project that writes Markdown with inline HTML on purpose. no-bare-urls disallows bare URLs. And fenced-code-meta requires or disallows metadata for fenced code blocks.
The last recommended rule is require-alt-text, which requires alternative text for images.
So the recommended set is mostly about keeping documents navigable and consistent, while the opinionated ones about HTML and URLs are opt-in. That is a reasonable default, and a team that has standardised on CommonMark with no raw HTML gets most of the value without changing a line.
Where the plugin fits in the repository's own tooling
The repository lints itself with the plugin, which tells you how it is meant to be used.
The lint-staged configuration in the package manifest runs different tools on different file types. JavaScript files get eslint --fix and then prettier --write. Markdown files get eslint --fix, and then a second pass with eslint --fix -c eslint.config-content.js. Everything else gets prettier --write --ignore-unknown. And there is a special case for the rule source files, the rules documentation generator and the README, which get npm run build:update-rules-docs and a git add of the README, because the rule table in the README is generated from the rule metadata.
The README itself says the rule table is autogenerated and should not be manually edited, and the build script is tools/update-rules-docs.js, run through npm run build:update-rules-docs as part of the full build alongside build:rules and build:types.
The other scripts show the checks the project runs on itself: lint, which is eslint followed by eslint with the content config, lint:fix, lint:types using attw with the esm-only profile to check the published types, lint:unused using knip, fmt and fmt:check with prettier, and a prepare script that runs the build.
The tree also carries a .c8rc for coverage, a knip.json for the unused dependency check, a prettier.config.js, a tsconfig.json for the type build, an eslint.config.js alongside eslint.config-content.js, and an rfcs/ directory, which is where proposals for new rules would be discussed before implementation.
Where the plugin is the wrong tool
The first limit is that it lints code, not documents. The plugin checks whether the JavaScript in your examples is valid and whether your headings, links and definitions are consistent. It does not check whether the prose is accurate, whether the example still does what the surrounding text says it does, or whether the instructions work. A documentation site with executable examples that are deliberately wrong, a tutorial showing the error before the fix, or a page demonstrating a lint warning, will fail the recommended rules for reasons that are correct.
The second is the CommonMark assumption. The recommended configuration assumes CommonMark format, so a project that relies on a specific dialect, with its own extensions, may see false positives on constructs the dialect allows.
The third is the version coupling. The plugin needs ESLint v9.15.0 or greater, with type compatibility only guaranteed from v9.39.0. A team still on ESLint 8 with the legacy .eslintrc configuration cannot use it as a language plugin, and the flat config in the examples has no equivalent in the old configuration format.
The fourth is what it does not replace. A Markdown linter for prose style, a link checker that follows the links, and a documentation site generator that compiles and runs the examples are all separate tools with a different failure model.
Compare that with running the examples in a test suite. The difference in approach is that a test executes the example against the real library and fails when the behaviour is wrong, while a lint only proves the example parses and follows the configured rules. A lint catches a syntax error and a wrong import; a test catches a wrong expectation. The plugin is cheaper and it runs on every file you commit, including the ones nobody has a test for, and it is a poor substitute for a test suite over the same examples.
Licence, releases and what to check before you switch it on
The plugin is released under the MIT licence, with the licence field in the package manifest and a LICENSE file at the repository root. The MIT terms are permissive, so there is no obstacle to using it in a commercial repository, and read the LICENSE file yourself for the exact wording.
The release history shows a maintained 8.x line. v8.0.1 came on 2026-03-30, v8.0.2 on 2026-05-20 and v8.0.3 on 2026-07-01. The repository is not archived, and the last push was on 2026-10-21, so the tree is a few days old relative to today.
Note the version gap. The plugin is at 8.0.3 while the required ESLint is 9.15 or greater, and the two version numbers track different things, so pin the plugin version you test against rather than assuming the latest works with your ESLint.
The release process is automated, with a release-please-manifest.json and a release-please-config.json in the tree, and a CHANGELOG.md that is generated from it. The pre-commit hook is lint-staged, and the package manifest declares it under gitHooks.
What to check before you switch it on is short. Add the plugin to a flat config with the recommended preset and run it over your documentation to see the current damage, which is usually a batch of fenced code blocks with no language tag and a few heading level jumps. Then turn on fenced-code-language, which is recommended and is the rule most likely to need a sweep of your files. Finally, read the rules that are not in the recommended set, because no-html, no-bare-urls and fenced-code-meta are off by default and you should decide deliberately whether you want them.
Editorial conclusion
The plugin fits a team whose documentation is full of JavaScript examples that nobody checks, since it makes those examples first class lint targets under the same ESLint version and the same rules as the application code. It does not fit a documentation site whose code samples are deliberately invalid, and it does not cover the rendering of the prose itself. Before adopting it, add the plugin to a flat config with the recommended preset, turn on fenced-code-language so unlabeled blocks are flagged, and check the rules that are not in the recommended set, since no-html, no-bare-urls and fenced-code-meta are off by default.
Frequently asked questions
What does the @eslint/markdown plugin do?
It is the official ESLint language plugin for Markdown, so Markdown files are parsed as source and linted like any other file. That covers the JS, JSX, TypeScript and more inside fenced code blocks, and it adds rules for the Markdown structure itself such as headings, links and definitions.
Which ESLint version does @eslint/markdown need?
The plugin is installed alongside ESLint v9.15.0 or greater, and type compatibility is guaranteed with ESLint v9.39.0 or greater. The configuration is a flat config, so the legacy .eslintrc setup is not used.
How do I enable the plugin in eslint.config.js?
Import defineConfig from eslint/config and the plugin as markdown, then add a config block with files set to **/*.md, the markdown plugin registered, and extends set to markdown/recommended. You can add rules to that block, for example setting markdown/no-html to error.
What is the difference between the recommended and processor configurations?
The recommended configuration lints all .md files with the recommended rules and assumes CommonMark format. The processor configuration enables extracting code blocks from all .md files so those blocks can be individually linted.
Which @eslint/markdown rules are not enabled by default?
The table marks no-html, which disallows HTML tags, no-bare-urls, which disallows bare URLs, and fenced-code-meta, which requires or disallows metadata for fenced code blocks as not recommended. Rules such as fenced-code-language, heading-increment and require-alt-text are recommended.
What licence does @eslint/markdown use?
The plugin is released under the MIT licence, with the terms in the LICENSE file at the repository root. The current published version is 8.0.3 and it is available on npm and on JSR for Deno.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/eslint-markdown)Was this useful?