skip to content
Jason Worden
Table of Contents

Astro makes it easy to build a blog out of Markdown. That is the good part.

The annoying part shows up later, after the site has real posts, README snippets, maybe a few docs pages, and one or two Mermaid diagrams. A typo in a diagram can still be valid Markdown. Astro can still build the page. Then the reader gets a red Mermaid error box where your architecture diagram was supposed to be.

I do not want content errors to be a browser surprise. I want them to fail in CI.

For a new Astro blog, I want these to be true before a post ships:

  • The repo is formatted.
  • JS, TS, and Astro linting pass.
  • Markdown structure is checked.
  • Mermaid diagrams parse.
  • Astro says the site is type-safe.
  • The site builds.

Here is the setup that gets you there:

  • Prettier formats code, Astro files, and Markdown.
  • ESLint checks JavaScript, TypeScript, and Astro files.
  • astro check validates Astro templates and content-aware TypeScript.
  • markdownlint-cli2 checks Markdown structure.
  • @mermaid-lint/markdownlint validates Mermaid fences inside that same Markdown lint pass.
  • pnpm verify runs the whole gate before a post ships.

The tools overlap in files, but not in jobs.

flowchart TD
  A["author Markdown"] --> B["Prettier formats"]
  B --> C["markdownlint checks Markdown"]
  C --> D["@mermaid-lint/markdownlint checks Mermaid fences"]
  D --> E["astro check validates site types"]
  E --> F["astro build proves the Astro site builds"]

A flowchart in Mermaid

Install the tools

Start with the boring stack. Boring is a feature here.

Terminal window
pnpm add -D prettier prettier-plugin-astro
pnpm add -D eslint @eslint/js eslint-plugin-astro typescript-eslint eslint-config-prettier globals
pnpm add -D markdownlint-cli2 @mermaid-lint/markdownlint
pnpm add -D @astrojs/check typescript

Then add scripts that say exactly what they do:

{
"scripts": {
"check": "astro check",
"format": "prettier . --write",
"format:check": "prettier . --check",
"lint": "pnpm lint:code && pnpm lint:markdown",
"lint:code": "eslint \"**/*.{js,mjs,ts,astro}\"",
"lint:markdown": "markdownlint-cli2",
"verify": "pnpm format:check && pnpm lint && pnpm check && pnpm build"
}
}

You can add tests to verify too. The important part is that contributors have one local command that matches CI closely enough to trust.

Format with Prettier

Prettier is not a Markdown linter. It is the tool that keeps formatting out of code review.

This config uses 2-space indentation, spaces instead of tabs, single quotes in JS/TS, and normal-looking double quotes inside Markdown frontmatter and examples.

prettier.config.mjs
export default {
plugins: ['prettier-plugin-astro'],
printWidth: 100,
proseWrap: 'preserve',
singleQuote: true,
tabWidth: 2,
useTabs: false,
overrides: [
{
files: ['*.md', '**/*.md'],
options: {
singleQuote: false,
},
},
],
};

That Markdown override is small, but it keeps examples from looking weird. JavaScript can use single quotes. YAML, JSON snippets, and shell examples can keep the quotes readers expect.

Lint Markdown and Mermaid together

This is the key move: if your diagrams live in Markdown, lint them with Markdown.

Do not add a separate diagram-only CI step unless you need one. Do not maintain a custom list of diagram files. Let the Markdown lint command own authored content, including fenced Mermaid blocks.

.markdownlint-cli2.mjs
import mermaid from '@mermaid-lint/markdownlint';
export default {
globs: ['README.md', 'docs/**/*.md', 'src/content/**/*.md'],
ignores: ['node_modules/**', 'dist/**', '.astro/**'],
customRules: mermaid,
config: {
default: true,
MD013: false,
MD010: { code_blocks: false },
MD033: false,
MD041: false,
},
};

That gives you two checks from one command:

  1. markdownlint-cli2 checks Markdown structure.
  2. @mermaid-lint/markdownlint validates every fenced mermaid block.

There is no separate mermaid-lint --strict command in this setup. The package here is the markdownlint adapter, @mermaid-lint/markdownlint, and the gate is pnpm lint:markdown.

The dedicated @mermaid-lint/cli is still useful when you want a standalone command, JSON output, or a custom CI step. For an Astro blog, the markdownlint adapter is the cleaner default.

Why Astro build is not enough

Astro can prove a lot about your site. It can check content collections, route generation, imports, templates, TypeScript usage, and whether the site builds.

It does not make every Mermaid source block correct.

This is valid Markdown:

flowchart TD
A -> B

It is not valid Mermaid. The edge should usually be -->, not ->.

With @mermaid-lint/markdownlint in the Markdown lint pass, that typo fails before the post is merged. Without it, the first person to find the problem may be a reader.

Add ESLint and astro check

Keep code linting separate from content linting.

For Astro projects, ESLint should own JS, TS, and Astro lint rules. Astro should own framework-aware type and template checking through astro check.

I do not add standalone tsc --noEmit by default for the Astro site itself. .astro files and content collections are not plain TypeScript. astro check understands that layer.

If your repo has a separate package of pure TypeScript utilities, add tsc --noEmit there. For the Astro app, start with astro check.

What about Biome?

Biome is worth watching, but I would not use it as the Markdown linting answer for this setup yet. Biome has an open Markdown support issue, and today this recipe needs a Markdown linter that can also run the Mermaid custom rule.

So the recommendation stays boring for now: Prettier formats Markdown; markdownlint-cli2 lints Markdown; @mermaid-lint/markdownlint validates diagrams.

Skip Airbnb and Standard for this

I would not add Airbnb or Standard to a fresh Astro blog unless the team already prefers them.

The mismatch is concrete. Current eslint-config-airbnb peers against ESLint 7/8 and expects React, JSX a11y, React Hooks, and import plugins. This setup is an Astro content site on modern flat ESLint config, and it does not need a React lint stack. standard also brings its own ESLint 8-era dependency tree plus JSX/React-related packages.

Start smaller:

  • @eslint/js
  • typescript-eslint
  • eslint-plugin-astro
  • eslint-config-prettier
  • globals

Then add project-specific rules when the project earns them.

Put it in GitHub Actions

Once the local command works, GitHub Actions can stay simple:

name: test
on:
pull_request:
push:
branches: [main]
jobs:
hygiene:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: 24
cache: pnpm
- uses: pnpm/action-setup@v4
with:
run_install: false
- run: pnpm install --frozen-lockfile
- run: pnpm verify

You can split format, lint, check, tests, and build into separate GitHub Actions jobs later if your repo needs faster feedback. Start with the one command people can also run locally.

That is the setup

That is the whole idea: one local command, one CI gate, and a content pipeline that refuses to publish broken docs.