CLI tool
markuplint/markuplint avatar
markuplint/markuplint

markuplint: an HTML linter that understands the template around the HTML

An HTML linter for all markup developers.

627 stars70 forksTypeScriptMIT

At a glance

What is it?
markuplint lints HTML, but its distinguishing move is that it parses the template first, so JSX, Vue, Svelte, Astro, Alpine.js, Pug and PHP files are checked as what they are rather than as broken HTML. It ships as a CLI, a VS Code extension and a playground, and it is built as a monorepo of small packages.
Who is it for?
Adopt markuplint if you write markup inside a template language, because a plain HTML linter sees your component files as malformed HTML and reports noise, while markuplint parses the template and lints the markup inside it. Do not adopt it expecting a single binary with a stable configuration format, since the tool is a monorepo of packages with a JSON schema for the config and separate spec packages you have to keep in step.
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 received new commits within the last day.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The problem with linting markup inside templates

The tagline is peace of mind in your markup, and the description is an HTML linter for all markup developers, which sounds like a small idea until you look at the topic list of the repository: Alpine.js, custom elements, HTMX, JSX templates, Liquid, Mustache templates, Nunjucks, PHP, Pug, React, Svelte and Vue. A conventional HTML linter pointed at a .vue or .jsx file sees a syntax that is not HTML and reports errors that are not errors. markuplint's approach is to parse the template first and lint the markup inside it, which means the rule set has to know about the surrounding syntax. That is the whole idea in one sentence, and it is why the repository is a collection of packages rather than a single linter, since each template language needs its own parser to make the idea work.

Five core packages, each with a job

The package table in the README shows the decomposition. The CLI package is called markuplint and runs on Node.js, and it is the piece you install as a dev dependency and run in a script or in CI. The core package, @markuplint/ml-core, is universal and holds the linter engine, so it can run in a browser or an editor as well as in Node. The rules live in @markuplint/rules, also universal, which is what makes the rule set shareable between the CLI and the other front ends. The HTML parser is a separate package, @markuplint/html-parser, and the specifications and schemas are a fifth package, @markuplint/html-spec, which is where the definition of what the HTML rules check against lives. Separating the spec from the rules is the detail that pays off over time, since the specification data can be regenerated and updated without touching the rule implementations, and the up:gen script in the repository exists to regenerate it.

Parser plugins, including a spec-only one for HTMX

The template parser plugins are the extension points. There is a parser package for JSX, one for Vue, one for Svelte, one for Astro, one for Alpine.js, one for Pug and one for PHP, and the README points to further packages beyond that list. The HTMX entry is different and worth noticing. Instead of a parser, HTMX is delivered as @markuplint/htmx-spec, marked spec-only, which means the tool knows the HTMX attributes and their rules without needing its own way to parse the file. That is a sensible split for a directive-based extension, where the attributes are what matter rather than the template syntax around them. The same logic applies to the topic list, where Liquid, Mustache and Nunjucks appear, since those are template languages where the markup around the expressions still has to be linted. Each plugin is its own npm package, so you install only the ones your project needs.

A monorepo with a strict toolchain

The repository is a monorepo, and the root package.json shows the discipline. It is private, typed as modules, and requires Node 24 or newer. Workspaces are managed with lerna, and the build, clean and dev scripts all run through it, with dev running packages in parallel. Tests use Vitest with type checking enabled, and there are separate configurations for cross-platform runs and for the Nu validator. Linting is not one tool but three in parallel: oxlint with the .oxlintrc.json configuration, oxfmt for formatting across packages, the editor extension, the website and scripts, and actionlint for the GitHub Actions workflows. The formatting script covers TypeScript, JavaScript variants, JSON, YAML, Markdown, MDX and CSS, which is a good indication of how wide the project surface is. The repository also carries a .textlintrc for prose, a .commitlintrc in both JavaScript and TypeScript forms, husky hooks and a .mise.toml, so the tooling covers commits, hooks, runtimes and prose rather than only the source files.

Releases are conventional commits run through lerna

The release scripts are unusually explicit. The main release command runs lerna version with the exact flag, conventional commits and the conventional-graduate option, and no push, so tagging is prepared locally and the push is a separate decision. There are three matching prerelease scripts, one each for alpha, beta and release candidates, and the prerelease workflow runs the build and the tests first. The up script is the maintenance counterpart, with up:deps running npm-check-updates across the workspaces in interactive mode and grouped by format, up:gen regenerating the HTML spec package, and up:schema running the schema task in each package. The published version history is quick at the moment, with v5.1.0 on 2026-10-08, v5.1.1 on 2026-10-09 and v5.2.0 on 2026-10-13, and the last push on 2026-10-20, on the dev branch, which is the default branch in the repository.

A CLI, a VS Code extension and a playground

Three ways to use it are documented. The website is markuplint.dev and there is a browser playground at playground.markuplint.dev, which matters because the core package is universal and can run in a browser, so you can try a configuration without installing anything. For editors there is a Visual Studio Code extension on the marketplace, and the repository contains a vscode/ directory with its own build script, plus a separate extension in the packages listing. The container recipe shows how the CLI is meant to run elsewhere, building on a Node 26 image, installing with yarn, running the build and linking the binary into the path:

dockerfile
FROM node:26
WORKDIR /markuplint
RUN yarn
RUN yarn run build

The configuration is validated against a schema, with config.schema.json at the repository root and a schema task per package, so an editor can offer completion and catch typos in your rules file. The licence is MIT, and the README lists the projects the linter was inspired by: HTMLHint, ESLint, Stylelint and textlint.

Editorial conclusion

Adopt markuplint if you write markup inside a template language, because a plain HTML linter sees your component files as malformed HTML and reports noise, while markuplint parses the template and lints the markup inside it. Do not adopt it expecting a single binary with a stable configuration format, since the tool is a monorepo of packages with a JSON schema for the config and separate spec packages you have to keep in step. Verify first that the parser plugin for your template language exists and covers the syntax you use, read the config schema at config.schema.json before writing rules by hand, and check that your Node version satisfies the engines requirement of 24 or newer before wiring it into CI.

Frequently asked questions

What is markuplint and who is it for?

It is an HTML linter for markup developers, published under the slogan peace of mind in your markup. Its distinguishing feature is that it parses the surrounding template language first, so JSX, Vue, Svelte, Astro, Alpine.js, Pug and PHP files are linted as the markup they contain.

Which packages does markuplint consist of?

The repository publishes a CLI package called markuplint for Node.js, plus the universal packages @markuplint/ml-core for the engine, @markuplint/rules for the rule set, @markuplint/html-parser for parsing and @markuplint/html-spec for the specifications and schemas.

Does markuplint support my template language?

There are parser plugins for JSX, Vue, Svelte, Astro, Alpine.js, Pug and PHP, with more listed in the repository, and HTMX is supported through a spec-only package rather than a parser because the attributes are what matter there. The topic list also names Liquid, Mustache and Nunjucks.

How is markuplint run in CI or in a container?

The repository ships a Dockerfile based on a Node 26 image that installs with yarn, runs the build and links the markuplint binary, with the entrypoint set to markuplint.mjs. The root package.json also defines build, test and lint scripts that a CI job can call.

How are markuplint releases produced?

Through lerna with conventional commits: the release script runs lerna version with the exact and conventional-graduate options and no push, and there are separate scripts for alpha, beta and rc prereleases, with a prerelease step that builds and tests first.

Official sources

  1. License: MIT
  2. markuplint/markuplint on GitHub
  3. Project website
  4. README
  5. Releases
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.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/markuplint-markuplint.svg)](https://hysenlabs.com/projects/markuplint-markuplint)

Was this useful?