Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

Welcome to mdbook-lint, a fast and comprehensive markdown linter designed specifically for mdBook projects.

What is mdbook-lint

mdbook-lint is a command-line tool and mdBook preprocessor that helps you maintain high-quality markdown documentation by detecting common issues, enforcing consistent style, and providing mdBook-specific linting rules.

Key Features

  • Fast Performance: Built in Rust for speed and efficiency
  • Comprehensive Rule Set: 55 standard markdown rules, 18 mdBook-specific rules, 17 ADR rules, and 10 content rules (100 total)
  • Flexible Integration: Works as a standalone CLI tool or as an mdBook preprocessor
  • Rustdoc Linting: Lint module-level documentation (//! comments) in Rust source files
  • ADR Validation: Validate Architecture Decision Records (Nygard and MADR 4.0 formats)
  • Configurable: Customize rules and behavior through configuration files
  • Zero Dependencies: Self-contained binary with no external dependencies

Why Use mdbook-lint

Documentation quality matters. Consistent, well-formatted markdown makes your documentation:

  • More readable for contributors and users
  • Easier to maintain across large documentation projects
  • More professional in appearance and structure
  • Less prone to rendering issues in mdBook

Getting Started

Ready to improve your documentation quality? Head over to the Installation guide to get started, or jump straight to Getting Started for a quick walkthrough.

Community and Support

mdbook-lint is open source and welcomes contributions. Visit our GitHub repository to:

  • Report issues
  • Request features
  • Contribute code
  • Browse the source

For development information, see our Contributing guide.

Acknowledgments

mdbook-lint builds on the excellent work of:

  • markdownlint - The original Node.js markdown linter that defined the standard rule set (MD001-MD059)
  • rumdl - A fast Rust markdown linter that inspired our implementation approach

We aim to be compatible with markdownlint's rule definitions while adding mdBook-specific functionality.

Installation

mdbook-lint can be installed through several methods depending on your needs.

Homebrew (macOS/Linux)

If you use Homebrew, you can install mdbook-lint from the tap:

brew tap joshrotenberg/brew
brew install mdbook-lint

From Crates.io

Install via Cargo from crates.io:

cargo install mdbook-lint

By default, this includes all rule sets:

  • standard - 55 markdown syntax rules (MD001-MD060)
  • mdbook - 18 mdBook-specific rules (MDBOOK001-MDBOOK025)
  • content - 10 content quality rules (CONTENT001-CONTENT011)

To install without specific rule sets:

# Without content rules
cargo install mdbook-lint --no-default-features --features standard,mdbook,lsp

# Only standard markdown rules
cargo install mdbook-lint --no-default-features --features standard,lsp

From Source

To install the latest development version or contribute to the project:

git clone https://github.com/joshrotenberg/mdbook-lint.git
cd mdbook-lint
cargo install --path crates/mdbook-lint-cli

The repository root is a virtual workspace manifest, so the path must point at the mdbook-lint-cli crate rather than at ..

Pre-built Binaries

Pre-built binaries for common platforms are available on the GitHub releases page.

Download the appropriate binary for your platform and add it to your PATH.

Requirements

  • Rust 1.88 or later (if building from source)
  • No runtime dependencies required

Verification

After installation, verify that mdbook-lint is working correctly:

mdbook-lint --version

The output shows the installed version:

mdbook-lint x.y.z

Next Steps

Once installed, head to the Getting Started guide to learn how to use mdbook-lint with your projects.

Getting Started

This guide will walk you through using mdbook-lint for the first time.

Quick Start

The fastest way to get started is to run mdbook-lint on some markdown files:

# Lint a single file
mdbook-lint lint README.md

# Lint multiple files
mdbook-lint lint src/*.md docs/*.md

# Lint all markdown files in a directory
mdbook-lint lint .

# Auto-fix violations where possible
mdbook-lint lint --fix src/*.md

Start with the Baseline Preset

Existing documentation often produces too many findings when every stable rule is enabled at once. For incremental CI adoption, start with the curated baseline preset:

mdbook-lint lint --preset baseline docs/

Or commit the selection in .mdbook-lint.toml:

preset = "baseline"
fail-on-warnings = true

The preset contains a small, versioned set of stable structural, syntax, and whitespace checks. Inspect its exact membership with mdbook-lint rules --preset baseline. You can subtract a project-specific rule with disabled-rules or --disable. When the project is ready for the complete stable ruleset, remove preset = "baseline".

Understanding the Output

When mdbook-lint finds issues, it will display them like this:

README.md:15:1: MD013: Line length (line too long, 85 > 80)
src/intro.md:3:1: MD001: Heading levels should only increment by one level at a time

Each line shows:

  • File and location: filename:line:column
  • Rule ID: The specific rule that was violated (e.g., MD013)
  • Description: What the issue is and how to fix it

Your First Configuration

Create a .mdbook-lint.toml file in your project root:

# Fail the build on warnings
fail-on-warnings = true

# Disable rules that don't fit your project
disabled-rules = ["MD013"]  # Allow long lines

# Configure specific rules
[MD007]
indent = 4  # Use 4-space indentation for lists

Using with mdBook

To integrate mdbook-lint with your mdBook project:

  1. Add to book.toml:

    [preprocessor.lint]
    
  2. Build your book:

    
    mdbook build
    

mdbook-lint will now check your markdown files every time you build your book.

Choosing Your Integration: You can run mdbook-lint either as an mdBook preprocessor (shown above) OR as a standalone tool in CI. See CI vs Preprocessor to understand when to use each approach.

Automatic Fixing

mdbook-lint can automatically fix some common violations:

# Fix violations automatically
mdbook-lint lint --fix docs/

# Preview what would be fixed without applying changes
mdbook-lint lint --fix --dry-run docs/

# Apply all fixes, including potentially risky ones
mdbook-lint lint --fix-unsafe docs/

Currently supported fixes include:

  • MD009: Trailing spaces
  • MD010: Hard tabs → spaces
  • MD012: Multiple blank lines
  • MD018/MD019: Heading spacing issues
  • MD022: Blank lines around headings
  • MD023: Indented headings
  • MD027: Blockquote spacing
  • MD030: List marker spacing
  • MD034: Bare URLs
  • MD047: Missing trailing newline
  • And more!

Common Workflow

Here's a typical workflow for using mdbook-lint:

  1. Initial setup: Add configuration file and run first lint
  2. Auto-fix simple issues: Use --fix to handle common problems
  3. Fix remaining issues: Address structural problems manually
  4. Customize rules: Disable rules that don't fit your style
  5. Integrate with build: Add to mdBook or CI pipeline
  6. Maintain quality: Regular linting keeps documentation clean

Exploring Rules

To see all available rules:

# List all rules
mdbook-lint rules

# Show detailed rule descriptions
mdbook-lint rules --detailed

# Filter rules by category
mdbook-lint rules --category structure

# Explain the low-noise baseline preset
mdbook-lint rules --preset baseline --detailed

Next Steps

Configuration

mdbook-lint supports multiple configuration formats and provides flexible options for customizing linting behavior.

Configuration File Formats

mdbook-lint automatically detects and supports multiple configuration formats:

  • TOML: .mdbook-lint.toml or mdbook-lint.toml (recommended)
  • YAML: .mdbook-lint.yaml or .mdbook-lint.yml
  • JSON: .mdbook-lint.json
  • markdownlint: .markdownlint.json (for compatibility)

Configuration Discovery

mdbook-lint searches for configuration files in the following order:

  1. Current directory
  2. Parent directories (recursively up to root)

The first configuration file found is used. To use a specific file instead of discovery, pass --config <FILE> to lint, fix, or rustdoc.

Basic Configuration

For incremental adoption in an existing project, select the maintained low-noise baseline instead of copying a rule allowlist:

preset = "baseline"

Inspect the exact membership with mdbook-lint rules --preset baseline. Remove the setting to move to the complete stable default ruleset.

# Global settings
fail-on-warnings = false
fail-on-errors = true
disabled-rules = ["MD013", "MD033"]
enabled-rules = ["MD001", "MD002"]

# Rule-specific configuration
[MD007]
indent = 2

[MD013]
line-length = 120
code-blocks = false

YAML Format

fail-on-warnings: false
fail-on-errors: true
disabled-rules:
  - MD013
  - MD033
enabled-rules:
  - MD001
  - MD002

MD007:
  indent: 2

MD013:
  line-length: 120
  code-blocks: false

JSON Format

{
  "fail-on-warnings": false,
  "fail-on-errors": true,
  "disabled-rules": ["MD013", "MD033"],
  "enabled-rules": ["MD001", "MD002"],
  "MD007": {
    "indent": 2
  },
  "MD013": {
    "line-length": 120,
    "code-blocks": false
  }
}

Advanced Rule Control

The [rules] Section

The [rules] section provides fine-grained control over which rules run:

[rules]
# Disable all rules by default
default = false

# Only enable specific rules
[rules.enabled]
MD001 = true
MD002 = true
MD013 = true

# Rule-specific configuration still works
[MD013]
line-length = 120

This is particularly useful for:

  • Gradual adoption of linting rules
  • Testing specific rules
  • Creating minimal rule sets

Category-Based Rule Management

Control entire categories of rules at once:

# Enable/disable rule categories
enabled-categories = ["headings", "lists"]
disabled-categories = ["whitespace"]

# Individual rules override categories
enabled-rules = ["MD009"]  # Enable even though whitespace is disabled
disabled-rules = ["MD001"]  # Disable even though headings is enabled

Available categories:

  • headings - Heading-related rules
  • lists - List formatting rules
  • whitespace - Whitespace and blank line rules
  • code - Code block and inline code rules
  • style - General style rules
  • links - Link and reference rules
  • mdbook - mdBook-specific rules

markdownlint Compatibility

mdbook-lint can read .markdownlint.json files for compatibility:

{
  "default": false,
  "MD001": true,
  "MD013": {
    "line_length": 120,
    "code_blocks": false
  }
}

Enable full markdownlint compatibility mode:

markdownlint-compatible = true

This disables rules that are disabled by default in markdownlint.

Global Configuration Options

SettingTypeDefaultDescription
presetstringunsetCurated base rule set ("baseline")
fail-on-warningsbooleanfalseExit with error code on warnings
fail-on-errorsbooleantrueExit with error code on errors
disabled-rulesarray[]List of rule IDs to disable
enabled-rulesarray[]List of rule IDs to explicitly enable
enabled-categoriesarray[]List of categories to enable
disabled-categoriesarray[]List of categories to disable
ignore-pathsarray[]Glob patterns for files to skip entirely
markdownlint-compatiblebooleanfalseEnable markdownlint compatibility
deprecated-warningstring"warn"How to handle deprecated rules ("warn", "info", "silent")
malformed-markdownstring"warn"How to handle malformed markdown ("error", "warn", "skip")

Ignoring Paths

Use ignore-paths to skip files entirely before any rule runs. Patterns are glob-style and behave like .gitignore entries:

ignore-paths = [
    "vendor/",        # a trailing slash ignores everything under a directory
    "drafts/**",      # or use ** explicitly
    "*.backup.md",    # a slash-less pattern matches at any depth
]

* does not cross path separators; ** does. Both ignore-paths and ignore_paths spellings are accepted.

Configuration Precedence

Configuration is resolved in the following order (later overrides earlier):

  1. Built-in defaults
  2. Configuration file (.mdbook-lint.toml, etc.)
  3. mdBook preprocessor config (in book.toml)
  4. Command-line arguments

mdBook Integration

When used as an mdBook preprocessor, configuration can be specified in book.toml:

[preprocessor.lint]
preset = "baseline"
fail-on-warnings = true
disabled-rules = ["MD025"]

[preprocessor.lint.MD013]
line-length = 100

Example Configurations

Baseline - Incremental Adoption

preset = "baseline"
fail-on-warnings = true

Strict - All Rules with Custom Settings

fail-on-warnings = true
fail-on-errors = true

[MD007]
indent = 2

[MD013]
line-length = 80
code-blocks = true
tables = true
headings = true

[MD024]
siblings-only = true

[MD029]
style = "ordered"

mdBook Projects

# Disable rules that conflict with mdBook conventions
disabled-rules = [
    "MD025",  # Multiple H1s are OK in books
    "MD041",  # First line doesn't need to be H1
]

# mdBook-specific rules
enabled-categories = ["mdbook"]

[MD013]
line-length = 100  # Longer lines for documentation

Progressive Adoption

Start with a few rules and gradually enable more:

# Phase 1: Start with formatting rules
[rules]
default = false

[rules.enabled]
MD009 = true  # Trailing spaces
MD010 = true  # Hard tabs
MD012 = true  # Multiple blank lines

# Phase 2: Add heading rules (uncomment when ready)
# MD001 = true  # Heading increment
# MD003 = true  # Heading style

# Phase 3: Add more rules...

Migration from markdownlint

If migrating from markdownlint, start with compatibility mode:

markdownlint-compatible = true

# Then gradually customize...
[MD013]
line-length = 100

Generating Configuration Files

Use the init command to generate a configuration file:

# Generate minimal configuration
mdbook-lint init

# Generate comprehensive configuration with every rule documented
mdbook-lint init --include-all

# Generate in a different format
mdbook-lint init --format yaml
mdbook-lint init --format json

Configuration Examples

For real-world configuration examples:

Configuration Validation

To validate your configuration:

# Check if configuration file is valid
mdbook-lint check .mdbook-lint.toml

# List the available rules and their categories
mdbook-lint rules

Next Steps

CLI Usage

This page documents the command-line interface for mdbook-lint.

Basic Commands

preprocessor

Run as an mdBook preprocessor (reads from stdin, writes to stdout). This is the mode mdBook invokes; you normally do not run it by hand.

mdbook-lint preprocessor

See mdBook Integration for details.

lint

Lint markdown files and directories.

mdbook-lint lint [OPTIONS] [FILES]...

fix

Automatically fix issues in markdown files (shorthand for lint --fix).

mdbook-lint fix [OPTIONS] [FILES]...

rules

List available linting rules by category.

mdbook-lint rules [OPTIONS]

check

Check a configuration file for validity.

mdbook-lint check <CONFIG>

init

Generate a default configuration file.

mdbook-lint init [OPTIONS]

supports

Check whether the preprocessor supports a given renderer (used by mdBook).

mdbook-lint supports <RENDERER>

rustdoc

Lint module-level documentation (//! comments) in Rust source files.

mdbook-lint rustdoc [OPTIONS] [PATHS]...

See Rustdoc Linting for detailed documentation.

lsp

Run as a Language Server Protocol (LSP) server. Available only when mdbook-lint is built with the lsp feature.

mdbook-lint lsp [OPTIONS]

help

Show help information.

mdbook-lint help [COMMAND]

Options

Global Options

  • -h, --help: Print help information
  • -V, --version: Print version information
  • -v, --verbose: Enable verbose output
  • -q, --quiet: Suppress non-error output

Lint Options

  • --config <FILE>: Use specific configuration file
  • --fail-on-warnings: Exit with error code on warnings
  • --disable <RULES>: Disable specific rules (comma-separated)
  • --enable <RULES>: Enable only specific rules (comma-separated)
  • --preset <NAME>: Use a curated rule preset (baseline)
  • --fix: Automatically fix violations where possible
  • --fix-unsafe: Apply all fixes, including potentially unsafe ones
  • --dry-run: Show what would be fixed without applying changes (requires --fix or --fix-unsafe)
  • --no-backup: Skip creating backup files when applying fixes
  • --output <FORMAT>: Output format (default, json, github, sarif)
  • --output-file <PATH>: Write the report to a file instead of stdout, used with --output sarif
  • --color <WHEN>: Control colored output (auto, always, never)

Rules Options

  • -d, --detailed: Show detailed information about each rule
  • -c, --category <CATEGORY>: Filter by rule category
  • -p, --provider <PROVIDER>: Show only rules from a specific provider
  • --standard-only: Show only standard rules (MD001-MD059)
  • --mdbook-only: Show only mdBook-specific rules
  • --preset <NAME>: Show only rules in a curated preset
  • --format <FORMAT>: Output format (default, json)
  • --json: Output in JSON format (shorthand for --format json)

For linting commands, --preset baseline may be combined with --disable, which subtracts rules from the preset. It conflicts with --enable, --standard-only, and --mdbook-only where those flags are available because they choose a different base rule set.

Output Format

By default, mdbook-lint displays violations in a cargo/rustc-style format with colors:

error[MD001]: Expected heading level 2 but got level 3
  --> src/chapter.md:15:1
     |
  15 | ### Skipped heading level
     | ^^^ heading-increment

warning[MD009]: Trailing spaces detected
  --> src/intro.md:8:42
     |
   8 | This line has trailing spaces   
     |                                  ^ no-trailing-spaces

Found: 1 error(s), 1 warning(s)

Output Formats

  • default: Colored, human-readable format (shown above)
  • JSON: Machine-readable JSON output
  • GitHub: GitHub Actions annotation format

Controlling Colors

Use --color to control colored output:

# Auto-detect (default) - colors when terminal supports it
mdbook-lint lint docs/

# Always use colors (useful for CI with color support)
mdbook-lint lint --color always docs/

# Never use colors (useful for piping to files)
mdbook-lint lint --color never docs/ > report.txt

Examples

# Lint current directory
mdbook-lint lint .

# Lint specific files
mdbook-lint lint README.md src/chapter1.md

# Lint with custom config
mdbook-lint lint --config custom-lint.toml src/

# Auto-fix violations where possible
mdbook-lint lint --fix docs/

# Preview fixes without applying them
mdbook-lint lint --fix --dry-run docs/

# Apply all fixes including potentially unsafe ones
mdbook-lint lint --fix-unsafe docs/

# Fix without creating backup files
mdbook-lint lint --fix --no-backup docs/

# Show all rules with descriptions
mdbook-lint rules --detailed

# Lint and fail on warnings
mdbook-lint lint --fail-on-warnings docs/

Exit Codes

  • 0: Success (no errors)
  • 1: Linting errors found
  • 2: Invalid arguments or configuration

Next Steps

Rustdoc Linting

The rustdoc subcommand extracts and lints module-level documentation comments (//!) from Rust source files. This helps maintain high-quality documentation in your Rust crates.

Basic Usage

# Lint a single file
mdbook-lint rustdoc src/lib.rs

# Lint all Rust files in a directory (recursive)
mdbook-lint rustdoc src/

# Lint the entire crate
mdbook-lint rustdoc .

How It Works

The rustdoc command:

  1. Finds all .rs files in the specified paths (recursively for directories)
  2. Extracts module-level documentation (//! comments) from each file
  3. Converts the documentation to markdown
  4. Lints the markdown using standard rules
  5. Maps violation line numbers back to the original source locations

What Gets Extracted

Only module-level documentation comments are extracted:

#![allow(unused)]
fn main() {
//! This line IS extracted (module-level doc)
//! This line IS extracted too

/// This line is NOT extracted (item-level doc)
fn example() {}

// This line is NOT extracted (regular comment)
}

The extraction stops when it encounters:

  • A regular comment (//)
  • Any non-comment code
  • End of file

Default Disabled Rules

Some rules are disabled by default for rustdoc because they don't apply well to documentation comments:

RuleNameReason
MD041first-line-headingRustdoc often starts with a description, not a heading
MD047trailing-newlineDoc comments don't have trailing newlines
MD025single-h1Rustdoc idiomatically uses multiple # sections

You can re-enable these rules if needed:

mdbook-lint rustdoc --enable MD041,MD025 src/

Options

The rustdoc command supports most of the same options as lint:

# Use a specific configuration file
mdbook-lint rustdoc --config .mdbook-lint.toml src/

# Disable specific rules
mdbook-lint rustdoc --disable MD013,MD033 src/

# Enable only specific rules
mdbook-lint rustdoc --enable MD001,MD003,MD018 src/

# Output as JSON
mdbook-lint rustdoc --output json src/

# Fail on warnings (useful in CI)
mdbook-lint rustdoc --fail-on-warnings src/

# Verbose output showing which files are checked
mdbook-lint rustdoc --verbose src/

Directory Handling

When given a directory, the command:

  • Recursively finds all .rs files
  • Skips hidden directories (starting with .)
  • Skips the target/ directory
  • Processes files in parallel for performance
# This will skip .git/, target/, and any hidden directories
mdbook-lint rustdoc .

Example Output

warning[MD018]: No space after hash on atx style heading
  --> src/lib.rs:5:3
     |
   5 | //! ##Bad heading
     |   ^ no-missing-space-atx

warning[MD032]: Lists should be surrounded by blank lines
  --> src/parser.rs:12:1
     |
  12 | //! - First item
     | ^^^ blanks-around-lists

Found: 2 warning(s)

Note how the line numbers point to the actual source file locations, making it easy to find and fix issues.

CI Integration

GitHub Actions

- name: Lint rustdoc
  run: mdbook-lint rustdoc --fail-on-warnings --output github src/

The --output github format produces GitHub Actions annotations that appear inline in pull request diffs.

Generic CI

- name: Lint rustdoc
  run: mdbook-lint rustdoc --fail-on-warnings .

Common Patterns

Linting Before Publishing

Add to your CI pipeline to catch documentation issues before publishing:

# In your CI script
cargo fmt --check
cargo clippy -- -D warnings
cargo test
mdbook-lint rustdoc --fail-on-warnings .
cargo publish --dry-run

Workspace Projects

For Cargo workspaces, lint each crate:

mdbook-lint rustdoc crates/

Or lint specific crates:

mdbook-lint rustdoc crates/core/src crates/cli/src

Combining with Regular Linting

If you have both a Rust library and an mdBook:

# Lint the book
mdbook-lint lint docs/

# Lint the rustdoc
mdbook-lint rustdoc src/

Limitations

  • Only extracts //! (module-level) documentation, not /// (item-level)
  • Does not parse doc attributes (#![doc = "..."])
  • Code blocks inside documentation are not validated for correctness (use cargo test --doc for that)

Tips for Better Rustdoc

  1. Use headings consistently: Stick to conventional sections like # Examples, # Panics, # Errors, # Safety

  2. Include code examples: They serve as both documentation and tests

    #![allow(unused)]
    fn main() {
    //! # Examples
    //!
    //! ```rust
    //! let result = my_function(42);
    //! assert_eq!(result, 84);
    //! ```
    }
  3. Keep line lengths reasonable: Long lines in doc comments are hard to read in source

  4. Use proper markdown: Lists need blank lines around them, headings need proper formatting

Next Steps

mdBook Integration

mdbook-lint can run as an mdBook preprocessor, so every chapter is checked as part of mdbook build and mdbook serve. The preprocessor reports diagnostics without changing chapter content.

If you are deciding between build-time checks and a separate CI lint step, see CI vs Preprocessor.

Installation

Install mdbook-lint where mdBook can find it on PATH:

cargo install mdbook-lint

Prebuilt binaries are also available from GitHub Releases.

Verify the installation before configuring the book:

mdbook-lint --version
mdbook --version

Basic setup

Add the preprocessor to book.toml:

[preprocessor.lint]

mdBook derives the mdbook-lint command from the lint preprocessor name. You can make the command explicit if needed:

[preprocessor.lint]
command = "mdbook-lint"

The preprocessor now runs during normal mdBook commands:

mdbook build
mdbook serve
mdbook test

By default, warnings are reported without failing the build. Errors fail the build.

Configuration

There are two supported configuration sources for preprocessor mode:

  1. A discovered mdbook-lint configuration file, preferably .mdbook-lint.toml.
  2. Supported keys in [preprocessor.lint] in book.toml.

The external file is the place for rule-specific settings and the broader CLI configuration surface:

# .mdbook-lint.toml
fail-on-warnings = false
disabled-rules = ["MD013", "MD033"]

[MD024]
siblings_only = true

[MD040]
language_optional = false

See Configuration for all global settings and rule-specific syntax.

Supported book.toml keys

The [preprocessor.lint] table accepts these mdbook-lint settings:

SettingTypePurpose
presetstringSelect a curated base rule set ("baseline")
fail-on-warningsbooleanFail the mdBook build when warnings are found
fail-on-errorsbooleanFail the mdBook build when errors are found
enabled-rulesarrayRun only the listed rule IDs
disabled-rulesarraySkip the listed rule IDs
enabled-categoriesarrayEnable the listed rule categories
disabled-categoriesarrayDisable the listed rule categories

For example:

[preprocessor.lint]
preset = "baseline"
fail-on-warnings = true
disabled-rules = ["MD014"]
disabled-categories = ["whitespace"]

Rule-specific tables and options such as ignore-paths belong in the external configuration file. They are not read from book.toml.

Configuration precedence

Preprocessor configuration is resolved in this order, with later supported values overriding earlier values:

  1. Built-in defaults.
  2. The first configuration file discovered from the book root upward.
  3. [preprocessor.lint] in book.toml.

The discovery order at each directory is:

  1. .mdbook-lint.toml
  2. mdbook-lint.toml
  3. .mdbook-lint.yaml
  4. .mdbook-lint.yml
  5. .mdbook-lint.json

The CLI --config option applies to standalone commands such as mdbook-lint lint; mdBook does not pass it to the preprocessor.

Environment variables

mdbook-lint does not provide environment-variable configuration overrides. Set build behavior in book.toml, use a discovered configuration file, or run the standalone CLI with explicit flags in CI.

Common workflows

Strict CI and informational local builds

Keep the preprocessor informational for local builds:

[preprocessor.lint]
fail-on-warnings = false

Run a separate strict lint step in CI:

mdbook-lint lint --config .mdbook-lint.toml --fail-on-warnings src/
mdbook build

This avoids relying on an environment-variable override and gives CI direct control over paths and output format.

Progressive adoption

Start with the maintained low-noise preset:

[preprocessor.lint]
preset = "baseline"

Inspect the exact list with mdbook-lint rules --preset baseline. Remove preset when the book is ready to use the complete stable default rule set.

Rule-specific configuration

Keep rule behavior in .mdbook-lint.toml:

[MD003]
style = "atx"

[MD013]
line_length = 100
code_blocks = false

[MD033]
allowed_elements = ["details", "summary"]

Validate the file before building:

mdbook-lint check .mdbook-lint.toml

GitHub workflows

The most predictable CI setup runs the CLI explicitly and then builds the book:

name: Documentation

on:
  push:
  pull_request:

jobs:
  docs:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable

      - name: Install documentation tools
        run: |
          cargo install mdbook
          cargo install mdbook-lint

      - name: Validate lint configuration
        run: mdbook-lint check .mdbook-lint.toml

      - name: Lint documentation
        run: mdbook-lint lint --fail-on-warnings --output github src/

      - name: Build documentation
        run: mdbook build

Use --config <FILE> on the lint command if the configuration is not named for automatic discovery.

Limitations

  • Preprocessor mode checks every chapter mdBook supplies. It does not support include or exclude chapter globs.
  • Inline HTML comments cannot enable or disable rules for a portion of a file.
  • Preprocessor diagnostics do not have selectable concise, detailed, or JSON formats. The standalone lint command supports default, json, and github output.
  • mdbook-lint does not consume RUST_LOG; use --verbose with standalone CLI commands and mdbook build -v for mdBook diagnostics.

Troubleshooting

If the preprocessor does not run, first check that both executables are on PATH and that book.toml contains [preprocessor.lint]:

command -v mdbook
command -v mdbook-lint
mdbook build -v

For configuration problems, validate the external file and compare standalone behavior:

mdbook-lint check .mdbook-lint.toml
mdbook-lint --verbose lint --config .mdbook-lint.toml src/

See the Troubleshooting Guide for more diagnostic steps.

Next steps

CI vs Preprocessor: Choosing Your Integration Strategy

This guide helps you choose between running mdbook-lint as an mdBook preprocessor or as a standalone CLI tool in CI, and explains why you typically want one approach but not both.

TL;DR Recommendation

For CI/CD pipelines: Use the standalone CLI. For local development: Use the preprocessor (optional).

The standalone CLI gives you more control, better error handling, and avoids configuration discovery issues that can occur in preprocessor mode.

Quick Decision Guide

Use CaseRecommended ApproachWhy
CI/CD pipelinesStandalone CLIMore control, fail fast, better error output
Local development with mdBookPreprocessorAutomatic feedback during mdbook serve
Pure markdown documentation (no mdBook)Standalone CLINo mdBook dependency needed
Need SARIF/GitHub integrationStandalone CLIBetter tool integration options
Complex CI pipeline with multiple checksStandalone CLIMore control over when/how linting runs

Integration Approaches

Approach 1: mdBook Preprocessor (Best for Local Development)

When to use:

  • You want automatic linting during mdbook serve
  • You prefer configuration in book.toml
  • You want immediate feedback while writing

When NOT to use:

  • In CI/CD pipelines (use standalone CLI instead)
  • When you need precise control over exit codes
  • When you need detailed error output for debugging

Setup in book.toml:

[preprocessor.lint]
fail-on-warnings = false  # Set to true for strict mode
disabled-rules = ["MD013", "MD033"]

[MD007]
indent = 4

In CI (GitHub Actions):

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Install mdBook and mdbook-lint

        run: |

          cargo install mdbook
          cargo install mdbook-lint
      - name: Build book (linting happens automatically)
        run: mdbook build
        env:
# Optional: Override settings for CI

          MDBOOK_PREPROCESSOR__MDBOOK_LINT__FAIL_ON_WARNINGS: true

Advantages:

  • Linting happens automatically during mdbook serve
  • Immediate feedback while writing documentation
  • Works seamlessly with local mdBook workflow

Disadvantages:

  • Configuration discovery can be tricky in CI environments
  • Limited control over error handling and exit codes
  • No SARIF output for GitHub Security tab
  • Errors appear inline with mdBook build output
  • Can't fail fast in CI (must start book build first)

When to use:

  • CI/CD pipelines (recommended for all CI use cases)
  • You want to fail fast before other expensive operations
  • You need clear, actionable error output
  • You need SARIF output for GitHub Security integration
  • You don't use mdBook (just markdown files)

Setup with .mdbook-lint.toml:

# Start existing documentation with the low-noise baseline.
preset = "baseline"
fail-on-warnings = true

Inspect the exact selection with mdbook-lint rules --preset baseline. Remove the preset line when the project is ready to adopt the complete stable ruleset. Project-specific exclusions can be added with disabled-rules.

In CI (GitHub Actions):

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
# Option A: Using GitHub Action

      - name: Lint Markdown
        uses: joshrotenberg/mdbook-lint-action@v1
        with:
          files: 'docs/**/*.md'
          format: sarif
          output-file: results.sarif
      
# Option B: Direct installation

      - name: Install and run mdbook-lint

        run: |

          cargo install mdbook-lint
          mdbook-lint lint docs/ --preset baseline --fail-on-warnings
      
# Optional: Upload SARIF results

      - name: Upload SARIF
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: results.sarif

SARIF output

The CLI emits SARIF v2.1.0 directly:

mdbook-lint lint docs/ --output sarif --output-file results.sarif

--output-file writes the report to disk and leaves stdout clean, so normal diagnostics and the process exit status are unaffected. The report is written even when the run fails, which is what a code-scanning upload step needs:

      - name: Lint Markdown
        run: mdbook-lint lint docs/ --output sarif --output-file results.sarif
        continue-on-error: true

      - name: Upload SARIF
        uses: github/codeql-action/upload-sarif@v3
        if: always()
        with:
          sarif_file: results.sarif

A clean run still produces a valid report with an empty results array, so the upload step does not need to special-case it.

Each result carries the rule ID, a severity mapped to the SARIF levels (error, warning, note), and a 1-based line and column. Rule descriptors are derived from rule metadata and link to the published rule reference. Results are sorted by location, so repeated runs over unchanged input produce identical reports.

Advantages:

  • Fails fast in CI pipeline before expensive build steps
  • Clear, standalone error output for debugging
  • SARIF output for GitHub Security tab
  • Full control over when and how linting runs
  • Can run in parallel with other checks
  • Works with or without mdBook
  • Supports smart CLI detection (e.g., mdbook-lint docs/)

Disadvantages:

  • No automatic linting during local mdbook serve
  • Requires explicit invocation in CI workflow
  • Consider adding preprocessor for local development feedback

Why Not Both

Running mdbook-lint both as a preprocessor AND standalone in CI is usually redundant and can cause problems:

Problems with Running Both

  1. Duplicate Work: The same files get linted twice, wasting CI time
  2. Configuration Drift: Two places to maintain rules can lead to inconsistencies
  3. Confusing Failures: Issues might be reported twice in different formats
  4. Maintenance Burden: Updates need to be synchronized in multiple places

Valid Exception: Different Rule Sets

The only scenario where using both makes sense is when you intentionally want different rules:

# CI: Strict linting before build
- name: Strict lint check
  run: mdbook-lint lint docs/ --config .mdbook-lint.strict.toml

# Build: Lenient linting during build
- name: Build with lenient linting
  run: mdbook build  # Uses preprocessor with book.toml config

Migration Strategies

From Preprocessor to Standalone CI

If you're currently using the preprocessor but want to switch to standalone CI:

  1. Extract configuration from book.toml to .mdbook-lint.toml
  2. Remove preprocessor section from book.toml
  3. Update CI to run mdbook-lint before mdbook build
  4. Document the change for your team

From Standalone to Preprocessor

If you're using standalone but want to switch to preprocessor:

  1. Add preprocessor section to book.toml
  2. Copy configuration from .mdbook-lint.toml to book.toml
  3. Remove standalone lint step from CI
  4. Update documentation for developers
# .github/workflows/docs.yml
jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Install mdbook-lint
        run: cargo install mdbook-lint
      - name: Lint documentation
        run: mdbook-lint docs/src/ --preset baseline --fail-on-warnings

For Local Development: Add Preprocessor (Optional)

# book.toml - for local development feedback only
[preprocessor.lint]
fail-on-warnings = false

Combined Setup (Best of Both Worlds)

Use standalone CLI in CI for control and reliability, with optional preprocessor for local development:

# .github/workflows/docs.yml - CI uses standalone
- name: Lint documentation
  run: mdbook-lint docs/src/ --preset baseline --fail-on-warnings
- name: Build book
  run: mdbook build docs/
# book.toml - local development uses preprocessor (optional)
[preprocessor.lint]
fail-on-warnings = false

This approach gives you:

  • Reliable CI with clear error output
  • Fast feedback during local mdbook serve
  • No duplicate configuration (use .mdbook-lint.toml for both)

Common Pitfalls to Avoid

  1. Don't duplicate the same rules in both preprocessor and standalone configs

  2. Don't run both in CI unless you have a specific reason

  3. Don't use || true to ignore failures - fix the issues or disable specific rules

  4. Don't forget to document which approach you're using for new contributors

Summary

  • Use standalone CLI in CI: Better control, clearer errors, fail-fast capability
  • Use preprocessor for local development: Optional, provides feedback during mdbook serve
  • Avoid using both in CI: Redundant and can cause confusion
  • Share configuration: Use .mdbook-lint.toml which works for both modes
  • Be consistent: Document your choice and stick with it across your project

Compatibility

mdbook-lint is designed to work seamlessly across different versions of mdBook and various environments.

mdBook Version Support

mdbook-lint supports both mdBook 0.4.x and 0.5.x series.

Supported Versions

mdBook VersionStatusNotes
0.4.40+SupportedFully tested
0.5.0SupportedJSON format changes handled automatically
0.5.1+SupportedLatest recommended

Automatic Version Detection

When running as an mdBook preprocessor, mdbook-lint automatically detects the mdBook version and handles protocol differences transparently. You don't need to configure anything differently based on your mdBook version.

mdBook 0.5.x Changes

mdBook 0.5.0 introduced breaking changes to the preprocessor JSON protocol:

  • The sections field was renamed to items in book chapter structures
  • The __non_exhaustive marker field was removed

mdbook-lint automatically normalizes these differences, so your configuration and usage remain the same regardless of which mdBook version you use.

Platform Support

mdbook-lint provides prebuilt binaries for all major platforms:

PlatformArchitectureBinary
Linuxx86_64 (glibc)mdbook-lint-linux-x86_64
Linuxx86_64 (musl)mdbook-lint-linux-x86_64-musl
macOSIntel (x86_64)mdbook-lint-macos-x86_64
macOSApple Silicon (aarch64)mdbook-lint-macos-aarch64
Windowsx86_64mdbook-lint-windows-x86_64.exe

Minimum Rust Version

If building from source, mdbook-lint requires:

  • Rust Edition: 2024
  • Minimum Supported Rust Version (MSRV): 1.85.0

Configuration Compatibility

markdownlint Compatibility

mdbook-lint aims for compatibility with markdownlint rule definitions. Standard rules (MD001-MD060) follow the same semantics as markdownlint where applicable.

Configuration differences:

  • mdbook-lint uses TOML configuration by default (.mdbook-lint.toml)
  • YAML and JSON configuration formats are also supported
  • Rule configuration options may have slightly different names

CI Environment Support

mdbook-lint works in all major CI environments:

  • GitHub Actions
  • GitLab CI
  • CircleCI
  • Jenkins
  • Azure Pipelines

See CI vs Preprocessor for guidance on choosing the right integration approach.

Continuous Compatibility Testing

mdbook-lint runs automated compatibility tests against multiple mdBook versions on a weekly schedule. These tests verify that the preprocessor integration works correctly across all supported mdBook versions.

You can view the test results in the mdBook Compatibility workflow on GitHub.

Reporting Compatibility Issues

If you encounter compatibility issues with a specific mdBook version or platform, please open an issue with:

  1. Your mdBook version (mdbook --version)
  2. Your mdbook-lint version (mdbook-lint --version)
  3. Your operating system and architecture
  4. The error message or unexpected behavior
  5. A minimal example that reproduces the issue

Troubleshooting Guide

This guide covers common installation, configuration, preprocessor, rule, and CI problems. Commands and configuration examples match the current mdbook-lint CLI.

Installation

Command Not Found

Confirm whether the executable is on PATH:

command -v mdbook-lint
mdbook-lint --version

For a Cargo installation, the executable normally lives in ~/.cargo/bin:

cargo install mdbook-lint --force
export PATH="$HOME/.cargo/bin:$PATH"

If mdBook runs in CI or a container, install mdbook-lint in that environment as well. Installing it on the host does not make it available inside a container.

Installation from Source Fails

Update the stable Rust toolchain and retry with locked dependencies:

rustup update stable
cargo install mdbook-lint --locked --force

Include the Rust version and the complete Cargo error when reporting a build failure:

rustc --version
cargo --version

Configuration

Configuration Is Not Loading

Validate the file directly:

mdbook-lint check .mdbook-lint.toml

Then run linting with an explicit path and verbose status output:

mdbook-lint --verbose lint --config .mdbook-lint.toml src/

Without --config, mdbook-lint searches the current directory and its parents. At each directory it checks these names in order:

  1. .mdbook-lint.toml
  2. mdbook-lint.toml
  3. .mdbook-lint.yaml
  4. .mdbook-lint.yml
  5. .mdbook-lint.json

The verbose command prints the selected configuration path. A similarly named file outside the search path is not loaded.

Rule Configuration Is Ignored

Rule-specific configuration uses a top-level table named after the rule:

[MD013]
line_length = 100
code_blocks = false

[MD024]
siblings_only = true

Do not nest rule configuration under [core], [rules.config], or [preprocessor.lint.rules]. Those tables are not part of the configuration schema.

Start from the generated reference when you are unsure which options a rule accepts:

mdbook-lint init --include-all --output reference.toml
mdbook-lint rules --detailed

Configuration in book.toml Is Ignored

The mdBook preprocessor reads only these settings from [preprocessor.lint]:

  • preset
  • fail-on-warnings
  • fail-on-errors
  • enabled-rules
  • disabled-rules
  • enabled-categories
  • disabled-categories

For rule-specific settings and other global options, create a discovered .mdbook-lint.toml file in the book root or a parent directory.

For example:

# book.toml
[preprocessor.lint]
preset = "baseline"
fail-on-warnings = true
disabled-rules = ["MD014"]
# .mdbook-lint.toml
[MD013]
line_length = 100

Environment Variables Have No Effect

mdbook-lint does not implement environment-variable configuration overrides. Names such as MDBOOK_PREPROCESSOR__..., MDBOOK_LINT_CONFIG, and RUST_LOG are not read by the application.

Use one of the supported mechanisms instead:

  • a discovered configuration file;
  • supported [preprocessor.lint] keys in book.toml;
  • --config, --fail-on-warnings, --enable, or --disable with the standalone CLI.

Preprocessor Issues

The Preprocessor Does Not Run

Check the executables and the mdBook configuration:

command -v mdbook
command -v mdbook-lint
grep -n "preprocessor.lint" book.toml
mdbook build -v

A minimal book.toml entry is:

[preprocessor.lint]

If command discovery is unusual in your environment, set it explicitly:

[preprocessor.lint]
command = "mdbook-lint"

The Build Fails Unexpectedly

Warnings fail the build only when fail-on-warnings = true. Errors fail by default. Run the linter directly to see the same diagnostics without mdBook's output around them:

mdbook-lint lint src/

To check whether build policy is the cause, inspect book.toml and the discovered configuration file for:

fail-on-warnings = true
fail-on-errors = true

Set RUST_BACKTRACE=1 only when diagnosing a panic. It does not enable normal application logging.

Another Preprocessor Appears to Conflict

mdbook-lint does not modify chapter content, so content-order conflicts are unusual. Temporarily remove other preprocessor tables from a copy of book.toml, rebuild, and add them back one at a time to identify the source.

Do not rely on mdbook-lint-specific before or after keys; mdbook-lint does not read or enforce them.

Rule Behavior

A Rule Reports a False Positive

First reproduce the result with only that rule enabled:

mdbook-lint lint --enable MD033 path/to/file.md

If the rule has supported options, configure its top-level table:

[MD033]
allowed_elements = ["details", "summary"]

Otherwise disable it globally or for that command:

disabled-rules = ["MD033"]
mdbook-lint lint --disable MD033 src/

Inline mdbook-lint-disable HTML comments are not implemented. If a rule needs per-file or inline suppression, open a feature request separately from the false-positive report.

When reporting a bug, include the smallest input that reproduces it, the rule ID, the exact command, and mdbook-lint --version.

Rules Appear to Conflict

Run each rule independently to identify which diagnostics and fixes overlap:

mdbook-lint lint --enable MD018 path/to/file.md
mdbook-lint lint --enable MD020 path/to/file.md

Preview automatic fixes before applying them:

mdbook-lint lint --fix --dry-run path/to/file.md

If two automatic fixes conflict, report both rule IDs and the original input.

Performance

Compare build time with and without the preprocessor enabled in a temporary copy of book.toml:

time mdbook build

Then select the maintained baseline rather than adding unsupported chapter globs:

[preprocessor.lint]
preset = "baseline"

Preprocessor mode checks the chapters supplied by mdBook and does not implement include or exclude patterns. If path-level filtering is required, run the standalone CLI on explicit paths and use ignore-paths in .mdbook-lint.toml.

CI

CI Differs from Local Results

Print tool versions and validate the same configuration used locally:

mdbook --version
mdbook-lint --version
mdbook-lint check .mdbook-lint.toml
mdbook-lint lint --config .mdbook-lint.toml --fail-on-warnings src/

Pinning versions in CI avoids changes caused by installing different releases on different runs.

For GitHub Actions annotations, use the standalone output format:

mdbook-lint lint --output github --fail-on-warnings src/

Preprocessor output does not have a configurable JSON or GitHub format.

Useful Diagnostics

Use the CLI's supported status and output options:

mdbook-lint --verbose lint src/
mdbook-lint lint --output json src/
mdbook-lint lint --color never src/
mdbook build -v

List rules and inspect configuration separately:

mdbook-lint rules --detailed
mdbook-lint check .mdbook-lint.toml

Getting Help

Search or open an issue at https://github.com/joshrotenberg/mdbook-lint/issues. Include:

  • mdbook-lint --version;
  • mdbook --version when preprocessor mode is involved;
  • the relevant configuration with secrets removed;
  • the smallest input that reproduces the problem;
  • the exact command and complete diagnostic output.

For configuration syntax, also see Configuration. For preprocessor setup, see mdBook Integration.

Rules Reference

mdbook-lint provides comprehensive markdown linting with three rule categories.

Standard Markdown Rules

59 rules (MD001-MD059) based on the widely-used markdownlint specification. These rules ensure consistent markdown formatting and style.

Categories

mdBook-Specific Rules

Rules specifically designed for mdBook projects, validating mdBook-specific syntax and conventions.

Rules

ADR Rules

17 rules (ADR001-ADR017) for validating Architecture Decision Records against Nygard and MADR 4.0 formats.

Categories

  • Structure Rules (ADR001-ADR006) - Title, status, date, required sections
  • Validation Rules (ADR007-ADR009) - Status values, date format, filename
  • Collection Rules (ADR010-ADR013) - Multi-document analysis
  • Content Quality Rules (ADR014-ADR017) - Meaningful content validation

Quick Reference

Rules with Automatic Fix Support

The following rules can automatically fix violations:

  • MD009 - Remove trailing spaces
  • MD010 - Replace hard tabs with spaces
  • MD012 - Remove multiple consecutive blank lines
  • MD018 - Add space after hash in ATX headings
  • MD019 - Fix multiple spaces after hash
  • MD020 - Add missing spaces inside closed ATX headings
  • MD021 - Fix multiple spaces inside closed ATX headings
  • MD023 - Remove indentation from headings
  • MD027 - Fix multiple spaces after blockquote symbol
  • MD030 - Fix spaces after list markers
  • MD034 - Wrap bare URLs in angle brackets
  • MD047 - Ensure files end with single newline

Disabling Rules

Rules can be disabled globally or for specific files:

# Disable globally
[rules]
MD002 = false
MD041 = false

# Disable for specific files
[ignore]
MD013 = ["CHANGELOG.md", "docs/api/*.md"]

Standard Markdown Rules

mdbook-lint implements 59 standard markdown linting rules based on the markdownlint specification. These rules help maintain consistent, readable, and properly formatted markdown documentation.

Rule Categories

Heading Rules

Rules for heading hierarchy, formatting, and style consistency.

List Rules

Rules for list formatting, indentation, and marker consistency.

Whitespace Rules

Rules for managing spaces, tabs, and blank lines.

Rules for URL formatting, link text, and reference links.

Code Rules

Rules for code blocks, inline code, and fencing style.

Emphasis Rules

Rules for bold, italic, and other emphasis formatting.

Complete Rule List

Rule IDNameDescriptionFix
MD001heading-incrementHeading levels should only increment by one level at a time
MD002first-heading-h1First heading should be a top-level heading
MD003heading-styleHeading style
MD004ul-styleUnordered list style
MD005list-indentInconsistent indentation for list items at the same level
MD006ul-start-leftConsider starting lists at the beginning of the line
MD007ul-indentUnordered list indentation
MD008no-bare-urlsBare URLs should be wrapped in angle brackets
MD009no-trailing-spacesTrailing spaces
MD010no-hard-tabsHard tabs
MD011no-reversed-linksReversed link syntax
MD012no-multiple-blanksMultiple consecutive blank lines
MD013line-lengthLine length
MD014commands-show-outputDollar signs used before commands without showing output
MD015no-missing-space-closed-atxNo space after hash on closed atx style heading
MD016no-reversed-heading-styleHeading levels should only increment
MD017blanks-around-headingsBlank lines around headings
MD018no-missing-space-atxNo space after hash on atx style heading
MD019no-multiple-space-atxMultiple spaces after hash on atx style heading
MD020no-missing-space-closed-atxMissing space inside hashes on closed ATX style heading
MD021no-multiple-space-closed-atxMultiple spaces inside hashes on closed atx style heading
MD022blanks-around-headingsHeadings should be surrounded by blank lines
MD023heading-start-leftHeadings must start at the beginning of the line
MD024no-duplicate-headingMultiple headings with the same content
MD025single-h1Multiple top-level headings in the same document
MD026no-trailing-punctuationTrailing punctuation in heading
MD027no-multiple-space-blockquoteMultiple spaces after blockquote symbol
MD028no-blanks-blockquoteBlank line inside blockquote
MD029ol-prefixOrdered list item prefix
MD030list-marker-spaceSpaces after list markers
MD031blanks-around-fencesFenced code blocks should be surrounded by blank lines
MD032blanks-around-listsLists should be surrounded by blank lines
MD033no-inline-htmlInline HTML
MD034no-bare-urlsBare URL used
MD035hr-styleHorizontal rule style
MD036no-emphasis-as-headingEmphasis used instead of a heading
MD037no-space-in-emphasisSpaces inside emphasis markers
MD038no-space-in-codeSpaces inside code span elements
MD039no-space-in-linksSpaces inside link text
MD040fenced-code-languageFenced code blocks should have a language specified
MD041first-line-h1First line in file should be a top-level heading
MD042no-empty-linksNo empty links
MD043required-headingsRequired heading structure
MD044proper-namesProper names should have correct capitalization
MD045no-alt-textImages should have alternate text
MD046code-block-styleCode block style
MD047single-trailing-newlineFiles should end with a single newline character
MD048code-fence-styleCode fence style
MD049emphasis-styleEmphasis style should be consistent
MD050strong-styleStrong style should be consistent
MD051link-fragmentsLink fragments should be valid
MD052reference-links-imagesReference links and images should use a label that is defined
MD053link-image-reference-definitionsLink and image reference definitions should be needed
MD054link-image-styleLink and image style
MD055table-pipe-styleTable pipe style
MD056table-column-countTable column count
MD057table-rowsTable rows
MD058blanks-around-tablesTables should be surrounded by blank lines
MD059table-alignmentTable alignment

Legend:

  • ✅ Automatic fix available
  • ❌ Manual fix required

Heading Rules

Heading rules ensure proper document structure, hierarchy, and formatting for markdown headings.

Rules in This Category

RuleDescriptionFix
MD001Heading levels should only increment by one level at a time
MD002First heading should be a top-level heading
MD003Heading style (ATX vs Setext)
MD018No space after hash on ATX style heading
MD019Multiple spaces after hash on ATX style heading
MD020Missing space inside hashes on closed ATX style heading
MD021Multiple spaces inside hashes on closed ATX style heading
MD022Headings should be surrounded by blank lines
MD023Headings must start at the beginning of the line
MD024Multiple headings with the same content
MD025Multiple top-level headings in the same document
MD026Trailing punctuation in heading
MD041First line in file should be a top-level heading

Best Practices

Document Structure

A well-structured document follows these heading principles:

  1. Start with H1: Documents should begin with a single H1 heading
  2. Sequential Levels: Never skip heading levels (H1 → H3 is wrong)
  3. Logical Hierarchy: Use headings to create a document outline
  4. Consistent Style: Use either ATX (#) or Setext style consistently

ATX vs Setext Headings

ATX Style (Recommended):

# Heading 1
## Heading 2
### Heading 3

Setext Style (Limited to H1 and H2):

Heading 1
=========

Heading 2
---------

Closed ATX Headings

Some prefer closed ATX headings for symmetry:

#Heading 1#


##Heading 2##


###Heading 3###


Rules MD020 and MD021 ensure proper formatting of closed headings.

Common Issues and Solutions

Issue: Inconsistent Heading Hierarchy

Problem: Jumping between heading levels disrupts document flow.

# Main Title
### Subsection (skips H2)
## Back to H2
##### Deep section (skips H3 and H4)

Solution: Maintain sequential heading levels.

# Main Title
## Section
### Subsection
## Another Section
### Subsection
#### Deeper Content
##### Deepest Content

Issue: Multiple H1 Headings

Problem: Multiple top-level headings confuse document structure.

# First Title
Content...
# Second Title
More content...

Solution: Use a single H1 with H2s for major sections.

# Document Title
## First Section
Content...
## Second Section
More content...

Issue: Indented Headings

Problem: Headings with leading spaces may not render correctly.

    # This might not be a heading
  ## This is problematic

Solution: Start headings at the beginning of the line.

# Proper Heading
## Another Proper Heading

Accessibility Considerations

Proper heading structure is crucial for accessibility:

  • Screen Readers: Use headings to navigate and understand document structure
  • Keyboard Navigation: Many tools allow jumping between headings
  • Document Outline: Assistive technologies generate outlines from headings
  • WCAG Compliance: Proper heading hierarchy is part of WCAG 2.1 guidelines

Integration with mdBook

mdBook relies heavily on proper heading structure:

  1. Table of Contents: Generated from heading hierarchy
  2. Search Index: Headings are weighted in search results
  3. Navigation: Sidebar navigation reflects heading structure
  4. Anchors: Automatic anchor generation for deep linking

Configuration Examples

Enforce ATX Style Only

[MD003]
style = "atx"

Allow Trailing Punctuation

[MD026]
enabled = false

Require Document to Start with H1

[MD041]
level = 1
front_matter_title = false

MD001 - Heading Increment

Heading levels should only increment by one level at a time.

This rule is triggered when you skip heading levels in a markdown document. For example, a heading level 1 should be followed by level 2, not level 3.

Why This Rule Exists

Proper heading hierarchy improves document structure, accessibility, and navigation. Screen readers and document outlines rely on sequential heading levels to convey the document's organization to users.

Examples

❌ Incorrect (violates rule)

# Title

### Subsection (skips h2)

## Back to h2

##### Deep section (skips h3 and h4)

✅ Correct

# Title

## Section

### Subsection

#### Subsubsection

##### Deep section

Configuration

This rule has no configuration options. It always enforces strict sequential heading levels.

When to Disable

Consider disabling this rule if:

  • You're working with generated content that doesn't follow strict hierarchy
  • You're importing documentation from external sources with different conventions
  • Your project has specific heading level requirements

Rule Details

  • Rule ID: MD001
  • Aliases: heading-increment
  • Category: Structure
  • Severity: Warning
  • Automatic Fix: Not available

Common Violations and Solutions

Skipping from H1 to H3

Problem:

# Main Title
### Subsection

Solution:

# Main Title
## Section
### Subsection

Deep Nesting Without Intermediate Levels

Problem:

## Chapter
##### Deep Detail

Solution:

## Chapter
### Section
#### Subsection
##### Deep Detail

Rationale for Sequential Headings

  1. Accessibility: Screen readers rely on proper heading hierarchy to help users navigate documents
  2. Document Structure: Sequential headings create a logical outline
  3. SEO: Search engines use heading structure to understand content hierarchy
  4. Table of Contents: Automated TOC generators expect proper nesting

Integration with mdBook

mdBook's sidebar generation relies on proper heading structure. Violating this rule can lead to:

  • Broken navigation in the sidebar
  • Incorrect TOC generation
  • Poor mobile navigation experience
  • MD002 - First heading should be a top-level heading
  • MD003 - Heading style consistency
  • MD022 - Headings should be surrounded by blank lines
  • MD025 - Multiple top-level headings in the same document

References

MD002 - First Heading H1

First heading should be a top-level heading (H1).

Deprecated: This rule is superseded by MD041 which offers an improved implementation. Consider using MD041 instead.

Why This Rule Exists

Documents should start with a top-level heading (H1) to establish proper hierarchy. This ensures consistent document structure and helps screen readers and document outlines understand the content organization.

Examples

Incorrect

## Introduction

This document starts with an H2.

Correct

# Document Title

## Introduction

The document properly starts with an H1.

Configuration

[MD002]
level = 1  # Expected first heading level (default: 1)

When to Disable

  • When using MD041 instead (recommended)
  • Documents that are fragments or partials
  • Auto-generated content with different conventions

Rule Details

  • Rule ID: MD002
  • Aliases: first-heading-h1
  • Category: Structure
  • Severity: Warning
  • Auto-fix: Yes (can add H1 if missing)
  • Deprecated: Yes (use MD041)
  • MD001 - Heading increment
  • MD041 - First line should be a top-level heading (replacement)
  • MD025 - Single top-level heading

MD003 - Heading Style

Heading style should be consistent throughout the document.

Why This Rule Exists

Markdown supports multiple heading styles. Mixing styles within a document creates visual inconsistency and can confuse readers and tooling.

Heading Styles

# Heading 1
## Heading 2
### Heading 3

ATX Closed Style

#Heading 1#

##Heading 2##

###Heading 3###

Setext Style (H1 and H2 only)

Heading 1
=========

Heading 2
---------

Examples

Incorrect

# ATX Heading

Setext Heading
--------------

### Another ATX

Correct

# Main Title

## Section One

### Subsection

Configuration

[MD003]
style = "atx"  # Options: "atx", "atx_closed", "setext", "consistent"
ValueDescription
atxUse # style headings
atx_closedUse # Heading # style
setextUse underline style (H1/H2 only)
consistentMatch the first heading's style

When to Disable

  • Working with legacy documents using mixed styles
  • Importing content from multiple sources

Rule Details

  • Rule ID: MD003
  • Aliases: heading-style
  • Category: Structure
  • Severity: Warning
  • Auto-fix: Yes
  • MD001 - Heading increment
  • MD018 - Space after hash in ATX headings
  • MD019 - Multiple spaces after hash

MD018 - No Space After Hash on ATX Style Heading

Severity: Warning
Category: Headings
Auto-fix: ✓ Available

Rule Description

This rule ensures there's a space after the hash character(s) in ATX-style headings. The space improves readability and is required by many markdown parsers.

Why This Rule Exists

A space after the hash is important because:

  • Many markdown parsers require it for proper heading recognition
  • Improves readability and consistency
  • Follows CommonMark specification
  • Prevents confusion with other hash-prefixed content

Examples

❌ Incorrect (violates rule)

# Heading without space

## Another heading missing space

### Third level also needs space

✅ Correct

# Heading with proper space
## Another heading correctly formatted
### Third level with space

Configuration

This rule has no configuration options.

Automatic Fix

This rule supports automatic fixing with --fix. The fix will:

  • Add a single space after the hash character(s)
  • Preserve the heading level and content
  • Handle all heading levels (1-6)

Apply Fix

# Fix heading spacing issues
mdbook-lint lint --fix docs/

# Preview what would be fixed
mdbook-lint lint --fix --dry-run docs/

When to Disable

Consider disabling this rule if:

  • You're working with a non-standard markdown parser that doesn't require spaces
  • Your content includes hash-prefixed text that isn't meant to be headings

Disable in Config

# .mdbook-lint.toml
disabled_rules = ["MD018"]

Disable Inline

<!-- mdbook-lint-disable MD018 -->
# NoSpaceHeading

<!-- mdbook-lint-enable MD018 -->
  • MD019 - Multiple spaces after hash on ATX heading
  • MD020 - Missing space inside hashes on closed ATX heading
  • MD021 - Multiple spaces inside hashes on closed ATX heading
  • MD022 - Headings should be surrounded by blank lines
  • MD023 - Headings must start at the beginning of the line

References

MD019 - Multiple Spaces After Hash on ATX Style Heading

Severity: Warning
Category: Headings
Auto-fix: ✓ Available

Rule Description

This rule ensures there's only a single space after the hash character(s) in ATX-style headings. Multiple spaces are unnecessary and can cause inconsistent formatting.

Why This Rule Exists

Single space after hash is important because:

  • Maintains consistent formatting across documents
  • Follows standard markdown conventions
  • Reduces unnecessary whitespace
  • Improves readability and predictability

Examples

❌ Incorrect (violates rule)

# Heading with multiple spaces

## Another heading with extra spaces

### Too many spaces here

✅ Correct

# Heading with single space
## Another heading correctly formatted
### Proper spacing

Configuration

This rule has no configuration options.

Automatic Fix

This rule supports automatic fixing with --fix. The fix will:

  • Reduce multiple spaces to a single space after hash character(s)
  • Preserve the heading level and content
  • Handle all heading levels (1-6)

Apply Fix

# Fix multiple space issues in headings
mdbook-lint lint --fix docs/

# Preview what would be fixed
mdbook-lint lint --fix --dry-run docs/

When to Disable

Consider disabling this rule if:

  • Your style guide requires multiple spaces for alignment
  • You're maintaining legacy content with specific spacing requirements

Disable in Config

# .mdbook-lint.toml
disabled_rules = ["MD019"]

Disable Inline

<!-- mdbook-lint-disable MD019 -->
## Heading with multiple spaces allowed

<!-- mdbook-lint-enable MD019 -->
  • MD018 - No space after hash on ATX heading
  • MD020 - Missing space inside hashes on closed ATX heading
  • MD021 - Multiple spaces inside hashes on closed ATX heading
  • MD022 - Headings should be surrounded by blank lines
  • MD023 - Headings must start at the beginning of the line

References

MD020 - Missing Space Inside Hashes on Closed ATX Style Headings

Severity: Warning
Category: Headings
Auto-fix: ✓ Available

Rule Description

This rule checks heading-like closed ATX syntax for missing separators between the heading text and its opening or closing hash sequence. A closed ATX heading uses spaces or tabs inside both sets of hashes.

Per CommonMark, a trailing hash sequence is a closing delimiter only when it is preceded by a space or tab. Content-adjacent hashes in headings such as C# and F# are ordinary heading text and do not trigger this rule.

Examples

❌ Incorrect

#Heading 1#
##Heading 2 ##

✅ Correct

# Heading 1 #
## Heading 2 ##

# Open headings are also valid
### C#
### F#

### Heading# is an open ATX heading whose text ends in #. MD020 does not guess that the final hash was intended as a closing delimiter.

Configuration

This rule has no configuration options.

Automatic Fix

The fix adds missing separators where closed-heading intent is unambiguous:

##Heading##  ->  ## Heading ##
##Heading ## ->  ## Heading ##

Content-adjacent hashes are never removed or converted into delimiters.

# Apply fixes
mdbook-lint lint --fix docs/

# Preview fixes
mdbook-lint lint --fix --dry-run docs/
  • MD003 - Heading style
  • MD018 - Missing space after hashes on ATX headings
  • MD019 - Multiple spaces after hashes on ATX headings
  • MD021 - Multiple spaces inside closed ATX headings

References

MD021 - Multiple Spaces Inside Hashes on Closed ATX Style Heading

Severity: Warning
Category: Headings
Auto-fix: ✓ Available

Rule Description

This rule ensures closed ATX-style headings have only a single space inside the hash markers. Multiple spaces create inconsistent formatting and unnecessary whitespace.

Why This Rule Exists

Single space inside closed headings is important because:

  • Maintains consistent formatting
  • Follows standard markdown conventions
  • Improves readability
  • Ensures proper rendering across different parsers

Examples

❌ Incorrect (violates rule)

#Heading with multiple spaces#


##Another heading##


###Too many internal spaces###


✅ Correct

#Heading with single spaces#


##Another heading##


###Properly spaced heading###


# Open heading is also fine

Configuration

This rule has no configuration options.

Automatic Fix

This rule supports automatic fixing with --fix. The fix will:

  • Reduce multiple spaces to single spaces inside hash markers
  • Preserve the heading level and content
  • Maintain the closed heading style

Apply Fix

# Fix multiple spaces in closed headings
mdbook-lint lint --fix docs/

# Preview what would be fixed
mdbook-lint lint --fix --dry-run docs/

When to Disable

Consider disabling this rule if:

  • Your style guide requires multiple spaces for alignment
  • You prefer open ATX headings (without closing hashes)

Disable in Config

# .mdbook-lint.toml
disabled_rules = ["MD021"]

Disable Inline

<!-- mdbook-lint-disable MD021 -->
##Heading with multiple spaces##


<!-- mdbook-lint-enable MD021 -->
  • MD018 - No space after hash on ATX heading
  • MD019 - Multiple spaces after hash on ATX heading
  • MD020 - Missing space inside hashes on closed ATX heading
  • MD022 - Headings should be surrounded by blank lines
  • MD003 - Heading style

References

MD022 - Blanks Around Headings

Headings should be surrounded by blank lines.

Why This Rule Exists

Blank lines around headings improve readability and ensure consistent rendering across Markdown parsers. Some parsers require blank lines to properly recognize headings.

Examples

Incorrect

Some paragraph text.
## Heading
More text here.

Correct

Some paragraph text.

## Heading

More text here.

Configuration

[MD022]
lines_above = 1  # Blank lines before heading (default: 1)
lines_below = 1  # Blank lines after heading (default: 1)

When to Disable

  • Documents with compact formatting requirements
  • Content where headings immediately follow other headings

Rule Details

  • Rule ID: MD022
  • Aliases: blanks-around-headings
  • Category: Structure
  • Severity: Warning
  • Auto-fix: Yes
  • MD023 - Headings start at beginning of line
  • MD031 - Blanks around fenced code blocks
  • MD032 - Blanks around lists

MD023 - Headings Must Start at the Beginning of the Line

Severity: Warning
Category: Headings
Auto-fix: ✓ Available

Rule Description

This rule ensures headings start at the beginning of the line without any leading spaces or tabs. Indented headings are not valid in standard markdown.

Why This Rule Exists

Headings at line start are important because:

  • Indented text with hashes may be interpreted as code or regular text
  • Ensures headings are properly recognized by all parsers
  • Maintains consistent document structure
  • Follows CommonMark specification

Examples

❌ Incorrect (violates rule)

  # Indented heading
    ## Another indented heading
    ### Tab-indented heading

✅ Correct

# Heading at line start
## Another proper heading
### Correctly positioned heading

Configuration

This rule has no configuration options.

Automatic Fix

This rule supports automatic fixing with --fix. The fix will:

  • Remove all leading whitespace (spaces and tabs) before headings
  • Preserve the heading level and content
  • Maintain proper heading structure

Apply Fix

# Fix indented headings
mdbook-lint lint --fix docs/

# Preview what would be fixed
mdbook-lint lint --fix --dry-run docs/

When to Disable

Consider disabling this rule if:

  • You're documenting markdown syntax and showing indented hash examples
  • Your content includes code blocks with hash-prefixed comments

Disable in Config

# .mdbook-lint.toml
disabled_rules = ["MD023"]

Disable Inline

<!-- mdbook-lint-disable MD023 -->
    # This indented text is intentional
<!-- mdbook-lint-enable MD023 -->
  • MD018 - No space after hash on ATX heading
  • MD019 - Multiple spaces after hash on ATX heading
  • MD022 - Headings should be surrounded by blank lines
  • MD025 - Multiple top-level headings in the same document
  • MD026 - Trailing punctuation in heading

References

MD024 - No Duplicate Headings

Multiple headings with the same content.

Why This Rule Exists

Duplicate headings can confuse readers and break anchor links. Each heading generates a URL fragment, and duplicates create ambiguous navigation targets.

Examples

Incorrect

# Guide

## Introduction

Some content.

## Introduction

More content with same heading.

Correct

# Guide

## Introduction

Some content.

## Getting Started

Different heading for different section.

Siblings Only Mode

With siblings_only: true, duplicates are allowed in different sections:

# Chapter 1

## Summary

Chapter 1 summary.

# Chapter 2

## Summary

Chapter 2 summary (allowed - different parent).

Configuration

[MD024]
siblings_only = false  # Only check sibling headings (default: false)
allow_different_nesting = false  # Allow same text at different levels

When to Disable

  • Documents with intentionally repeated section names
  • Auto-generated content with structured repetition

Rule Details

  • Rule ID: MD024
  • Aliases: no-duplicate-heading
  • Category: Content
  • Severity: Warning
  • Auto-fix: No
  • MD025 - Single top-level heading
  • MD051 - Link fragments validation

MD025 - Single Top-Level Heading

Multiple top-level headings in the same document.

Why This Rule Exists

A document should have a single H1 heading that serves as its title. Multiple H1 headings suggest the content should be split into separate documents or the heading hierarchy needs adjustment.

Examples

Incorrect

# First Title

Content here.

# Second Title

More content.

Correct

# Document Title

## First Section

Content here.

## Second Section

More content.

Configuration

[MD025]
level = 1        # Heading level to check (default: 1)
front_matter_title = ""  # Regex for front matter title

When to Disable

  • SUMMARY.md files in mdBook (use MDBOOK025 instead)
  • Documents intentionally containing multiple articles
  • Changelog files with version headings as H1

Rule Details

  • Rule ID: MD025
  • Aliases: single-title, single-h1
  • Category: Structure
  • Severity: Warning
  • Auto-fix: No

mdBook Integration

For SUMMARY.md files, this rule is automatically relaxed. Use MDBOOK025 which understands mdBook's multi-section SUMMARY format.

  • MD001 - Heading increment
  • MD002 - First heading H1
  • MD041 - First line top-level heading
  • MDBOOK025 - SUMMARY.md heading structure

MD026 - No Trailing Punctuation

Trailing punctuation in headings.

Why This Rule Exists

Headings typically don't end with punctuation like periods or commas. Trailing punctuation can look awkward in tables of contents and navigation menus.

Examples

Incorrect

# Welcome to the Guide.

## Getting Started:

### What is Markdown?

Correct

# Welcome to the Guide

## Getting Started

### What is Markdown

Questions (Configurable)

## Frequently Asked Questions

### How do I install it?

Configuration

[MD026]
punctuation = ".,;:!?"  # Characters to flag (default: ".,;:!")

The ? is excluded by default to allow question headings in FAQ sections.

When to Disable

  • Documents with headings that are complete sentences
  • Stylistic choice to include punctuation
  • FAQ sections with question marks (or adjust punctuation config)

Rule Details

  • Rule ID: MD026
  • Aliases: no-trailing-punctuation
  • Category: Formatting
  • Severity: Warning
  • Auto-fix: Yes (removes trailing punctuation)
  • MD018 - Space after hash
  • MD021 - Spaces in closed ATX headings

MD041 - First Line Top-Level Heading

First line in a file should be a top-level heading.

Why This Rule Exists

Documents should start with a title heading to establish context. This helps with document navigation, accessibility, and table of contents generation.

Examples

Incorrect

Some introductory text before the heading.

# Document Title

Content here.

Correct

# Document Title

Some introductory text.

Content here.

With Front Matter

---
title: My Document
---

# Document Title

Content here.

Configuration

[MD041]
level = 1              # Expected heading level (default: 1)
front_matter_title = "^\\s*title\\s*[:=]"  # Regex for front matter title

If front_matter_title matches, the document is considered to have a title and the rule passes.

When to Disable

  • Fragment documents included in larger documents
  • Files with front matter providing the title
  • Auto-generated content with different structure

Rule Details

  • Rule ID: MD041
  • Aliases: first-line-heading, first-line-h1
  • Category: Structure
  • Severity: Warning
  • Auto-fix: No

Replaces MD002

This rule supersedes MD002 with improved handling of front matter and more configuration options.

  • MD001 - Heading increment
  • MD002 - First heading H1 (deprecated)
  • MD025 - Single top-level heading

List Rules

List rules ensure consistent formatting, indentation, and structure for both ordered and unordered lists.

Rules in This Category

RuleDescriptionFix
MD004Unordered list style (consistent markers)
MD005Inconsistent indentation for list items at the same level
MD006Consider starting lists at the beginning of the line
MD007Unordered list indentation
MD029Ordered list item prefix
MD030Spaces after list markers
MD031Fenced code blocks should be surrounded by blank lines
MD032Lists should be surrounded by blank lines

List Basics

Unordered Lists

Markdown supports three markers for unordered lists:

* Item with asterisk
- Item with dash
+ Item with plus

All render the same, but consistency is important.

Ordered Lists

1. First item
2. Second item
3. Third item

Or with lazy numbering:

1. First item
1. Second item
1. Third item

Best Practices

Consistent Markers

Pick one unordered list marker and stick with it:

Good:

* First item
* Second item
  * Nested item
  * Another nested
* Third item

Bad:

* First item
- Second item
  + Nested item
  * Another nested
+ Third item

Proper Indentation

Use consistent indentation for nested lists:

2-space indentation:

* Parent item
  * Child item
    * Grandchild item
  * Another child
* Another parent

4-space indentation:

* Parent item
    * Child item
        * Grandchild item
    * Another child
* Another parent

Spacing After Markers

Maintain consistent spacing after list markers:

Good (single space):

* Item one
* Item two
1. First item
2. Second item

Bad (inconsistent):

*Item one
*  Item two
1.First item
2.  Second item

Complex List Structures

Multi-line List Items

For list items with multiple paragraphs:

1. First item with multiple paragraphs.

   This is still part of the first item. Note the blank line above
   and the indentation.

2. Second item.

   * Nested list in second item
   * Another nested item

3. Third item.

Lists with Code Blocks

Proper indentation for code blocks in lists:

1. Install the package:

   ```bash
   npm install mdbook-lint
  1. Configure the linter:

    {
      "rules": {
        "MD009": true
      }
    }
    
  2. Run the linter.


### Task Lists

GitHub Flavored Markdown task lists:

```markdown
- [x] Completed task
- [ ] Incomplete task
- [ ] Another todo
  - [x] Completed subtask
  - [ ] Incomplete subtask

Common Issues and Solutions

Issue: Inconsistent List Indentation

Problem:

* Item 1
  * Nested with 2 spaces
    * Deep nested with 4 spaces
 * Wrong indentation
   * More inconsistency

Solution:

* Item 1
  * Nested with 2 spaces
  * Consistent 2-space indent
  * All items aligned
    * Deeper nesting maintains pattern

Issue: Missing Blank Lines Around Lists

Problem:

Some paragraph text
* List starts immediately
* No separation
Paragraph continues here

Solution:

Some paragraph text

* List has blank line before
* Proper separation

Paragraph has blank line after list

Issue: Lazy Numbering Problems

Problem with lazy numbering:

1. First item
1. Second item
5. Oops, wrong number
1. Fourth item

Solution 1 (sequential):

1. First item
2. Second item
3. Third item
4. Fourth item

Solution 2 (all ones):

1. First item
1. Second item
1. Third item
1. Fourth item

Accessibility Considerations

Proper list formatting improves accessibility:

  1. Screen Readers: Announce list structure and item count
  2. Navigation: Users can skip between lists
  3. Context: Proper nesting conveys relationships
  4. Semantics: Lists convey meaning beyond visual formatting

mdBook-Specific Considerations

In mdBook projects:

  1. Table of Contents: Lists in SUMMARY.md define book structure
  2. Navigation: Nested lists create hierarchical navigation
  3. Rendering: List formatting affects HTML output
  4. Search: List items are indexed for search

Configuration Examples

Enforce Consistent Unordered List Style

[MD004]
style = "asterisk"  # or "dash" or "plus"

Set List Indentation

[MD007]
indent = 2  # or 4, or any consistent value

Configure Ordered List Style

[MD029]
style = "ordered"  # or "one" for all 1s

Spaces After List Markers

[MD030]
ul_single = 1  # Spaces after unordered list marker
ol_single = 1  # Spaces after ordered list marker
ul_multi = 1   # Spaces after marker for multi-line items
ol_multi = 1   # Spaces after marker for multi-line items
  • MD013 - Line length (affects long list items)
  • MD022 - Blank lines (around list blocks)
  • MD031 - Code blocks in lists
  • MD032 - Blank lines around lists

References

MD004 - Unordered List Style

Unordered list style should be consistent.

Why This Rule Exists

Markdown supports three markers for unordered lists: -, *, and +. Using different markers inconsistently creates visual noise and can indicate accidental mixing of content from different sources.

Examples

Incorrect

- Item one
* Item two
+ Item three

Correct

- Item one
- Item two
- Item three

Or consistently using asterisks:

* Item one
* Item two
* Item three

Configuration

[MD004]
style = "dash"  # Options: "dash", "asterisk", "plus", "consistent"
ValueMarkerExample
dash-- Item
asterisk** Item
plus++ Item
consistentFirst usedMatches first list marker

When to Disable

  • Documents intentionally using different markers to distinguish list types
  • Importing content from multiple sources

Rule Details

  • Rule ID: MD004
  • Aliases: ul-style
  • Category: Formatting
  • Severity: Warning
  • Auto-fix: Yes
  • MD005 - List item indentation
  • MD006 - Lists start at beginning of line
  • MD007 - Unordered list indentation
  • MD029 - Ordered list prefix style

MD005 - List Item Indentation

List item indentation should be consistent within a list.

Why This Rule Exists

Inconsistent indentation within lists can cause rendering issues and makes documents harder to read in source form. Proper indentation also ensures nested lists render correctly.

Examples

Incorrect

- Item one
 - Item two (wrong indentation)
- Item three

Correct

- Item one
- Item two
- Item three

Nested Lists (Correct)

- Item one
  - Nested item
  - Another nested
- Item two

Configuration

This rule has no configuration options. It enforces consistent indentation within each list.

When to Disable

  • Working with auto-generated content that has intentional spacing
  • Documents with complex nested structures requiring manual control

Rule Details

  • Rule ID: MD005
  • Aliases: list-indent
  • Category: Formatting
  • Severity: Warning
  • Auto-fix: Yes
  • MD004 - Unordered list style
  • MD006 - Lists start at beginning of line
  • MD007 - Unordered list indentation depth
  • MD030 - Spaces after list markers

MD006 - List Start Left

Consider starting bulleted lists at the beginning of the line.

Why This Rule Exists

Lists that don't start at the beginning of the line can cause unexpected rendering behavior in some Markdown parsers. Starting lists at column 0 ensures consistent rendering across all platforms.

Examples

Incorrect

Some text:
  - Indented list item
  - Another indented item

Correct

Some text:

- List item at start of line
- Another item at start of line

Nested Lists (Allowed)

- Top level item
  - Nested item (indentation is fine for nesting)
  - Another nested item

Configuration

This rule has no configuration options.

When to Disable

  • Documents with intentionally indented lists for specific formatting
  • Content that uses indentation for semantic meaning

Rule Details

  • Rule ID: MD006
  • Aliases: ul-start-left
  • Category: Formatting
  • Severity: Warning
  • Auto-fix: Yes
  • MD004 - Unordered list style
  • MD005 - List item indentation consistency
  • MD007 - Unordered list indentation
  • MD023 - Headings start at beginning of line

MD007 - Unordered List Indentation

Unordered list indentation should use consistent spacing.

Why This Rule Exists

Proper indentation of nested lists ensures correct rendering and improves readability. Different Markdown parsers may interpret inconsistent indentation differently.

Examples

Incorrect (4-space indent when 2 expected)

- Item one
    - Nested too far
- Item two

Correct (2-space indent)

- Item one
  - Properly nested
  - Another nested item
- Item two

Correct (4-space indent with configuration)

- Item one
    - Nested with 4 spaces
    - Another nested item
- Item two

Configuration

[MD007]
indent = 2           # Spaces per indentation level (default: 2)
start_indented = false  # Allow first level to be indented
OptionDefaultDescription
indent2Number of spaces per nesting level
start_indentedfalseAllow top-level items to be indented

When to Disable

  • Documents following a different indentation standard
  • Content imported from tools using different conventions

Rule Details

  • Rule ID: MD007
  • Aliases: ul-indent
  • Category: Formatting
  • Severity: Warning
  • Auto-fix: Yes
  • MD004 - Unordered list style
  • MD005 - List item indentation consistency
  • MD006 - Lists start at beginning of line
  • MD029 - Ordered list prefix style

MD029 - Ordered List Prefix

Ordered list item prefix consistency.

Why This Rule Exists

Markdown supports different numbering styles for ordered lists. Consistent style improves readability and makes reordering items easier.

Styles

One-Based (All 1s)

1. First item
1. Second item
1. Third item

Advantage: Easy to reorder without renumbering.

Sequential (Ordered)

1. First item
2. Second item
3. Third item

Advantage: Source reflects rendered numbers.

Zero-Based

0. First item
0. Second item
0. Third item

Examples

Incorrect (Mixed)

1. First item
2. Second item
1. Third item

Correct

1. First item
2. Second item
3. Third item

Configuration

[MD029]
style = "one_or_ordered"  # Options: "one", "ordered", "zero", "one_or_ordered"
ValueDescription
oneAll items use 1.
orderedSequential numbering (1, 2, 3...)
zeroAll items use 0.
one_or_orderedAllow either 1. or sequential

When to Disable

  • Documents with intentional mixed numbering
  • Content using numbers for reference purposes

Rule Details

  • Rule ID: MD029
  • Aliases: ol-prefix
  • Category: Formatting
  • Severity: Warning
  • Auto-fix: Yes
  • MD004 - Unordered list style
  • MD030 - Spaces after list markers

MD030 - Spaces After List Markers

Severity: Warning
Category: Lists
Auto-fix: ✓ Available

Rule Description

This rule ensures consistent spacing after list markers (*, -, +, or numbers for ordered lists). Proper spacing improves readability and ensures correct parsing.

Why This Rule Exists

Consistent list marker spacing is important because:

  • Ensures lists are properly recognized by all parsers
  • Maintains uniform formatting across documents
  • Improves readability and visual structure
  • Some parsers require specific spacing for proper rendering

Examples

❌ Incorrect (violates rule)

*No space after asterisk
-No space after dash
+No space after plus
1.No space after number

*   Too many spaces
-    Excessive spacing
1.    Too much space in ordered list

✅ Correct

* Single space after asterisk
- Single space after dash
+ Single space after plus
1. Single space after number

* Consistent spacing
  * Nested items also follow rules
    * Multi-level nesting works

Configuration

[MD030]
ul_single = 1    # Spaces after single-line unordered list marker (default: 1)
ul_multi = 1     # Spaces after multi-line unordered list marker (default: 1)
ol_single = 1    # Spaces after single-line ordered list marker (default: 1)
ol_multi = 1     # Spaces after multi-line ordered list marker (default: 1)

Automatic Fix

This rule supports automatic fixing with --fix. The fix will:

  • Adjust spacing after list markers to match configuration
  • Handle both ordered and unordered lists
  • Preserve list content and nesting
  • Maintain proper indentation for nested lists

Apply Fix

# Fix list marker spacing
mdbook-lint lint --fix docs/

# Preview what would be fixed
mdbook-lint lint --fix --dry-run docs/

When to Disable

Consider disabling this rule if:

  • Your markdown processor has different spacing requirements
  • You're working with generated content with specific formatting
  • Your style guide requires different spacing patterns

Disable in Config

# .mdbook-lint.toml
disabled_rules = ["MD030"]

Disable Inline

<!-- mdbook-lint-disable MD030 -->
*No space after marker allowed here
<!-- mdbook-lint-enable MD030 -->
  • MD004 - Unordered list style
  • MD005 - Consistent list indentation
  • MD006 - Consider starting lists at the beginning of the line
  • MD007 - Unordered list indentation
  • MD029 - Ordered list item prefix
  • MD032 - Lists surrounded by blank lines

References

MD032 - Blanks Around Lists

Lists should be surrounded by blank lines.

Why This Rule Exists

Blank lines around lists ensure proper parsing and improve readability. Without blank lines, some Markdown parsers may not correctly identify list boundaries.

Examples

Incorrect

Some introductory text.
- Item one
- Item two
Following paragraph.

Correct

Some introductory text.

- Item one
- Item two

Following paragraph.

Configuration

This rule has no configuration options.

When to Disable

  • Documents with compact formatting requirements
  • Content with tight list-to-text flow

Rule Details

  • Rule ID: MD032
  • Aliases: blanks-around-lists
  • Category: Structure
  • Severity: Warning
  • Auto-fix: Yes
  • MD022 - Blanks around headings
  • MD031 - Blanks around fenced code blocks
  • MD058 - Blanks around tables

Whitespace Rules

These rules ensure consistent whitespace usage throughout your markdown documents.

Rules in This Category

Auto-fix Available ✓

  • MD009 - No trailing spaces
  • MD010 - Hard tabs
  • MD012 - Multiple consecutive blank lines
  • MD027 - Multiple spaces after blockquote symbol
  • MD047 - Files should end with a single newline

Why Whitespace Matters

Consistent whitespace usage:

  • Improves readability and maintainability
  • Prevents version control issues (unnecessary diffs)
  • Ensures consistent rendering across different viewers
  • Follows standard text file conventions
  • Reduces file size

Quick Configuration

# .mdbook-lint.toml

# Configure MD009 - Trailing spaces
[MD009]
br_spaces = 2  # Allow 2 spaces for line breaks

# Configure MD010 - Hard tabs
[MD010]
spaces_per_tab = 4  # Convert tabs to 4 spaces

# Configure MD012 - Multiple blank lines
[MD012]
maximum = 1  # Allow max 1 consecutive blank line

# Configure MD027 - Blockquote spacing
[MD027]
spaces = 1  # Require 1 space after >

Disable All Whitespace Rules

# .mdbook-lint.toml
disabled_rules = ["MD009", "MD010", "MD012", "MD027", "MD047"]

MD009 - No Trailing Spaces

This rule checks for trailing spaces at the end of lines.

Why This Rule Exists

Trailing spaces are usually unintentional and can cause issues:

  • They're invisible in most editors, making them hard to spot
  • They can cause unexpected behavior in version control systems
  • They may render differently across different markdown processors
  • They increase file size unnecessarily

Examples

❌ Incorrect (violates rule)

This line has trailing spaces   ← spaces
This one has a tab at the end	← tab
Multiple spaces here    ← spaces

(Where arrows indicate invisible whitespace characters)

✅ Correct

This line has no trailing spaces
This one is clean too
Two spaces for line break are allowed  
when configured (br_spaces = 2)

Configuration

[MD009]
br_spaces = 2  # Number of trailing spaces allowed for line breaks (default: 2)
strict = false # If true, disallow even configured line break spaces (default: false)

Automatic Fix

This rule supports automatic fixing. The fix will:

  • Remove all trailing whitespace from lines
  • Preserve configured line break spaces (typically 2 spaces)
  • Maintain the line's content and structure

When to Disable

Consider disabling this rule if:

  • Your project intentionally uses trailing spaces for formatting

Rule Details

  • Rule ID: MD009
  • Aliases: no-trailing-spaces
  • Category: Whitespace
  • Severity: Warning
  • Automatic Fix: ✅ Available

Configuration Options

[MD009]
br_spaces = 2  # Number of spaces allowed for line breaks (default: 2)
strict = false # If true, disallow even configured line break spaces (default: false)

Understanding Line Breaks

Markdown supports two types of line breaks:

Hard Line Break (Two Spaces)

First line  
Second line on new line

Renders as:

First line
Second line on new line

Paragraph Break (Blank Line)

First paragraph

Second paragraph

Renders as:

First paragraph

Second paragraph

Common Issues and Solutions

Invisible Trailing Spaces

Problem: Spaces at line ends are invisible in most editors.

This line has spaces    ← invisible spaces
Another line with tab	← invisible tab

Solution: Configure your editor to show whitespace or use the automatic fix.

This line has no trailing spaces
Another clean line

Inconsistent Line Break Handling

Problem: Different markdown processors handle trailing spaces differently.

Some text   
More text    
Final text     

Solution: Use exactly two spaces for line breaks when needed.

Some text  
More text  
Final text

Editor Configuration

Visual Studio Code

Add to settings.json:

{
  "files.trimTrailingWhitespace": true,
  "markdown.preview.breaks": true,
  "[markdown]": {
    "files.trimTrailingWhitespace": false
  }
}

Vim

Add to .vimrc:

" Show trailing spaces
set list listchars=trail:·

" Remove trailing spaces on save
autocmd BufWritePre * %s/\s\+$//e

Sublime Text

Add to preferences:

{
  "trim_trailing_white_space_on_save": true,
  "draw_white_space": ["all"]
}

Version Control Considerations

Git Configuration

Configure Git to warn about trailing whitespace:

git config core.whitespace trailing-space

Pre-commit Hooks

Use a pre-commit hook to catch trailing spaces:

#!/bin/sh
# .git/hooks/pre-commit
exec git diff --check --cached

When Line Break Spaces Are Needed

Sometimes two trailing spaces are intentional:

Poetry and Verses

Roses are red  
Violets are blue  
Markdown is great  
And so are you  

Addresses

123 Main Street  
Suite 100  
Anytown, ST 12345  

Preserving Formatting

Name:     John Doe  
Email:    john@example.com  
Phone:    555-1234  

Performance Impact

Trailing spaces can impact:

  1. File Size: Unnecessary whitespace increases file size
  2. Diff Noise: Changes to trailing spaces clutter version control
  3. Search/Replace: Invisible characters can break patterns
  4. Copy/Paste: Trailing spaces may cause unexpected behavior

Automatic Fix Behavior

The automatic fix will:

  1. Remove all trailing whitespace from each line
  2. Preserve exactly br_spaces spaces when configured (default: 2)
  3. Handle tabs and mixed whitespace
  4. Maintain line endings (LF/CRLF)

Fix Examples

Before:

Text with spaces    
Text with tab    
Text with mixed       

After (default config):

Text with spaces
Text with tab
Text with mixed

After (br_spaces=2, preserving line breaks):

Paragraph text
Line break needed  
Next line
  • MD010 - Hard tabs
  • MD012 - Multiple consecutive blank lines
  • MD047 - Files should end with a single newline

References

MD010 - Hard Tabs

Severity: Warning
Category: Whitespace
Auto-fix: ✓ Available

Rule Description

This rule checks for hard tab characters in the document and suggests replacing them with spaces for consistency.

Why This Rule Exists

Hard tabs can cause formatting inconsistencies:

  • Tab width varies between editors (2, 4, or 8 spaces)
  • Mixing tabs and spaces leads to misaligned text
  • Different markdown renderers may handle tabs differently
  • Code blocks and indentation become unpredictable

Examples

❌ Incorrect (violates rule)

→   This line starts with a tab
-→  List item with tab after marker
```→   Code block with tab indent

(Where → represents a tab character)

✅ Correct

    This line uses spaces for indentation
- List item with spaces after marker

```    Code block with space indent

Configuration

[MD010]
code_blocks = true  # Check for tabs in code blocks (default: true)
spaces_per_tab = 4  # Number of spaces to replace each tab with (default: 4)

Automatic Fix

This rule supports automatic fixing with --fix. The fix will:

  • Replace each tab character with the configured number of spaces
  • Preserve the visual indentation of your content
  • Handle tabs in all contexts (text, lists, code blocks)

Apply Fix

# Fix all tab issues in your markdown files
mdbook-lint lint --fix docs/

# Preview what would be fixed
mdbook-lint lint --fix --dry-run docs/

When to Disable

Consider disabling this rule if:

  • Your project requires hard tabs (e.g., Makefiles in code examples)
  • You're working with legacy content that uses tabs consistently
  • Your team has standardized on tabs instead of spaces

Disable in Config

# .mdbook-lint.toml
disabled_rules = ["MD010"]

Disable Inline

<!-- mdbook-lint-disable MD010 -->
→   Content with tabs allowed here
<!-- mdbook-lint-enable MD010 -->
  • MD009 - No trailing spaces
  • MD012 - Multiple consecutive blank lines
  • MD047 - Files should end with newline

References

MD012 - Multiple Consecutive Blank Lines

Severity: Warning
Category: Whitespace
Auto-fix: ✓ Available

Rule Description

This rule checks for multiple consecutive blank lines in the document. Excessive blank lines add unnecessary whitespace without improving readability.

Why This Rule Exists

Multiple consecutive blank lines create issues:

  • Inconsistent spacing throughout documents
  • Unnecessary vertical space in rendered output
  • Potential confusion about section boundaries
  • Increased file size without benefit

Examples

❌ Incorrect (violates rule)

# Heading

First paragraph.

Second paragraph with too many blank lines above.

Third paragraph with even more blank lines.

✅ Correct

# Heading

First paragraph.

Second paragraph with single blank line.

Third paragraph properly spaced.

Configuration

[MD012]
maximum = 1  # Maximum consecutive blank lines allowed (default: 1)

Automatic Fix

This rule supports automatic fixing with --fix. The fix will:

  • Reduce multiple consecutive blank lines to the configured maximum
  • Preserve intentional spacing at the maximum level
  • Handle blank lines anywhere in the document

Apply Fix

# Fix excessive blank lines
mdbook-lint lint --fix docs/

# Preview what would be fixed
mdbook-lint lint --fix --dry-run docs/

When to Disable

Consider disabling this rule if:

  • Your style guide requires multiple blank lines for visual separation
  • You're working with generated content that uses specific spacing
  • You need extra spacing for ASCII art or diagrams

Disable in Config

# .mdbook-lint.toml
disabled_rules = ["MD012"]

Disable Inline

<!-- mdbook-lint-disable MD012 -->
Content with multiple

blank lines allowed here
<!-- mdbook-lint-enable MD012 -->
  • MD009 - No trailing spaces
  • MD010 - Hard tabs
  • MD047 - Files should end with newline

References

MD027 - Multiple Spaces After Blockquote Symbol

Severity: Warning
Category: Blockquotes
Auto-fix: ✓ Available

Rule Description

This rule ensures there's only a single space (or no space) after the blockquote marker (>). Multiple spaces create inconsistent formatting.

Why This Rule Exists

Consistent blockquote spacing is important because:

  • Maintains uniform appearance across documents
  • Reduces unnecessary whitespace
  • Follows standard markdown conventions
  • Improves readability and predictability

Examples

❌ Incorrect (violates rule)

> Multiple spaces after blockquote marker
>
>

> Even more spaces here
>
>

> Too much spacing

✅ Correct

> Single space after marker
> Consistent spacing throughout
> Clean and readable
>
>
>

>No space is also valid
>When configured appropriately

Configuration

[MD027]
spaces = 1  # Number of spaces after blockquote marker (default: 1, can be 0)

Automatic Fix

This rule supports automatic fixing with --fix. The fix will:

  • Adjust spaces after blockquote markers to match configuration
  • Preserve blockquote content and nesting
  • Handle multi-level blockquotes correctly

Apply Fix

# Fix blockquote spacing
mdbook-lint lint --fix docs/

# Preview what would be fixed
mdbook-lint lint --fix --dry-run docs/

When to Disable

Consider disabling this rule if:

  • Your style guide has different blockquote spacing requirements
  • You're preserving legacy content with specific formatting
  • You need variable spacing for visual emphasis

Disable in Config

# .mdbook-lint.toml
disabled_rules = ["MD027"]

Disable Inline

<!-- mdbook-lint-disable MD027 -->
> Blockquote with custom spacing

<!-- mdbook-lint-enable MD027 -->
  • MD028 - Blank line inside blockquote
  • MD032 - Lists surrounded by blank lines

References

MD028 - No Blanks in Blockquote

Blank line inside blockquote.

Why This Rule Exists

A blank line inside a blockquote ends the quote in most Markdown parsers. This can cause unexpected rendering where content intended to be quoted appears as regular text.

Examples

Incorrect

> First paragraph of quote.
>

> Second paragraph (this is a new blockquote).

Correct

> First paragraph of quote.
>
> Second paragraph (still in same blockquote).

Multiple Paragraphs

> This is a long quote that spans
> multiple paragraphs.
>
> The blank line has a `>` marker
> to continue the blockquote.

Configuration

This rule has no configuration options.

When to Disable

  • Documents intentionally using separate blockquotes
  • Content where visual separation is desired

Rule Details

  • Rule ID: MD028
  • Aliases: no-blanks-blockquote
  • Category: Formatting
  • Severity: Warning
  • Auto-fix: Yes (adds > to blank lines)
  • MD027 - Multiple spaces after blockquote symbol

MD047 - Files Should End with a Single Newline Character

Severity: Warning
Category: Whitespace
Auto-fix: ✓ Available

Rule Description

This rule ensures files end with exactly one newline character. This is a POSIX standard and helps with version control systems.

Why This Rule Exists

Files ending with a newline are important because:

  • POSIX standard requires text files to end with a newline
  • Git and other VCS show "No newline at end of file" warnings
  • Prevents issues when concatenating files
  • Ensures consistent file formatting
  • Some tools expect the trailing newline

Examples

❌ Incorrect (violates rule)

# Document

Last line without newline```

Or with multiple newlines:

```markdown
# Document

Last line with multiple newlines

✅ Correct

# Document

Last line with single newline

Configuration

This rule has no configuration options.

Automatic Fix

This rule supports automatic fixing with --fix. The fix will:

  • Add a newline if the file doesn't end with one
  • Remove extra newlines if there are multiple
  • Ensure exactly one newline at the end of the file

Apply Fix

# Fix file endings
mdbook-lint lint --fix docs/

# Preview what would be fixed
mdbook-lint lint --fix --dry-run docs/

When to Disable

Consider disabling this rule if:

  • You're working with files that intentionally lack final newlines
  • Your toolchain doesn't support files with trailing newlines
  • You're dealing with generated content that doesn't include newlines

Disable in Config

# .mdbook-lint.toml
disabled_rules = ["MD047"]

Disable for a Specific File

Add an inline directive at the top of the file you want to exclude:

<!-- mdbook-lint-disable MD047 -->
  • MD009 - No trailing spaces
  • MD010 - Hard tabs
  • MD012 - Multiple consecutive blank lines

References

Link Rules

These rules ensure proper link formatting and validation in markdown documents.

Rules in This Category

Auto-fix Available ✓

  • MD011 - Reversed link syntax
  • MD039 - Spaces inside link text
  • MD042 - No empty links
  • MD051 - Link fragments are valid
  • MD052 - Reference links and images should use a label
  • MD053 - Link and image reference definitions should be needed
  • MD054 - Link and image style
  • MD059 - Link and image reference style

Proper link formatting:

  • Ensures links are clickable in all renderers
  • Improves accessibility with descriptive text
  • Maintains consistent link style
  • Prevents broken references
  • Enhances document navigation
[Link text](https://example.com)
[Relative link](./other-page.md)
[Anchor link](#section-heading)
[Link text][reference]
[Another link][1]

[reference]: https://example.com
[1]: ./other-page.md
<https://example.com>
<user@example.com>

Quick Configuration

# .mdbook-lint.toml

# No configuration for MD034 - it auto-fixes bare URLs

# Disable specific link rules
disabled_rules = ["MD051", "MD052"]

Best Practices

  1. Use descriptive link text: Avoid "click here" or "link"
  2. Prefer relative paths: For internal documentation links
  3. Check anchors: Ensure heading anchors exist
  4. Use reference style: For frequently used URLs
  5. Wrap bare URLs: Use <URL> syntax or proper links

MD011 - Reversed Link Syntax

Reversed link syntax should be corrected.

Why This Rule Exists

A common typo when writing Markdown links is reversing the bracket order, writing ](url)[text instead of [text](url). This rule catches these mistakes before they break your rendered documentation.

Examples

Incorrect

Check out ](https://example.com)[this link for more info.

See ](./other-page.md)[the other page.

Correct

Check out [this link](https://example.com) for more info.

See [the other page](./other-page.md).

Configuration

This rule has no configuration options.

When to Disable

  • Generally should not be disabled as reversed links never render correctly

Rule Details

  • Rule ID: MD011
  • Aliases: no-reversed-links
  • Category: Content
  • Severity: Error
  • Auto-fix: Yes (swaps to correct order)

MD034 - Bare URL Used

Severity: Warning
Category: Links
Auto-fix: ✓ Available

Rule Description

This rule flags bare URLs that should be enclosed in angle brackets or converted to proper markdown links. Bare URLs may not be clickable in all markdown renderers.

Why This Rule Exists

Proper URL formatting is important because:

  • Ensures URLs are clickable in all markdown renderers
  • Improves document accessibility
  • Provides consistent link formatting
  • Allows for descriptive link text

Examples

❌ Incorrect (violates rule)

Visit https://example.com for more information.

Check out http://github.com/user/repo

Documentation at www.example.org

✅ Correct

Visit <https://example.com> for more information.

Check out [this repository](http://github.com/user/repo)

Documentation at [example.org](https://www.example.org)

<!-- URLs in code blocks are ignored -->

git clone https://github.com/user/repo.git

Configuration

This rule has no configuration options.

Automatic Fix

This rule supports automatic fixing with --fix. The fix will:

  • Wrap bare URLs in angle brackets (<URL>)
  • Preserve surrounding text and formatting
  • Skip URLs in code blocks and inline code
  • Handle both HTTP and HTTPS URLs

Apply Fix

# Fix bare URLs
mdbook-lint lint --fix docs/

# Preview what would be fixed
mdbook-lint lint --fix --dry-run docs/

Manual Enhancement

After auto-fix, consider manually converting to descriptive links:

<!-- After auto-fix -->
Visit <https://example.com> for more information.

<!-- Better: manually add descriptive text -->
Visit [our website](https://example.com) for more information.

When to Disable

Consider disabling this rule if:

  • Your markdown renderer automatically links bare URLs
  • You're documenting URLs that shouldn't be clickable
  • Your content includes many URLs in plain text format

Disable in Config

# .mdbook-lint.toml
disabled_rules = ["MD034"]

Disable Inline

<!-- mdbook-lint-disable MD034 -->
Plain URL: https://example.com
<!-- mdbook-lint-enable MD034 -->
  • MD011 - Reversed link syntax
  • MD039 - Spaces inside link text
  • MD042 - No empty links

References

MD039 - Spaces Inside Link Text

Spaces inside link text brackets.

Why This Rule Exists

Leading or trailing spaces inside link text brackets create inconsistent formatting and may render with unwanted whitespace in the clickable text.

Examples

Incorrect

[ Click here ](https://example.com)

[  Documentation  ](./docs.md)

[ Link ](url)

Correct

[Click here](https://example.com)

[Documentation](./docs.md)

[Link](url)

Configuration

This rule has no configuration options.

When to Disable

  • Generally should not be disabled as spaced link text is usually unintentional

Rule Details

  • Rule ID: MD039
  • Aliases: no-space-in-links
  • Category: Links
  • Severity: Warning
  • Auto-fix: Yes (removes extra spaces)
  • MD037 - Spaces inside emphasis
  • MD038 - Spaces inside code spans
  • MD042 - Empty links
  • MD051 - Link fragments

MD042 - No Empty Links

No empty links allowed.

Why This Rule Exists

Empty links with no URL serve no purpose and indicate incomplete content or a mistake during editing. They create broken user experiences when clicked.

Examples

Incorrect

Click [here]() for more information.

See the [documentation]().

[Empty link]()

Correct

Click [here](https://example.com) for more information.

See the [documentation](./docs.md).

[Valid link](https://example.com)

If you need placeholders during drafting, use comments:

<!-- TODO: Add link -->
Click [here](#) for more information.

Configuration

This rule has no configuration options.

When to Disable

  • Draft documents with intentional placeholders
  • Templates where links are filled programmatically

Rule Details

  • Rule ID: MD042
  • Aliases: no-empty-links
  • Category: Links
  • Severity: Error
  • Auto-fix: No
  • MD011 - Reversed link syntax
  • MD039 - Spaces inside link text
  • MD051 - Link fragments
  • MD052 - Reference links and images

MD051 - Link Fragments

Link fragments should be valid.

Why This Rule Exists

Fragment links (anchors) like #section-name should point to actual headings in the document. Invalid fragments create broken navigation.

Examples

Incorrect

# Introduction

See the [configuration](#config) section.

## Configuration Options

Content here.

The fragment #config doesn't match #configuration-options.

Correct

# Introduction

See the [configuration](#configuration-options) section.

## Configuration Options

Content here.

How Fragments Are Generated

Headings become fragments by:

  1. Converting to lowercase
  2. Replacing spaces with hyphens
  3. Removing special characters
HeadingFragment
## Getting Started#getting-started
## API Reference#api-reference
## What's New?#whats-new

Configuration

This rule has no configuration options.

When to Disable

  • Documents with custom anchor IDs
  • Content using JavaScript-based navigation
  • Files processed by tools that modify anchors

Rule Details

  • Rule ID: MD051
  • Aliases: link-fragments
  • Category: Links
  • Severity: Warning
  • Auto-fix: No

Performance

This rule is optimized for large documents. It builds a heading index once and validates all fragments efficiently.

  • MD024 - Duplicate headings
  • MD042 - Empty links
  • MD052 - Reference links and images
  • MDBOOK002 - Internal link validation

MD052 - Reference Links and Images

Reference links and images should use a label that is defined.

Why This Rule Exists

Reference-style links like [text][label] must have a corresponding definition. Undefined references render as plain text instead of links.

Examples

Incorrect

Check out the [documentation][docs] for more info.

Visit [our website][site].

<!-- Missing definitions for 'docs' and 'site' -->

Correct

Check out the [documentation][docs] for more info.

Visit [our website][site].

[docs]: https://docs.example.com
[site]: https://example.com

Images

![Logo][logo]

[logo]: ./images/logo.png "Company Logo"

Configuration

[MD052]
# Labels that are never reported as undefined.
# Defaults cover task list checkboxes and GitHub-style admonitions.
ignored_labels = [" ", "x", "!note", "!tip", "!important", "!warning", "!caution"]

# Whether bare [label] shortcut references are validated. Default: false.
shortcut_syntax = false

shortcut_syntax

With the default false, only full [text][label] and collapsed [text][] references are checked. Bare [label] text is left alone, so prose such as - [web] Fix the sidebar is not reported as a broken link.

Set it to true to also validate shortcut references:

[MD052]
shortcut_syntax = true

Under true, [missing] is reported when no [missing]: definition exists.

When to Disable

  • Documents where references are defined in included files
  • Templates with programmatically injected definitions

Rule Details

  • Rule ID: MD052
  • Aliases: reference-links-images
  • Category: Links
  • Severity: Warning
  • Auto-fix: No
<!-- Full reference -->
[Link text][label]

<!-- Collapsed reference (label matches text) -->
[Example][]

<!-- Shortcut reference -->
[Example]

<!-- Definition -->
[label]: url "Optional Title"
  • MD042 - Empty links
  • MD051 - Link fragments
  • MD053 - Unused reference definitions

MD053 - Link and Image Reference Definitions

Link and image reference definitions should be needed.

Why This Rule Exists

Unused reference definitions clutter documents and may indicate dead links or incomplete edits. Keeping only used definitions improves maintainability.

Examples

Incorrect

Check out the [documentation](https://docs.example.com).

[unused]: https://example.com
[also-unused]: https://other.com

The definitions are never used.

Correct

Check out the [documentation][docs].

[docs]: https://docs.example.com

Or remove unused definitions:

Check out the [documentation](https://docs.example.com).

Configuration

[MD053]
ignored_definitions = []  # Definitions to ignore (e.g., for includes)

Ignoring Definitions

[MD053]
ignored_definitions = ["//", "TODO"]

When to Disable

  • Documents with definitions used in included content
  • Templates with conditional reference usage
  • Files serving as definition libraries

Rule Details

  • Rule ID: MD053
  • Aliases: link-image-reference-definitions
  • Category: Links
  • Severity: Warning
  • Auto-fix: No
  • MD052 - Reference links must be defined
  • MD054 - Link and image style

MD054 - Link and Image Style

Link and image style should be consistent.

Why This Rule Exists

Markdown supports inline and reference-style links. Consistent style throughout a document improves readability and maintainability.

Styles

Inline

[Link text](https://example.com)
![Alt text](image.png)

Reference (Full)

[Link text][ref]
![Alt text][img]

[ref]: https://example.com
[img]: image.png

Reference (Collapsed)

[Example][]

[Example]: https://example.com

Reference (Shortcut)

[Example]

[Example]: https://example.com

Examples

Incorrect (Mixed)

See [inline link](https://example.com) and [reference link][ref].

[ref]: https://other.com

Correct

See [inline link](https://example.com) and [other link](https://other.com).

Configuration

[MD054]
autolink = true           # Allow autolinks <https://example.com>
inline = true             # Allow inline style
full = true               # Allow full reference style
collapsed = true          # Allow collapsed reference style
shortcut = true           # Allow shortcut reference style
url_inline = true         # Allow inline for URLs only

When to Disable

  • Documents with intentional style mixing
  • Content where different styles serve different purposes

Rule Details

  • Rule ID: MD054
  • Aliases: link-image-style
  • Category: Links
  • Severity: Warning
  • Auto-fix: No
  • MD052 - Reference links must be defined
  • MD053 - Unused reference definitions
  • MD059 - Descriptive link text

MD059 - Descriptive Link Text

Link text should be descriptive.

Why This Rule Exists

Generic link text like "click here" or "this link" provides no context about the destination. Descriptive text improves accessibility and helps users understand where links lead.

Examples

Incorrect

For more information, [click here](https://docs.example.com).

See [this link](./guide.md) for details.

[Here](https://api.example.com) is the API documentation.

Read more [here](./faq.md).

Correct

For more information, see the [complete documentation](https://docs.example.com).

See the [installation guide](./guide.md) for details.

Read the [API documentation](https://api.example.com).

Read the [frequently asked questions](./faq.md).

Configuration

This rule has no configuration options.

When to Disable

  • UI documentation where "click here" matches actual button text
  • Content with intentionally brief link descriptions

Rule Details

  • Rule ID: MD059
  • Aliases: descriptive-link-text
  • Category: Accessibility
  • Severity: Warning
  • Auto-fix: No

Common Non-Descriptive Phrases

The rule flags these common patterns:

  • "click here"
  • "here"
  • "this link"
  • "this"
  • "link"
  • "read more"

Why Descriptive Text Matters

  1. Screen Readers: Users often navigate by links; "click here" provides no context
  2. Scanning: Users scan pages looking for relevant links
  3. SEO: Search engines use link text to understand content relationships
  4. Mobile: Touch targets benefit from clear labels

Accessibility Standards

This rule helps comply with:

  • WCAG 2.1 Success Criterion 2.4.4 (Link Purpose in Context)
  • WCAG 2.1 Success Criterion 2.4.9 (Link Purpose Link Only)
  • MD042 - No empty links
  • MD045 - Images should have alt text

Code Rules

These rules ensure proper formatting of code blocks and inline code in markdown documents.

Rules in This Category

  • MD040 - Fenced code blocks should have a language specified
  • MD014 - Dollar signs used before commands without showing output
  • MD031 - Fenced code blocks should be surrounded by blank lines
  • MD038 - Spaces inside code span elements
  • MD046 - Code block style
  • MD048 - Code fence style

Why Code Rules Matter

Proper code formatting:

  • Enables syntax highlighting for better readability
  • Maintains consistency across code examples
  • Improves copy-paste reliability
  • Ensures proper rendering in different viewers
  • Helps readers identify programming languages quickly

Code Block Styles

```javascript
function example() {
    return "Hello, world!";
}
```

Indented Code Blocks

    function example() {
        return "Hello, world!";
    }

Inline Code

Use the `console.log()` function to debug.

Language Specifications

Common language tags for syntax highlighting:

LanguageTags
JavaScriptjs, javascript
TypeScriptts, typescript
Pythonpy, python
Rustrs, rust
Shellsh, bash, shell
JSONjson
YAMLyml, yaml

Quick Configuration

# .mdbook-lint.toml

# Configure MD040 - Require language tags
[MD040]
allowed_languages = ["js", "python", "rust", "bash"]

# Configure MD046 - Code block style
[MD046]
style = "fenced"  # Options: "fenced", "indented", "consistent"

# Configure MD048 - Code fence style
[MD048]
style = "backtick"  # Options: "backtick", "tilde", "consistent"

Best Practices

  1. Always specify language: Enables syntax highlighting
  2. Use fenced blocks: More flexible than indented blocks
  3. Surround with blank lines: Improves readability
  4. Be consistent: Use the same style throughout
  5. Escape special characters: Use backslash when needed

Shell Commands

# Good - shows command without prompt
npm install mdbook-lint

# Avoid - dollar sign without output
npm install mdbook-lint

MD014 - Dollar Signs in Commands

Dollar signs used before commands without showing output.

Why This Rule Exists

Including $ prompts in shell code blocks makes it harder for users to copy and paste commands. If the code block shows command output, the prompt helps distinguish input from output. Otherwise, it's just noise.

Examples

Incorrect

```bash
$ npm install
$ npm run build

### Correct (no prompts)

```markdown
```bash
npm install
npm run build

### Correct (showing output)

```markdown
```bash
$ echo "Hello"
Hello
$ ls
file1.txt  file2.txt

## Configuration

This rule has no configuration options.

## When to Disable

- Tutorial content where prompts help indicate user input
- Documents showing interactive shell sessions
- Content distinguishing between different shell types

## Rule Details

- **Rule ID**: MD014
- **Aliases**: no-dollar-signs, commands-show-output
- **Category**: Content
- **Severity**: Warning
- **Auto-fix**: Yes (removes `$` prefix)

## Related Rules

- [MD040](./md040.md) - Fenced code blocks should have language
- [MD046](./md046.md) - Code block style

MD031 - Blanks Around Fences

Fenced code blocks should be surrounded by blank lines.

Why This Rule Exists

Blank lines around code blocks improve readability and ensure consistent rendering. Some parsers may not correctly identify code blocks without surrounding blank lines.

Examples

Incorrect

Some text here.
```code
let x = 1;

More text here.


### Correct

```markdown
Some text here.

```code
let x = 1;

More text here.


## Configuration

```toml
[MD031]
list_items = true  # Apply rule inside list items (default: true)

When to Disable

  • Documents with compact formatting requirements
  • Content where code blocks intentionally flow with text

Rule Details

  • Rule ID: MD031
  • Aliases: blanks-around-fences
  • Category: Formatting
  • Severity: Warning
  • Auto-fix: Yes
  • MD022 - Blanks around headings
  • MD032 - Blanks around lists
  • MD040 - Fenced code blocks language
  • MD046 - Code block style

MD038 - Spaces Inside Code Spans

Spaces inside code span markers.

Why This Rule Exists

Extra spaces inside backticks create inconsistent code formatting. The spaces become part of the rendered code, which usually isn't intended.

Examples

Incorrect

Use the ` print() ` function.

Run ` npm install ` to install.

Correct

Use the `print()` function.

Run `npm install` to install.

Intentional Spaces

If you need a backtick inside code, use double backticks with spaces:

Use `` `backticks` `` for code.

Configuration

This rule has no configuration options.

When to Disable

  • Documents using spaces for specific code formatting
  • Content requiring literal spaces in code spans

Rule Details

  • Rule ID: MD038
  • Aliases: no-space-in-code
  • Category: Code
  • Severity: Warning
  • Auto-fix: Yes (removes extra spaces)
  • MD037 - Spaces inside emphasis
  • MD039 - Spaces inside link text
  • MD040 - Fenced code blocks language

MD040 - Fenced Code Blocks Should Have a Language Specified

Severity: Warning
Category: Code
Auto-fix: Not available

Rule Description

This rule ensures that fenced code blocks specify a language for syntax highlighting. Language tags improve readability and enable proper syntax highlighting in rendered output.

Why This Rule Exists

Language specifications are important because:

  • Enables syntax highlighting in rendered markdown
  • Improves code readability and comprehension
  • Helps readers quickly identify the programming language
  • Ensures consistent code block presentation
  • Required by many documentation tools (including mdBook)

Examples

❌ Incorrect (violates rule)

```
function hello() {
    console.log("Hello, world!");
}
```

```
SELECT * FROM users WHERE active = true;
```

✅ Correct

```javascript
function hello() {
    console.log("Hello, world!");
}
```

```sql
SELECT * FROM users WHERE active = true;
```

```bash
echo "Shell commands also benefit from highlighting"
```

```text
Plain text can be explicitly marked
```

Configuration

[MD040]
allowed_languages = []  # List of allowed languages (empty = all allowed)
language_optional = false  # Whether language tag is optional (default: false)

Common Language Tags

LanguageTags
JavaScriptjs, javascript
TypeScriptts, typescript
Pythonpy, python
Rustrs, rust
Shellsh, bash, shell
JSONjson
YAMLyml, yaml
Markdownmd, markdown
Plain Texttext, txt
TOMLtoml
HTMLhtml
CSScss
SQLsql

When to Disable

Consider disabling this rule if:

  • Your markdown renderer doesn't support syntax highlighting
  • You have many code blocks where language is obvious from context
  • You're using custom code block processors that don't require language tags

Disable in Config

# .mdbook-lint.toml
disabled_rules = ["MD040"]

Disable Inline

<!-- mdbook-lint-disable MD040 -->
```
Code block without language tag
```
<!-- mdbook-lint-enable MD040 -->
  • MD046 - Code block style
  • MD048 - Code fence style
  • MDBOOK001 - Code blocks should have language tags (mdBook-specific)

References

MD046 - Code Block Style

Code block style should be consistent.

Why This Rule Exists

Markdown supports both fenced code blocks (triple backticks) and indented code blocks (4 spaces). Consistent style improves readability.

Styles

```rust
fn main() {
    println!("Hello");
}
```

Indented

    fn main() {
        println!("Hello");
    }

Examples

Incorrect (Mixed)

```python
print("Hello")
```

    # Indented code block
    echo "World"

Correct (Consistent Fenced)

```python
print("Hello")
```

```bash
echo "World"
```

Configuration

[MD046]
style = "fenced"  # Options: "fenced", "indented", "consistent"
ValueDescription
fencedUse triple backticks
indentedUse 4-space indentation
consistentMatch first code block's style

When to Disable

  • Documents mixing styles intentionally
  • Legacy content with established patterns

Rule Details

  • Rule ID: MD046
  • Aliases: code-block-style
  • Category: Formatting
  • Severity: Warning
  • Auto-fix: Yes
  • Supports language specification for syntax highlighting
  • Clearer visual boundaries
  • Easier to copy and paste
  • Works better with nested content
  • MD031 - Blanks around fences
  • MD040 - Fenced code blocks language
  • MD048 - Code fence style

MD048 - Code Fence Style

Code fence style should be consistent.

Why This Rule Exists

Markdown supports two fence styles: backticks and tildes. Consistent style throughout a document improves readability and maintainability.

Styles

Backticks (Common)

```rust
let x = 1;
```

Tildes

~~~rust
let x = 1;
~~~

Examples

Incorrect (Mixed)

```python
print("Hello")
```

~~~bash
echo "World"
~~~

Correct

```python
print("Hello")
```

```bash
echo "World"
```

Configuration

[MD048]
style = "backtick"  # Options: "backtick", "tilde", "consistent"
ValueDescription
backtickUse triple backticks
tildeUse triple tildes
consistentMatch first fence's style

When to Disable

  • Documents with intentional style mixing
  • Content with nested code blocks (tildes inside backticks)

Rule Details

  • Rule ID: MD048
  • Aliases: code-fence-style
  • Category: Formatting
  • Severity: Warning
  • Auto-fix: Yes

Nesting Code Blocks

To show code fences inside code blocks, use different styles:

````markdown
```rust
let x = 1;
```
````
  • MD031 - Blanks around fences
  • MD040 - Fenced code blocks language
  • MD046 - Code block style

Style Rules

These rules enforce consistent styling choices throughout your markdown documents.

Rules in This Category

  • MD013 - Line length
  • MD003 - Heading style
  • MD035 - Horizontal rule style
  • MD036 - Emphasis used instead of a heading
  • MD044 - Proper names should have correct capitalization
  • MD049 - Emphasis style should be consistent
  • MD050 - Strong style should be consistent

Why Style Rules Matter

Consistent style:

  • Creates professional, polished documentation
  • Improves readability and scanning
  • Reduces cognitive load for readers
  • Maintains brand and project consistency
  • Facilitates team collaboration

Common Style Choices

Heading Styles

# ATX Style Heading (Recommended)

Setext Style Heading
====================

Emphasis Styles

*Italic with asterisks*
_Italic with underscores_

**Bold with asterisks**
__Bold with underscores__

Horizontal Rules

---
---

---

Quick Configuration

# .mdbook-lint.toml

# Configure MD013 - Line length
[MD013]
line_length = 100
code_blocks = false
tables = false

# Configure MD003 - Heading style
[MD003]
style = "atx"  # Options: "atx", "setext", "consistent"

# Configure MD035 - Horizontal rule style
[MD035]
style = "---"  # Use three hyphens

# Configure MD049 - Emphasis style
[MD049]
style = "asterisk"  # Options: "asterisk", "underscore", "consistent"

# Configure MD050 - Strong style
[MD050]
style = "asterisk"  # Options: "asterisk", "underscore", "consistent"

Style Guide Template

Create a consistent style guide for your project:

# .mdbook-lint.toml - Project Style Guide

# Line length for readability
[MD013]
line_length = 80

# ATX headings only
[MD003]
style = "atx"

# Consistent emphasis
[MD049]
style = "asterisk"

[MD050]
style = "asterisk"

# Three hyphens for horizontal rules
[MD035]
style = "---"

Best Practices

  1. Choose and document: Pick a style and document it
  2. Be consistent: Use the same style throughout
  3. Consider your audience: Technical vs. general readers
  4. Think about rendering: How it looks in your target output
  5. Automate checks: Use CI/CD to enforce style

MD013 - Line Length

Severity: Warning
Category: Style
Auto-fix: Not available

Rule Description

This rule enforces a maximum line length for markdown files. Long lines can be difficult to read and review, especially in terminals and diff views.

Why This Rule Exists

Line length limits are important because:

  • Improves readability in narrow windows and terminals
  • Makes diffs easier to review in version control
  • Follows traditional text formatting conventions
  • Prevents horizontal scrolling in editors
  • Facilitates side-by-side comparisons

Examples

❌ Incorrect (violates rule)

This is an extremely long line that goes on and on and on, exceeding the configured maximum line length and making it difficult to read in narrow terminals or when viewing diffs.

✅ Correct

This line is broken up into shorter segments.
It's easier to read and review.
Each line stays within the configured limit.

Configuration

[MD013]
line_length = 80        # Maximum line length (default: 80)
code_blocks = true      # Check code blocks (default: true)
tables = true           # Check tables (default: true)
headings = true         # Check headings (default: true)
ignore_reference_definitions = false  # Skip long reference definition lines (default: false)
strict = false          # Strict length (no leniency for URLs) (default: false)
stern = false           # Stern length (allow long lines with no spaces) (default: false)

A link or image reference definition puts the destination on its own line:

[some-long-label]: https://example.com/a/very/long/destination/that/exceeds/the/limit

The label and destination cannot be wrapped, so a long one triggers MD013 with no way to fix it. Set ignore_reference_definitions = true to skip these [label]: destination lines (used by both link and image references).

When to Disable

Consider disabling this rule if:

  • Your team prefers no line length limits
  • You're working with content that requires long lines (tables, URLs)
  • Your documentation is primarily viewed in wide screens
  • You have many long code examples

Disable in Config

# .mdbook-lint.toml
disabled_rules = ["MD013"]

Disable Inline

<!-- mdbook-lint-disable MD013 -->
This can be a very long line that exceeds the normal limits without triggering a violation.
<!-- mdbook-lint-enable MD013 -->

Disable for Specific Elements

[MD013]
# Keep line length check but exclude certain elements
code_blocks = false  # Don't check code blocks
tables = false       # Don't check tables

Tips for Compliance

  1. Break at natural points: Sentences, clauses, or phrases
  2. Use soft wrapping: Let your editor wrap visually while keeping semantic lines
  3. Consider semantic line breaks: One sentence per line
  4. Extract long URLs: Use reference-style links
<!-- Instead of -->
Check out [this very long link text](https://example.com/very/long/path/to/resource)

<!-- Use -->
Check out [this very long link text][1]

[1]: https://example.com/very/long/path/to/resource

References

MD035 - Horizontal Rule Style

Horizontal rule style should be consistent.

Why This Rule Exists

Markdown supports multiple horizontal rule syntaxes. Consistent style throughout a document improves readability and maintainability.

Styles

---

---


---


---


** *

All render as horizontal rules but mixing them is inconsistent.

Examples

Incorrect

Section one content.

---

Section two content.

---


Section three content.

Correct

Section one content.

---

Section two content.

---

Section three content.

Configuration

[MD035]
style = "---"  # Options: "---", "***", "___", "consistent"
ValueDescription
---Three dashes
***Three asterisks
___Three underscores
consistentMatch first occurrence

When to Disable

  • Documents with intentional style variation
  • Content imported from multiple sources

Rule Details

  • Rule ID: MD035
  • Aliases: hr-style
  • Category: Formatting
  • Severity: Warning
  • Auto-fix: Yes
  • MD003 - Heading style consistency
  • MD004 - Unordered list style consistency

MD043 - Required Heading Structure

Required heading structure not found.

Why This Rule Exists

Some documents must follow a specific heading structure for consistency across a project. This rule enforces a predefined heading pattern.

Examples

Configuration

[MD043]
headings = ["# Title", "## Introduction", "## Usage", "## API", "## License"]

Incorrect

# My Project

## Getting Started

## API Reference

Missing required "Introduction" and has different heading names.

Correct

# Title

## Introduction

## Usage

## API

## License

Configuration 2

[MD043]
headings = []          # Required headings in order
match_case = true      # Case-sensitive matching (default: true)

Wildcards

Use * to allow any heading at a position:

[MD043]
headings = ["# *", "## Introduction", "##*"]

When to Disable

  • Documents that don't follow a template
  • Creative content without structure requirements
  • Auto-generated files

Rule Details

  • Rule ID: MD043
  • Aliases: required-headings
  • Category: Structure
  • Severity: Warning
  • Auto-fix: No

Use Cases

  • README templates across repositories
  • Documentation standards
  • API documentation structure
  • Legal documents with required sections
  • MD001 - Heading increment
  • MD024 - Duplicate headings
  • MD025 - Single top-level heading

MD044 - Proper Names Capitalization

Proper names should have correct capitalization.

Why This Rule Exists

Brand names, product names, and technical terms often have specific capitalization. Consistent capitalization improves professionalism and readability.

Examples

Configuration

[MD044]
names = ["JavaScript", "GitHub", "macOS", "iOS"]
code_blocks = false  # Don't check inside code blocks

Incorrect

Install the package using Github.

This works on MacOS and IOS devices.

Learn javascript programming.

Correct

Install the package using GitHub.

This works on macOS and iOS devices.

Learn JavaScript programming.

Configuration 2

[MD044]
names = []            # List of proper names with correct capitalization
code_blocks = false   # Check inside code blocks (default: false)
html_elements = false # Check inside HTML elements (default: false)

Common Names

[MD044]
names = [
  "JavaScript", "TypeScript", "Node.js", "npm",
  "GitHub", "GitLab", "Bitbucket",
  "macOS", "iOS", "iPadOS", "watchOS", "tvOS",
  "MySQL", "PostgreSQL", "MongoDB", "SQLite",
  "Rust", "Python", "Ruby", "Kotlin"
]

When to Disable

  • Documents where capitalization varies intentionally
  • Code-heavy content where names appear in identifiers
  • Historical documents preserving original text

Rule Details

  • Rule ID: MD044
  • Aliases: proper-names
  • Category: Style
  • Severity: Warning
  • Auto-fix: No

Notes

The rule is smart about context:

  • Ignores text inside code blocks (configurable)
  • Ignores text inside inline code spans
  • Ignores URLs and link destinations
  • MD038 - Spaces inside code spans

Emphasis Rules

Rules for formatting bold and italic text.

Rules in This Category

RuleDescriptionAuto-fix
MD036Emphasis used instead of headingNo
MD037Spaces inside emphasis markersYes
MD049Emphasis style consistencyYes
MD050Strong emphasis style consistencyYes

Overview

Emphasis rules ensure consistent formatting of bold (**text**) and italic (*text*) text throughout your documents.

Common Issues

  • Using **bold** on a line by itself as a pseudo-heading

  • Spaces inside markers like **bold** that may not render

  • Mixing asterisks and underscores inconsistently

Best Practices

  • Use real headings (##) instead of bold text for sections
  • Keep emphasis markers tight against text (no internal spaces)
  • Choose one style (asterisks or underscores) and use it consistently

MD036 - Emphasis Instead of Heading

Emphasis used instead of a heading.

Why This Rule Exists

Using bold or italic text on its own line as a pseudo-heading breaks document structure. Real headings provide proper hierarchy for navigation, accessibility, and table of contents generation.

Examples

Incorrect

**Introduction**

This section introduces the topic.

*Getting Started*

Follow these steps to begin.

Correct

## Introduction

This section introduces the topic.

## Getting Started

Follow these steps to begin.

Configuration

[MD036]
punctuation = ".,;:!?。;:!?"  # Punctuation that indicates not a heading

Lines ending with punctuation are assumed to be emphasized text, not pseudo-headings.

When to Disable

  • Documents using emphasis for visual styling
  • Content where headings aren't appropriate
  • Presentations or slides with different formatting needs

Rule Details

  • Rule ID: MD036
  • Aliases: no-emphasis-as-heading
  • Category: Emphasis
  • Severity: Warning
  • Auto-fix: No

Why This Matters

Pseudo-headings created with emphasis:

  • Don't appear in table of contents
  • Break accessibility for screen readers
  • Can't be linked to with anchors
  • Don't contribute to document outline
  • MD001 - Heading increment
  • MD003 - Heading style
  • MD022 - Blanks around headings

MD037 - Spaces Inside Emphasis

Spaces inside emphasis markers.

Why This Rule Exists

Spaces immediately inside emphasis markers may prevent proper rendering in some Markdown parsers. The emphasis won't be applied, leaving literal asterisks or underscores in the output.

Examples

Incorrect

This is **bold** text.


This is *italic* text.


Here is __also bold__ text.

Correct

This is **bold** text.

This is *italic* text.

Here is __also bold__ text.

Configuration

This rule has no configuration options.

When to Disable

  • Generally should not be disabled as spaced emphasis rarely renders correctly

Rule Details

  • Rule ID: MD037
  • Aliases: no-space-in-emphasis
  • Category: Emphasis
  • Severity: Warning
  • Auto-fix: Yes (removes spaces inside markers)
  • MD038 - Spaces inside code spans
  • MD039 - Spaces inside link text
  • MD049 - Emphasis style
  • MD050 - Strong style

MD049 - Emphasis Style

Emphasis style should be consistent.

Why This Rule Exists

Markdown supports two emphasis markers: asterisks and underscores. Consistent style improves readability and maintainability.

Styles

Asterisks

This is *italic* text.

Underscores

This is _italic_ text.

Examples

Incorrect (Mixed)

This is *italic* and this is _also italic_.

Use *consistent* formatting _throughout_ the document.

Correct

This is *italic* and this is *also italic*.

Use *consistent* formatting *throughout* the document.

Configuration

[MD049]
style = "asterisk"  # Options: "asterisk", "underscore", "consistent"
ValueDescription
asteriskUse *text*
underscoreUse _text_
consistentMatch first occurrence

When to Disable

  • Documents with intentional style variation
  • Content imported from multiple sources

Rule Details

  • Rule ID: MD049
  • Aliases: emphasis-style
  • Category: Formatting
  • Severity: Warning
  • Auto-fix: Yes

Note on Underscores

Underscores inside words are not treated as emphasis:

some_variable_name  <!-- Not italic, just text -->
  • MD037 - Spaces inside emphasis
  • MD050 - Strong style

MD050 - Strong Style

Strong emphasis style should be consistent.

Why This Rule Exists

Markdown supports two strong emphasis markers: double asterisks and double underscores. Consistent style improves readability.

Styles

Asterisks (Common)

This is **bold** text.

Underscores

This is __bold__ text.

Examples

Incorrect (Mixed)

This is **bold** and this is __also bold__.

Correct

This is **bold** and this is **also bold**.

Configuration

[MD050]
style = "asterisk"  # Options: "asterisk", "underscore", "consistent"
ValueDescription
asteriskUse **text**
underscoreUse **text**

| consistent | Match first occurrence |

When to Disable

  • Documents with intentional style variation
  • Content imported from multiple sources

Rule Details

  • Rule ID: MD050
  • Aliases: strong-style
  • Category: Formatting
  • Severity: Warning
  • Auto-fix: Yes
  • MD037 - Spaces inside emphasis
  • MD049 - Emphasis style

Table Rules

Rules for formatting Markdown tables.

Rules in This Category

RuleDescriptionAuto-fix
MD055Table pipe style consistencyYes
MD056Table column countYes
MD058Tables surrounded by blank linesYes

Overview

Table rules ensure consistent and valid table formatting. Properly formatted tables render correctly across all Markdown parsers.

Common Issues

  • Inconsistent pipe style (leading/trailing pipes)
  • Rows with different numbers of columns
  • Tables not separated from surrounding content

Best Practices

  • Use leading and trailing pipes for clarity
  • Ensure all rows have the same number of columns
  • Surround tables with blank lines
  • Align columns for readable source (optional)

Example Table


| Header 1 | Header 2 | Header 3 |
|----------|----------|----------|
| Cell 1   | Cell 2   | Cell 3   |
| Cell 4   | Cell 5   | Cell 6   |

MD055 - Table Pipe Style

Table pipe style should be consistent.

Why This Rule Exists

Markdown tables can have leading and trailing pipes or omit them. Consistent style improves readability and source formatting.

Styles

| Header 1 | Header 2 |
|----------|----------|
| Cell 1   | Cell 2   |

No Leading/Trailing

Header 1 | Header 2
---------|----------
Cell 1   | Cell 2

Leading Only

| Header 1 | Header 2
|----------|----------
| Cell 1   | Cell 2

Examples

Incorrect (Mixed)

| Header 1 | Header 2 |
|----------|----------|
Cell 1   | Cell 2

Correct

| Header 1 | Header 2 |
|----------|----------|
| Cell 1   | Cell 2   |

Configuration

[MD055]
style = "leading_and_trailing"  # Options: see below
ValueDescription
leading_and_trailingPipes on both ends
leading_onlyOnly leading pipes
trailing_onlyOnly trailing pipes
no_leading_or_trailingNo outer pipes
consistentMatch first table's style

When to Disable

  • Documents with tables from different sources
  • Content where specific formatting is required

Rule Details

  • Rule ID: MD055
  • Aliases: table-pipe-style
  • Category: Formatting
  • Severity: Warning
  • Auto-fix: Yes
  • MD056 - Table column count
  • MD058 - Blanks around tables

MD056 - Table Column Count

Table column count should be consistent.

Why This Rule Exists

All rows in a table should have the same number of columns. Mismatched column counts cause rendering issues and indicate data entry errors.

Examples

Incorrect

| Header 1 | Header 2 | Header 3 |
|----------|----------|----------|
| Cell 1   | Cell 2   |
| Cell 1   | Cell 2   | Cell 3   | Cell 4 |

Row 2 has too few columns, row 3 has too many.

Correct

| Header 1 | Header 2 | Header 3 |
|----------|----------|----------|
| Cell 1   | Cell 2   | Cell 3   |
| Cell 4   | Cell 5   | Cell 6   |

Empty Cells

| Header 1 | Header 2 | Header 3 |
|----------|----------|----------|
| Cell 1   |          | Cell 3   |
| Cell 4   | Cell 5   |          |

Configuration

This rule has no configuration options.

When to Disable

  • Tables intentionally using colspan-like behavior
  • Content from sources with non-standard table formats

Rule Details

  • Rule ID: MD056
  • Aliases: table-column-count
  • Category: Structure
  • Severity: Error
  • Auto-fix: Yes (adds empty cells)
  • MD055 - Table pipe style
  • MD058 - Blanks around tables

MD058 - Blanks Around Tables

Tables should be surrounded by blank lines.

Why This Rule Exists

Blank lines around tables ensure proper parsing and improve readability. Some Markdown parsers require blank lines to correctly identify table boundaries.

Examples

Incorrect

Some text here.
| Header 1 | Header 2 |
|----------|----------|
| Cell 1   | Cell 2   |
More text here.

Correct

Some text here.

| Header 1 | Header 2 |
|----------|----------|
| Cell 1   | Cell 2   |

More text here.

Configuration

This rule has no configuration options.

When to Disable

  • Documents with compact formatting requirements
  • Content where tables flow tightly with surrounding text

Rule Details

  • Rule ID: MD058
  • Aliases: blanks-around-tables
  • Category: Formatting
  • Severity: Warning
  • Auto-fix: Yes
  • MD022 - Blanks around headings
  • MD031 - Blanks around fenced code blocks
  • MD032 - Blanks around lists
  • MD055 - Table pipe style
  • MD056 - Table column count

MD060 - Table Column Style

Table column style should be consistent.

Why This Rule Exists

Markdown table cells can be padded with spaces in different ways. Mixing styles within the same table, or between tables in the same document, makes source files harder to scan and signals that rows were edited by hand at different times without reformatting the table.

Styles

  • Compact - no padding at all around cell content: |A|B|
  • Tight - one or more spaces of padding around cell content: | A | B |
  • Aligned - a table-level label rather than a cell-level one. A table is classified aligned when at least one of its cells has more than one trailing space, which is what happens when padding is used to line columns up. The cells of such a table are still individually classified as tight.

Because a cell is only ever classified compact or tight, the comparison the rule performs is padded against not padded. It never measures column widths, and it never counts padding beyond the first space.

By default the rule detects the style from the first table in the document and enforces it on every table, including that first one. A mismatch inside a single table is reported, which is what the example below shows.

Examples

Incorrect

| A | B |
|---|---|
|1| 2 |

The header row uses tight style (| A |), but the data row mixes compact (|1|) and tight (2) cells.

Correct

| A | B |
|---|---|
| 1 | 2 |

Every cell uses the same single-space padding.

Aligned Style

| Column 1 | Col 2 | Column 3 |
|----------|-------|----------|
| Value 1  | V2    | Value 3  |
| V4       | Val 5 | V6       |

Extra trailing spaces are never a violation. In consistent mode a table like this one is detected as aligned, and that detection then requires the rest of the document only to pad its cells: a plain | A | B | table passes against it.

Tables inside fenced code blocks are ignored.

Configuration

[MD060]
# Options: "aligned", "compact", "tight", "any", "consistent"
style = "consistent"

style

  • consistent (default) - detect the style from the first table in the document and enforce it on every table, the first one included.
  • tight - require at least one space of padding around cell content. It does not require exactly one: | A | passes, and so does a table whose padding varies per column. Only a cell with no padding at all is a violation.
  • aligned - the same check as tight. It rejects cells with no padding and accepts every padded cell, so a plain | A | B | table passes. Nothing in the rule requires columns to line up when this is set; the only difference from tight is the word used in the violation message.
  • compact - require no padding around cell content. Any padded cell is a violation.
  • any - disable the check entirely; any mix of styles is allowed.

When to Disable

  • Documents that assemble tables from multiple generated sources
  • Content where table formatting is not worth normalizing

Rule Details

  • Rule ID: MD060
  • Aliases: table-column-style
  • Category: Formatting
  • Severity: Warning
  • Auto-fix: No
  • MD055 - Table pipe style
  • MD056 - Table column count
  • MD058 - Blanks around tables

Image Rules

Rules for image formatting and accessibility.

Rules in This Category

RuleDescriptionAuto-fix
MD045Images should have alt textYes

Overview

Image rules ensure that images are accessible and properly formatted.

Why Alt Text Matters

Alt text (alternative text) serves multiple purposes:

  • Accessibility: Screen readers use alt text to describe images
  • Fallback: Displays when images fail to load
  • SEO: Search engines use alt text to understand image content

Best Practices

  • Write descriptive alt text that conveys the image's purpose
  • Keep alt text concise but informative
  • For decorative images, use empty alt text ![](image.png)
  • Describe charts and diagrams with their key data points

Example

![Bar chart showing 50% increase in sales from Q1 to Q4](sales-chart.png)

MD045 - Images Should Have Alt Text

Images should have alternate text (alt text).

Why This Rule Exists

Alt text is essential for accessibility. Screen readers use it to describe images to visually impaired users. It also displays when images fail to load.

Examples

Incorrect

![](image.png)

![][logo]

[logo]: logo.png

Correct

![Screenshot of the dashboard](image.png)

![Company logo][logo]

[logo]: logo.png "Company Logo"

Good Alt Text

![Bar chart showing sales growth from 2020 to 2024](sales-chart.png)

![Red error icon indicating a failed operation](error-icon.svg)

Configuration

This rule has no configuration options.

When to Disable

  • Decorative images that don't convey information
  • Documents where images are supplementary

Rule Details

  • Rule ID: MD045
  • Aliases: no-alt-text
  • Category: Images
  • Severity: Warning
  • Auto-fix: Yes (adds placeholder alt text)

Writing Good Alt Text

Image TypeAlt Text Approach
InformativeDescribe the content and purpose
DecorativeUse empty alt !["](decorative.png)
ChartsSummarize the data shown
ScreenshotsDescribe what the screenshot shows
IconsDescribe the action or meaning

Accessibility Standards

This rule helps comply with:

  • WCAG 2.1 Success Criterion 1.1.1 (Non-text Content)
  • Section 508 accessibility requirements
  • MD033 - No inline HTML
  • MD052 - Reference links and images

HTML Rules

Rules for inline HTML usage in Markdown.

Rules in This Category

RuleDescriptionAuto-fix
MD033Inline HTML should be avoidedNo

Overview

HTML rules control the use of raw HTML within Markdown documents. While Markdown supports inline HTML, using it reduces portability and can introduce security concerns.

Why Avoid HTML

  • Portability: Not all Markdown renderers support HTML
  • Security: HTML can introduce XSS vulnerabilities
  • Maintainability: Markdown is easier to read and maintain
  • Consistency: Mixing HTML and Markdown creates inconsistent documents

When HTML Is Acceptable

Some features require HTML:

  • Collapsible sections (<details>)
  • Keyboard shortcuts (<kbd>)
  • Subscript/superscript (<sub>, <sup>)
  • Complex layouts not possible in Markdown

Configuration

Allow specific HTML elements while blocking others:

[MD033]
allowed_elements = ["details", "summary", "kbd", "br"]

MD033 - No Inline HTML

Inline HTML should be avoided.

Why This Rule Exists

Markdown documents should remain portable and renderable in environments that don't support HTML. Raw HTML also makes documents harder to maintain and can introduce security concerns in some contexts.

Examples

Incorrect

<div class="warning">
This is a warning message.
</div>

Click <a href="https://example.com">here</a> for more.

<br>

<img src="image.png" alt="An image">

Correct

> **Warning**: This is a warning message.

Click [here](https://example.com) for more.

![An image](image.png)

Configuration

[MD033]
allowed_elements = []  # HTML elements to allow (default: none)

Allow Specific Elements

[MD033]
allowed_elements = ["br", "details", "summary"]

When to Disable

  • Documents requiring HTML features not in Markdown
  • Content using HTML for accessibility features
  • mdBook projects using HTML preprocessors

Rule Details

  • Rule ID: MD033
  • Aliases: no-inline-html
  • Category: Content
  • Severity: Warning
  • Auto-fix: No

Common Allowed Elements

ElementUse Case
brLine breaks within paragraphs
detailsCollapsible sections
summarySummary for details
kbdKeyboard input
sub, supSubscript/superscript
  • MD045 - Images should have alt text

mdBook-Specific Rules

These rules are specifically designed for mdBook projects, validating mdBook-specific syntax, conventions, and structure.

Rules

Rule IDNameDescription
MDBOOK001code-block-languageCode blocks should have language tags
MDBOOK002summary-structureSUMMARY.md should follow mdBook structure
MDBOOK003internal-linksInternal links should be valid
MDBOOK004part-titlesPart titles should be formatted correctly
MDBOOK005chapter-pathsChapter paths should be relative
MDBOOK006draft-chaptersDraft chapters should have content or be marked
MDBOOK007separator-syntaxSeparator syntax should be correct

Why mdBook-Specific Rules

mdBook extends standard Markdown with special features:

  1. SUMMARY.md Structure: Defines book organization
  2. Include Syntax: {{#include file.md}}
  3. Playground Links: {{#playground file.rs}}
  4. Hidden Lines: Lines starting with # in Rust code blocks
  5. Quiz Support: Interactive quizzes in documentation
  6. Custom Renderers: Different output formats

These rules ensure your mdBook project:

  • Builds correctly
  • Renders properly in all output formats
  • Maintains consistent structure
  • Follows mdBook best practices

SUMMARY.md Structure

The SUMMARY.md file is the backbone of any mdBook project:

# Summary

[Introduction](./introduction.md)

# User Guide

- [Installation](./guide/installation.md)
- [Getting Started](./guide/getting-started.md)
  - [Basic Usage](./guide/basic-usage.md)
  - [Advanced Usage](./guide/advanced-usage.md)

# Reference

- [Configuration](./reference/configuration.md)
- [API](./reference/api.md)

---

[Contributors](./contributors.md)

Rules MDBOOK002-MDBOOK007 validate various aspects of this structure.

Common mdBook Issues

Missing Language Tags

Problem: Code blocks without language tags don't get syntax highlighting.

fn main() { println!("No highlighting!"); }

Solution: Always specify the language.

```rust
fn main() {
    println!("Properly highlighted!");
}

### Broken Internal Links

**Problem**: Links to non-existent chapters break navigation.

```markdown
- [Missing Chapter](./does-not-exist.md)

Solution: Ensure all linked files exist.

Invalid SUMMARY.md Format

Problem: Incorrect indentation or syntax breaks book generation.

- [Chapter 1](./chapter1.md)
    - [Wrong indent](./sub.md)  # Should be 2 spaces, not 4
[Missing dash](./chapter2.md)    # Should be "- [...]"

Solution: Follow mdBook's SUMMARY.md conventions.

Integration with CI/CD

Use these rules in your CI pipeline:

# .github/workflows/mdbook.yml
name: mdBook Checks

on: [push, pull_request]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run mdbook-lint

        run: |

          cargo install mdbook-lint
          mdbook-lint check

Configuration

Enable only mdBook rules:

# .mdbook-lint.toml
[rules]
# Disable all standard rules
"MD*" = false

# Enable only mdBook rules
"MDBOOK*" = true

Or enable both sets:

[rules]
# Use defaults (all rules enabled)

# Customize specific mdBook rules
[MDBOOK001]
allow_missing = false

Best Practices

  1. Run Checks Before Building: Catch issues early
  2. Include in Pre-commit Hooks: Prevent broken commits
  3. Document Exceptions: If disabling rules, explain why
  4. Test Rendering: Lint checks complement, not replace, build tests
  5. Version Control SUMMARY.md: Track structure changes

Some standard rules are particularly relevant for mdBook:

  • MD041 - First line should be a heading (important for chapters)
  • MD025 - Single H1 (one main heading per chapter)
  • MD051 - Link fragments (for cross-references)

References

MDBOOK001 - Code Blocks Should Have Language Tags

Code blocks should have language tags.

This rule is triggered when code blocks don't have language tags for syntax highlighting. Proper language tags help with documentation clarity and proper rendering in mdBook.

Why This Rule Exists

mdBook uses language tags for:

  • Syntax highlighting in rendered output
  • Proper code formatting and display
  • Enabling language-specific features (like line numbers, highlighting specific lines)
  • Improving accessibility for screen readers
  • Better SEO and content understanding

Examples

❌ Incorrect (violates rule)

```
fn main() {
    println!("Hello, world!");
}
```

✅ Correct

```rust
fn main() {
    println!("Hello, world!");
}
```

Other valid examples:

```bash
cargo build --release
```

```toml
[dependencies]
serde = "1.0"
```

```json
{
  "name": "example",
  "version": "1.0.0"
}
```

Special Language Tags

mdBook supports special language tags:

  • text or plain - for plain text without highlighting
  • console - for command-line output
  • diff - for showing differences
  • ignore - for Rust code that shouldn't be tested
  • no_run - for Rust code that compiles but shouldn't run
  • should_panic - for Rust code expected to panic

Configuration

This rule has no configuration options. All code blocks should have language tags.

When to Disable

Consider disabling this rule if:

  • You have many legacy code blocks without language tags
  • You're using a custom mdBook renderer that doesn't require language tags

Rule Details

  • Rule ID: MDBOOK001
  • Category: mdBook-specific
  • Severity: Warning
  • Automatic Fix: Not available (requires manual language identification)

Why Language Tags Matter in mdBook

Syntax Highlighting

mdBook uses language tags to apply syntax highlighting via highlight.js or similar libraries:

Without language tag:

```
fn main() {
    println!("Hello, world!");
}
```

Renders as plain text with no highlighting.

With language tag:

```rust
fn main() {
    println!("Hello, world!");
}
```

Renders with proper Rust syntax highlighting.

mdBook-Specific Features

Language tags enable mdBook-specific features:

Rust Playground Integration

fn main() {
    println!("This code can be run in the Rust Playground!");
}

Hidden Lines in Rust Code

```rust
# fn main() {
println!("Only this line is shown");
# }
```

Test Annotations

```rust,ignore
// This code won't be tested
fn example() {}
```

```rust,no_run
// This code is compiled but not run
fn main() {
    loop {} // Would hang if run
}
```

```rust,should_panic
// This code is expected to panic
fn main() {
    panic!("This is expected!");
}
```

Common Language Tags

Programming Languages

LanguageTagCommon Uses
RustrustPrimary language for mdBook documentation
JavaScriptjavascript or jsWeb examples, Node.js code
Pythonpython or pyScripts, examples
Shellbash or shCommand-line examples
TOMLtomlConfiguration files
JSONjsonData structures, APIs
YAMLyaml or ymlConfiguration, CI/CD
HTMLhtmlWeb markup
CSScssStyling examples
SQLsqlDatabase queries

Special Tags

TagPurpose
text or plainPlain text without highlighting
consoleTerminal output with prompt highlighting
diffShowing differences with +/- highlighting
markdown or mdMarkdown source code

Examples by Use Case

Configuration Files

TOML (Cargo.toml):

```toml
[package]
name = "my-project"
version = "0.1.0"

[dependencies]
serde = "1.0"
```

JSON (package.json):

```json
{
  "name": "my-project",
  "version": "1.0.0",
  "dependencies": {
    "react": "^18.0.0"
  }
}
```

Command-Line Examples

Shell commands:

```bash
# Install mdbook-lint
cargo install mdbook-lint

# Run the linter
mdbook-lint check
```

Console output:

```console
$ cargo build
   Compiling my-project v0.1.0
    Finished dev [unoptimized] target(s) in 2.34s
```

Showing Changes

Diff format:

```diff
- Old line that was removed
+ New line that was added
  Unchanged line
```

Multi-language Examples

HTML with embedded CSS and JavaScript:

```html
<!DOCTYPE html>
<html>
<head>
    <style>
        body { font-family: sans-serif; }
    </style>
</head>
<body>
    <h1>Hello</h1>
    <script>
        console.log('Hello, world!');
    </script>
</body>
</html>
```

Choosing the Right Language Tag

Decision Tree

  1. Is it code? → Use appropriate language tag
  2. Is it terminal output? → Use console
  3. Is it a diff? → Use diff
  4. Is it plain text? → Use text or plain
  5. Is it data? → Use format tag (json, yaml, toml)
  6. Not sure? → Use text rather than no tag

Language Detection Tips

Look for characteristic syntax:

  • Rust: fn, let, mut, impl, ::
  • Python: def, import, : for blocks, no semicolons
  • JavaScript: function, const, =>, var
  • Shell: $, # for comments, command names
  • JSON: {, }, :, quoted keys
  • TOML: [sections], key = value, # comments

Edge Cases

Mixed Language Blocks

For templates or mixed content, choose the primary language:

```html
<!-- This is primarily HTML even though it contains CSS -->
<div style="color: red;">Content</div>
```

Unknown or Custom Languages

For unsupported languages, use text:

```text
CUSTOM_SYNTAX {
    nonstandard = syntax
}
```

File Names as Context

Sometimes include the filename for context:

```rust
// src/main.rs
fn main() {
    println!("Hello!");
}
```

Impact on mdBook Features

Search Indexing

Code blocks with language tags are better indexed for search.

Syntax Theme Support

Language tags enable proper theme application:

  • Light themes show appropriate colors
  • Dark themes adjust for readability
  • Contrast themes maintain accessibility

Copy Button

mdBook's copy button works better with properly tagged code blocks.

Line Numbers

Some themes show line numbers only for tagged code blocks:

```rust,linenos
fn main() {
    println!("Line 1");
    println!("Line 2");
}
```

Configuration 2

This rule has no configuration options. All code blocks should have language tags for optimal mdBook rendering.

  • MD040 - Fenced code blocks should have a language specified (standard rule)
  • MD046 - Code block style
  • MD048 - Code fence style

References

MDBOOK002 - Invalid Internal Link

Severity: Error
Category: mdBook-specific
Auto-fix: Not available

Rule Description

This rule validates internal links within mdBook projects, ensuring they point to valid files and anchors. Broken internal links create a poor reading experience and navigation issues.

Why This Rule Exists

Valid internal links are crucial because:

  • Ensures readers can navigate between chapters
  • Prevents 404 errors in generated documentation
  • Maintains documentation integrity
  • Enables proper mdBook navigation features
  • Helps identify renamed or moved files

Examples

❌ Incorrect (violates rule)

<!-- Link to non-existent file -->
See [configuration](./configs.md) for details.

<!-- Link to non-existent anchor -->
Check the [installation section](./setup.md#install)

<!-- Broken relative path -->
Read more in [the guide](../guides/intro.md)

✅ Correct

<!-- Valid file link -->
See [configuration](./configuration.md) for details.

<!-- Valid anchor link -->
Check the [installation section](./getting-started.md#installation)

<!-- Correct relative path -->
Read more in [the introduction](./introduction.md)

<!-- External links are not checked -->
Visit [Rust website](https://www.rust-lang.org)

What This Rule Checks

  1. File existence: Verifies linked .md files exist
  2. Anchor validity: Confirms heading anchors are present
  3. Path resolution: Validates relative paths from current file
  4. SUMMARY.md links: Ensures all chapter links are valid

Configuration

[MDBOOK002]
check_anchors = false    # Validate same-document anchors (default: false)
allow_external = true    # Skip external URLs (default: true)
check_images = false     # Also validate image paths (default: false)
  • check_anchors: when enabled, same-document anchor links ([text](#section)) are checked against the headings in the file. Anchors that point at another file (other.md#section) are validated by MDBOOK006, not here.
  • allow_external: external URLs (http, https, mailto, ftp, tel) are skipped. Set it to false to report them instead, for books that must not link off-site.
  • check_images: when enabled, image paths (![alt](path)) are resolved the same way link paths are.

Common Issues and Solutions

Issue: File Renamed

<!-- Before -->
[Old name](./old-filename.md)

<!-- After -->
[New name](./new-filename.md)

Issue: Heading Changed

<!-- Heading changed from "## Installation" to "## Setup" -->
<!-- Before -->
[Install](./guide.md#installation)

<!-- After -->
[Install](./guide.md#setup)

Issue: File Moved

<!-- File moved to subdirectory -->
<!-- Before -->
[Guide](./guide.md)

<!-- After -->
[Guide](./user-guide/guide.md)

When to Disable

Consider disabling this rule if:

  • You're in the middle of a major restructuring
  • Your build process generates files dynamically
  • You have external link checking handled separately

Disable in Config

# .mdbook-lint.toml
disabled_rules = ["MDBOOK002"]

Disable Inline

<!-- mdbook-lint-disable MDBOOK002 -->
[Temporarily broken link](./todo.md)
<!-- mdbook-lint-enable MDBOOK002 -->

Tips for Compliance

  1. Use relative paths: More maintainable than absolute paths
  2. Update links when renaming: Use search and replace
  3. Test navigation: Click through links after changes
  4. Use anchor generation tools: Ensure correct anchor format

Anchor Format

mdBook generates anchors from headings using these rules:

  • Convert to lowercase
  • Replace spaces with hyphens
  • Remove special characters
  • Handle duplicates with numbers
## Hello World!     <!-- #hello-world -->
## User's Guide     <!-- #users-guide -->
## 1.2.3 Version    <!-- #123-version -->

References

MDBOOK003 - Invalid SUMMARY.md Structure

Severity: Error
Category: mdBook-specific
Auto-fix: Not available

Rule Description

This rule validates that SUMMARY.md follows mdBook's required structure and conventions. The SUMMARY.md file defines your book's table of contents and must follow specific formatting rules.

Why This Rule Exists

Proper SUMMARY.md structure is essential because:

  • mdBook uses it to generate navigation
  • Incorrect structure causes build failures
  • Defines the reading order of chapters
  • Controls the book's hierarchical organization
  • Enables proper sidebar navigation

Examples

❌ Incorrect (violates rule)

# Summary

[Introduction](./introduction.md)

- Part 1
  - [Chapter 1](./chapter1.md)
  
* [Chapter 2](./chapter2.md)  <!-- Mixed list markers -->

  - [Chapter 3](./chapter3.md)  <!-- Incorrect indentation -->
  
[](./empty.md)  <!-- Empty link text -->

- - [Double nested](./nested.md)  <!-- Invalid nesting -->

✅ Correct

# Summary

[Introduction](./introduction.md)

# User Guide

- [Getting Started](./getting-started.md)
  - [Installation](./installation.md)
  - [Configuration](./configuration.md)
- [Advanced Usage](./advanced.md)

# Reference

- [API Documentation](./api.md)
- [Configuration Reference](./config-ref.md)

---

[Contributors](./contributors.md)

SUMMARY.md Structure Rules

Required Elements

  1. Title: Must start with # Summary
  2. Prefix Chapter: Optional [Introduction](./intro.md) before numbered chapters
  3. Numbered Chapters: Use consistent list markers (- or *)
  4. Suffix Chapters: Optional unnumbered chapters after separator

Formatting Rules

  • Consistent indentation: Use 2 or 4 spaces per level
  • Consistent list markers: Use either - or * throughout
  • Valid links: All links must have text and valid paths
  • Proper nesting: Child chapters indented under parents
  • Part headers: Use # Part Name for sections

Special Elements

# Summary

[Preface](./preface.md)          <!-- Prefix chapter -->

# Part I

- [Chapter 1](./ch1.md)           <!-- Numbered chapter -->
  - [Section 1.1](./ch1-1.md)     <!-- Nested chapter -->
- [Chapter 2](./ch2.md)
  - [Draft]()                     <!-- Draft chapter (no link) -->

---                               <!-- Separator -->

[Appendix A](./appendix-a.md)    <!-- Suffix chapter -->

Configuration

[MDBOOK003]
allow_draft_chapters = true    # Allow chapters without links (default: true)
require_part_headers = false   # Require part headers (default: false)
max_depth = 3                  # Maximum nesting depth (default: unlimited)
  • allow_draft_chapters: draft entries are written [Title]() with an empty link. Set it to false to require every chapter to point at a file.
  • require_part_headers: when enabled, a summary that lists chapters must also declare at least one part header (# Part title).
  • max_depth: deepest chapter nesting allowed, counting top-level chapters as depth 1. Leave it unset for unlimited nesting.

Common Issues and Solutions

Issue: Mixed List Markers

<!-- Wrong -->
- [Chapter 1](./ch1.md)
* [Chapter 2](./ch2.md)

<!-- Correct -->
- [Chapter 1](./ch1.md)
- [Chapter 2](./ch2.md)

Issue: Incorrect Indentation

<!-- Wrong -->
- [Chapter 1](./ch1.md)
   - [Section](./sec.md)  <!-- 3 spaces -->

<!-- Correct -->
- [Chapter 1](./ch1.md)
  - [Section](./sec.md)   <!-- 2 spaces -->

Issue: Invalid Draft Syntax

<!-- Wrong -->
- [TODO](.)
- [Draft](/)

<!-- Correct -->
- [Draft]()

When to Disable

Consider disabling this rule if:

  • You're using a custom mdBook theme with different requirements
  • Your build process generates SUMMARY.md dynamically
  • You're migrating from another documentation system

Disable in Config

# .mdbook-lint.toml
disabled_rules = ["MDBOOK003"]

Tips for Compliance

  1. Use consistent indentation: Pick 2 or 4 spaces and stick with it
  2. Order matters: Chapters appear in the order listed
  3. Test the build: Run mdbook build to verify structure
  4. Use draft chapters: For work-in-progress sections

References

MDBOOK004 - No Duplicate Chapter Titles

Chapter titles should be unique across the book.

Why This Rule Exists

Duplicate chapter titles create confusion in navigation and can cause issues with mdBook's URL generation. Each chapter should have a distinct, identifiable title.

Examples

Incorrect (SUMMARY.md)

# Summary

- [Introduction](./intro.md)
- [Getting Started](./start.md)
- [Introduction](./advanced-intro.md)  <!-- Duplicate -->

Correct

# Summary

- [Introduction](./intro.md)
- [Getting Started](./start.md)
- [Advanced Introduction](./advanced-intro.md)

Configuration

[MDBOOK004]
case_sensitive = true                  # Case-sensitive comparison (default: true)
ignore_prefixes = ["Chapter", "Part"]  # Prefixes stripped before comparing (default: none)
  • case_sensitive: with the default, "Setup" and "setup" are different titles. Set it to false to treat them as duplicates.
  • ignore_prefixes: the first matching prefix is stripped from a title (along with the whitespace after it) before comparison, so "Chapter Setup" and "Setup" compare equal.

When to Disable

  • Books with intentionally repeated section names
  • Multi-part books where repetition is meaningful

Rule Details

  • Rule ID: MDBOOK004
  • Aliases: no-duplicate-chapter-titles
  • Category: MdBook
  • Severity: Warning
  • Auto-fix: No

Impact

Duplicate titles can cause:

  • Confusing navigation sidebar
  • Ambiguous URL paths
  • Search result confusion
  • Poor user experience

MDBOOK005 - Orphaned Files

Severity: Warning
Category: mdBook-specific
Auto-fix: Not available

Rule Description

This rule detects markdown files in your mdBook source directory that are not referenced in SUMMARY.md. Orphaned files won't be included in the built book and represent unused or forgotten content.

Why This Rule Exists

Detecting orphaned files is important because:

  • Identifies forgotten or lost content
  • Helps maintain a clean project structure
  • Prevents confusion about what's included in the book
  • Finds files that should be deleted or added to SUMMARY.md
  • Reduces repository size by identifying unused files

Examples

❌ Problematic Structure

src/
├── SUMMARY.md
├── introduction.md      ✓ (in SUMMARY.md)
├── chapter1.md         ✓ (in SUMMARY.md)
├── chapter2.md         ✓ (in SUMMARY.md)
├── old-chapter.md      ✗ (orphaned)
├── todo.md            ✗ (orphaned)
└── notes.md           ✗ (orphaned)

✅ Clean Structure

src/
├── SUMMARY.md
├── introduction.md      ✓ (in SUMMARY.md)
├── chapter1.md         ✓ (in SUMMARY.md)
├── chapter2.md         ✓ (in SUMMARY.md)
└── appendix.md         ✓ (in SUMMARY.md)

What This Rule Checks

The rule scans for:

  1. All .md files in the source directory
  2. Files referenced in SUMMARY.md
  3. Reports files not in SUMMARY.md as orphaned

Special Cases

Files that are not considered orphaned:

  • SUMMARY.md itself
  • README.md (often used as index)
  • Files in directories excluded by configuration
  • Files matching ignore patterns

Configuration

[MDBOOK005]
ignore_patterns = ["drafts/**", "*.backup.md"]  # Patterns to ignore
check_nested = true                             # Check subdirectories (default: true)
exclude_readme = true                           # Don't report README.md (default: true)

Common Scenarios

Scenario 1: Renamed File

You renamed a chapter but forgot to update SUMMARY.md:

# Old file still exists but not in SUMMARY.md
src/old-name.md  → orphaned
src/new-name.md  → in SUMMARY.md

Solution: Delete the old file or update SUMMARY.md

Scenario 2: Work in Progress

You're drafting new content not ready for inclusion:

src/draft-chapter.md  → orphaned (intentionally)

Solution: Move to a drafts folder or add ignore pattern

Scenario 3: Included Files

You have files that are included by other files:

src/snippets/example.md  → orphaned (but included via {{#include}})

Solution: Add to ignore patterns or move to non-source directory

Handling Orphaned Files

Option 1: Add to SUMMARY.md

# Summary

- [Existing Chapter](./existing.md)
- [Previously Orphaned](./orphaned.md)  <!-- Add this line -->

Option 2: Delete the File

rm src/orphaned-file.md

Option 3: Move Outside Source Directory

mkdir archived
mv src/orphaned.md archived/

Option 4: Add to Ignore Patterns

[MDBOOK005]
ignore_patterns = ["drafts/**", "work-in-progress.md"]

When to Disable

Consider disabling this rule if:

  • You intentionally keep reference files in the source directory
  • Your build process dynamically generates SUMMARY.md
  • You use many include files that aren't directly referenced
  • You're in the middle of major restructuring

Disable in Config

# .mdbook-lint.toml
disabled_rules = ["MDBOOK005"]

Best Practices

  1. Regular cleanup: Periodically review and remove orphaned files
  2. Use drafts folder: Keep work-in-progress in a separate directory
  3. Document intentional orphans: Add comments explaining why files are kept
  4. Version control: Check git history before deleting orphaned files

References

MDBOOK006 - Internal Cross-References

Internal cross-reference links must point to valid headings in target files.

Why This Rule Exists

Cross-references between chapters using anchor fragments must resolve to actual headings in the target file. Invalid fragments create broken navigation.

Examples

Incorrect

See the [configuration section](./config.md#settings) for details.

Where config.md has no ## Settings heading.

Correct

See the [configuration section](./config.md#configuration-options) for details.

Where config.md contains:

## Configuration Options

Content here.

How Fragments Are Generated

mdBook generates fragments from headings:

HeadingFragment
## Getting Started#getting-started
## API Reference#api-reference
## What's New?#whats-new

Configuration

This rule has no configuration options.

When to Disable

  • Books using custom anchor IDs
  • Content with JavaScript-based navigation

Rule Details

  • Rule ID: MDBOOK006
  • Aliases: internal-cross-references
  • Category: MdBook
  • Severity: Warning
  • Auto-fix: No
  • MD051 - Link fragments (same-file)
  • MDBOOK002 - Internal link validation

MDBOOK007 - Include Validation

Include directives must point to existing files with valid syntax.

Why This Rule Exists

mdBook's {{#include}} directive embeds content from other files. Invalid paths or syntax cause build failures or missing content.

Examples

Incorrect

{{#include missing-file.rs}}

{{#include ../src/lib.rs:nonexistent_anchor}}

\{{include src/main.rs}}  <!-- Missing # -->

Correct

{{#include ../src/lib.rs}}

{{#include ../src/lib.rs:main_function}}

{{#include ./snippets/example.rs:5:10}}

Include Syntax

<!-- Full file -->
{{#include path/to/file.rs}}

<!-- Line range -->
{{#include path/to/file.rs:5:10}}

<!-- From line to end -->
{{#include path/to/file.rs:5:}}

<!-- Named anchor -->
{{#include path/to/file.rs:anchor_name}}

Configuration

This rule has no configuration options.

When to Disable

  • Files with includes resolved at a different build stage
  • Templates with dynamic include paths

Rule Details

  • Rule ID: MDBOOK007
  • Aliases: include-validation
  • Category: MdBook
  • Severity: Error
  • Auto-fix: No

MDBOOK008 - Rustdoc Include Validation

Invalid {{#rustdoc_include}} paths or syntax.

Why This Rule Exists

The {{#rustdoc_include}} directive is similar to \{{#include}} but hides lines starting with # (used for rustdoc hidden lines). Invalid paths or syntax cause build failures.

Examples

Incorrect

{{#rustdoc_include missing-file.rs}}

{{#rustdoc_include ../src/lib.rs:bad_anchor}}

\{{rustdoc_include src/main.rs}}  <!-- Missing # -->

Correct

{{#rustdoc_include ../src/lib.rs}}

{{#rustdoc_include ../src/lib.rs:example}}

{{#rustdoc_include ./snippets/demo.rs:5:20}}

Rustdoc Include Syntax

<!-- Full file, hiding # lines -->
{{#rustdoc_include path/to/file.rs}}

<!-- Line range -->
{{#rustdoc_include path/to/file.rs:5:10}}

<!-- Named anchor -->
{{#rustdoc_include path/to/file.rs:anchor_name}}

Hidden Lines

In the source file, lines starting with # are hidden:

fn main() {
println!("This line is visible");
}

Renders as just:

#![allow(unused)]
fn main() {
println!("This line is visible");
}

Configuration

This rule has no configuration options.

Rule Details

  • Rule ID: MDBOOK008
  • Aliases: rustdoc-include-validation
  • Category: MdBook
  • Severity: Error
  • Stability: Experimental
  • Auto-fix: No

MDBOOK009 - Playground Validation

Invalid {{#playground}} configuration.

Why This Rule Exists

The {{#playground}} directive creates interactive Rust code examples. Invalid paths or configuration cause build failures or non-functional playgrounds.

Examples

Incorrect

{{#playground missing-file.rs}}

{{#playground ../src/example.rs invalid_option}}

\{{playground src/demo.rs}}  <!-- Missing # -->

Correct

{{#playground ../src/example.rs}}

{{#playground ../src/example.rs editable}}

{{#playground ../src/example.rs editable hide_lines=1-3}}

Playground Options

<!-- Basic playground -->
{{#playground path/to/file.rs}}

<!-- Editable playground -->
{{#playground path/to/file.rs editable}}

<!-- Hide specific lines -->
{{#playground path/to/file.rs hide_lines=1-3}}

<!-- Multiple options -->
{{#playground path/to/file.rs editable no_run}}

Available Options

OptionDescription
editableAllow users to edit the code
no_runShow code but disable running
ignoreDon't test this code
hide_linesHide specific line ranges

Configuration

This rule has no configuration options.

Rule Details

  • Rule ID: MDBOOK009
  • Aliases: playground-validation
  • Category: MdBook
  • Severity: Warning
  • Stability: Experimental
  • Auto-fix: No

MDBOOK010 - Preprocessor Validation

Missing or invalid preprocessor configuration.

Why This Rule Exists

mdBook preprocessors transform content before rendering. Using preprocessor directives without proper configuration causes silent failures or build errors.

Examples

Incorrect

Using a directive without configuring the preprocessor:

{{#katex}}
E = mc^2
\{{/katex}}

Without [preprocessor.katex] in book.toml.

Correct

First, configure in book.toml:

[preprocessor.katex]

Then use the directive:

{{#katex}}
E = mc^2
\{{/katex}}

Common Preprocessors

PreprocessorPurpose
katexMath equations
mermaidDiagrams
tocTable of contents
templateTemplate expansion
admonishCallout boxes

Configuration

This rule has no configuration options.

Rule Details

  • Rule ID: MDBOOK010
  • Aliases: preprocessor-validation
  • Category: MdBook
  • Severity: Warning
  • Stability: Experimental
  • Auto-fix: No

MDBOOK011 - Template Validation

Invalid {{#template}} syntax.

Why This Rule Exists

The {{#template}} directive expands templates with variable substitution. Invalid syntax or missing variables cause build failures.

Examples

Incorrect

{{#template missing-template.md}}

{{#template ./template.md var1=value}}  <!-- Missing closing -->

\{{template ./template.md}}  <!-- Missing # -->

Correct

{{#template ./templates/note.md}}

{{#template ./templates/warning.md title="Important" content="Read carefully"}}

Template Syntax

<!-- Basic template -->
{{#template path/to/template.md}}

<!-- With variables -->
{{#template path/to/template.md var1="value1" var2="value2"}}

Template File

<!-- templates/note.md -->
> **\{{title}}**
>
> \{{content}}

Usage

{{#template templates/note.md title="Note" content="This is important."}}

Configuration

This rule has no configuration options.

Rule Details

  • Rule ID: MDBOOK011
  • Aliases: template-validation
  • Category: MdBook
  • Severity: Warning
  • Stability: Experimental
  • Auto-fix: No

MDBOOK012 - Include Line Range Validation

Broken {{#include}} line ranges.

Why This Rule Exists

Include directives with line ranges must reference valid line numbers. Ranges that exceed the file length or have invalid syntax cause build failures or unexpected content.

Examples

Incorrect

<!-- File has only 50 lines -->
{{#include ../src/lib.rs:100:150}}

<!-- Invalid range (end before start) -->
{{#include ../src/lib.rs:20:10}}

<!-- Non-numeric range -->
{{#include ../src/lib.rs:start:end}}

Correct

{{#include ../src/lib.rs:1:10}}

{{#include ../src/lib.rs:5:}}

{{#include ../src/lib.rs::20}}

Line Range Syntax

<!-- Lines 5 through 10 -->
{{#include file.rs:5:10}}

<!-- Line 5 to end of file -->
{{#include file.rs:5:}}

<!-- Start of file through line 10 -->
{{#include file.rs::10}}

<!-- Single line (line 5 only) -->
{{#include file.rs:5:5}}

Configuration

This rule has no configuration options.

Rule Details

  • Rule ID: MDBOOK012
  • Aliases: include-line-range-validation
  • Category: MdBook
  • Severity: Error
  • Stability: Experimental
  • Auto-fix: No

MDBOOK016 - Rust Code Block Attributes

Rust code blocks should use valid mdBook/rustdoc attributes.

Why This Rule Exists

Rust code blocks accept a comma-separated list of attributes after the rust language tag, such as rust,ignore or rust,should_panic. These attributes control how mdBook and rustdoc treat the block when testing the book. An unrecognized attribute is usually a typo that silently does nothing instead of producing the intended behavior.

Examples

Incorrect

```rust,invalid_attr
fn main() {}
```
```rust,shouldpanic
fn main() { panic!(); }
```

Correct

```rust,ignore
fn main() {}
```
```rust,should_panic
fn main() { panic!(); }
```
```rust,no_run
fn main() {}
```
```rust,ignore,editable
fn main() {}
```

When the attribute is a recognized typo, such as shouldpanic, norun, or compilefail, the violation message suggests the correct spelling.

Valid Attributes

  • ignore, noplayground, noplaypen, mdbook-runnable, editable
  • hidelines (and hidelines=<prefix>)
  • should_panic, no_run, compile_fail
  • edition2015, edition2018, edition2021, edition2024
  • rust, rs, text, plain

The last group is a set of bare language identifiers that the rule accepts in an attribute position instead of reporting them, so rust,text and rust,plain produce no violation. rust and rs are skipped wherever they appear in the list, not only as the leading tag.

Non-Rust code blocks (python, javascript, and so on) are not checked by this rule. Only blocks tagged rust or rs are validated.

Configuration

This rule has no configuration options.

Rule Details

  • Rule ID: MDBOOK016
  • Aliases: rust-code-block-attributes
  • Category: MdBook
  • Severity: Warning
  • Auto-fix: No
  • MDBOOK017 - Rust code block hidden lines
  • MD040 - Fenced code blocks should have a language specified

MDBOOK017 - Hidden Code Prefix

Rust code blocks should use # to hide boilerplate from readers.

Why This Rule Exists

In mdBook, lines in a Rust code block that start with # are hidden from the rendered page but still compiled and run when the example is tested. Hiding setup boilerplate such as use statements and fn main() {} wrappers keeps the reader focused on the part of the example that actually matters, while the example still compiles as real, tested code.

This rule looks for common boilerplate lines in fenced Rust blocks and flags them when the block doesn't hide anything at all, on the assumption that an author who hasn't used # anywhere in the block probably isn't hiding lines on purpose.

Examples

Incorrect

```rust
use std::collections::HashMap;

fn main() {
    let map: HashMap<i32, i32> = HashMap::new();
}
```

Correct

```rust
# use std::collections::HashMap;
# fn main() {
let map: HashMap<i32, i32> = HashMap::new();
# }
```

The hidden lines still compile, but a reader viewing the rendered book only sees:

#![allow(unused)]
fn main() {
let map: HashMap<i32, i32> = HashMap::new();
}

Already Aware of Hidden Lines

If the block hides at least one line, the rule assumes the author knows about the feature and leaves the rest of the block alone, even if other boilerplate in the same block is left visible:

```rust
# use std::io;
fn main() {
    println!("Hello");
}
```

Boilerplate Patterns

The rule flags a line when it starts with one of these patterns and the surrounding block has no hidden (#-prefixed) lines:

  • use std::
  • use crate::
  • extern crate
  • fn main() { / fn main(){
  • pub fn main() {
  • async fn main() {
  • #![allow(, #![deny(, #![warn(, #![feature(

Lines starting with #[ (attributes like #[derive(Debug)]) and #! (inner attributes) are not treated as hidden-line markers, since those are ordinary visible Rust syntax rather than mdBook's hiding convention.

Configuration

This rule has no configuration options.

Rule Details

  • Rule ID: MDBOOK017
  • Aliases: hidden-code-prefix
  • Category: MdBook
  • Severity: Info
  • Stability: Stable
  • Auto-fix: No

MDBOOK021 - Single Title Directive Per Chapter

{{#title}} directive should appear only once per chapter.

Why This Rule Exists

The {{#title}} directive sets the page title shown in the browser tab. When a chapter contains more than one, mdBook doesn't merge or reject the extras: only one directive takes effect (usually the last one), and the others sit in the file doing nothing, misleading anyone editing the chapter about which title is actually in effect.

Examples

Incorrect

{{#title First Title}}

# Chapter

{{#title Second Title}}

Content.

The directive on line 5 is flagged as a duplicate, with the message pointing back to the first occurrence on line 1.

{{#title First}}
{{#title Second}}
{{#title Third}}

Every occurrence after the first is flagged, so this reports two violations.

Correct

{{#title My Page Title}}

# Chapter Title

Content.

A chapter with no {{#title}} directive at all is also correct; this rule only fires when more than one directive is present.

Scope

This rule does not skip fenced code blocks. It scans each raw line for the directive pattern, so a {{#title}} directive shown as an example inside a code block is still counted; the rule takes that position on the grounds that mdBook's preprocessor would otherwise process it there too.

Escaping does not exempt an example. The pattern the rule matches, \{\{#title\s+[^}]+\}\}, makes no allowance for a preceding backslash, so an escaped directive still matches, starting at the {{ and reported one column to the right of an unescaped one. Escaping keeps mdBook from rendering the directive, but this rule counts it either way. The escaped examples on this page are counted, and linting this page reports MDBOOK021 for each of them after the first.

Matching requires text after #title before the closing }}, so a bare mention like `{{#title}}` in prose is not treated as a directive and is not counted.

Configuration

This rule has no configuration options.

Rule Details

  • Rule ID: MDBOOK021
  • Aliases: single-title-directive
  • Category: MdBook
  • Severity: Warning
  • Stability: Stable
  • Auto-fix: No

MDBOOK022 - Title Directive Near Top

{{#title}} directive should appear near the top of the file.

Why This Rule Exists

The {{#title}} directive sets the page title shown in the browser tab. mdBook processes it wherever it appears, but placing it far from the top of the chapter makes it easy to miss and inconsistent with chapters that declare their title up front. Keeping it within the first few lines makes the chapter's title easy to find when skimming or editing the file.

Examples

Incorrect

# Chapter

Paragraph 1.

Paragraph 2.

{{#title Late Title}}

Content.

The directive is on line 7, past the default threshold of line 5.

Correct

{{#title My Page Title}}

# Chapter Title

Content.
# Chapter

Intro paragraph.

{{#title My Title}}

More content.

The second example places the directive on line 5, which is still within the default threshold. A chapter with no {{#title}} directive at all is also correct; this rule only checks the position of the first directive found.

Configuration

[MDBOOK022]
# Highest line number at which a {{#title}} directive is still considered
# "near the top" of the file.
max_line = 5

max-line (kebab-case) is also accepted as an alias for max_line.

Rule Details

  • Rule ID: MDBOOK022
  • Aliases: title-near-top
  • Category: MdBook
  • Severity: Warning
  • Stability: Stable
  • Auto-fix: No
  • MDBOOK021 - Single title directive per chapter
  • MDBOOK023 - Chapter title matching: the link text of a chapter entry in SUMMARY.md should match the H1 header of the linked file. It does not look at {{#title}} directives, and it only runs on SUMMARY.md.

MDBOOK023 - Chapter Title Matching

The link title used for a chapter in SUMMARY.md should match the H1 header in the linked file.

Why This Rule Exists

mdBook's navigation sidebar shows the title from SUMMARY.md, while the page itself opens with its own H1. When the two disagree, readers see one title in the sidebar and land on a page that calls itself something else, which reads as a broken or stale link even though the link itself works fine.

Examples

Incorrect

src/SUMMARY.md:

# Summary

- [Getting Started](intro.md)

src/intro.md:

# Introduction to the Project

Welcome!

The sidebar says "Getting Started" but the page opens with "Introduction to the Project".

Correct

src/SUMMARY.md:

# Summary

- [Getting Started](intro.md)

src/intro.md:

# Getting Started

Welcome!

Matching is case-insensitive and normalizes whitespace, so # getting started and # Getting Started are both accepted against a SUMMARY.md entry of Getting Started.

Configuration

This rule has no configuration options.

When to Disable

  • Chapters that intentionally use a shorter or reframed title in the navigation than in the page's own heading

Rule Details

  • Rule ID: MDBOOK023
  • Aliases: chapter-title-match
  • Category: MdBook
  • Severity: Warning
  • Auto-fix: No

Scope

This rule only checks SUMMARY.md. For each chapter link it finds, it resolves the linked file relative to the book's source directory and compares the link text to that file's first H1 header:

  • Draft chapters ([Title](), empty path) are skipped.
  • External links (http://, https://) and anchor-only links (#section) are skipped.
  • If the linked file doesn't exist, that's reported by MDBOOK002, not here.
  • If the linked file has no H1 header at all, that's reported by MD041, not here — this rule only compares titles when both sides are present.
  • MDBOOK002 - Invalid internal link
  • MDBOOK021 - Single title directive per chapter
  • MDBOOK022 - Title directive near top
  • MD041 - First line in a file should be a top-level heading

MDBOOK025 - Multiple H1 Headings Allowed in SUMMARY.md

Severity: Info
Category: mdBook-specific
Auto-fix: Not available

Rule Description

This rule specifically allows multiple H1 headings in SUMMARY.md files while still enforcing the single H1 rule (MD025) in regular chapter files. SUMMARY.md uses H1 headings to define part separators in the book structure.

Why This Rule Exists

SUMMARY.md has special requirements because:

  • H1 headings define book parts/sections
  • Multiple parts are common in large books
  • mdBook treats these H1s as structural elements
  • They don't represent document headings but navigation structure
  • Standard MD025 rule would incorrectly flag valid SUMMARY.md files

Examples

✅ Correct SUMMARY.md Structure

# Summary

[Introduction](./introduction.md)

# Part I: Getting Started

- [Installation](./chapter1/installation.md)
- [Configuration](./chapter1/configuration.md)

# Part II: User Guide

- [Basic Usage](./chapter2/basic-usage.md)
- [Advanced Features](./chapter2/advanced.md)

# Part III: Reference

- [API Documentation](./chapter3/api.md)
- [Configuration Reference](./chapter3/config-ref.md)

---

[Appendix A](./appendix-a.md)
[Appendix B](./appendix-b.md)

❌ What This Rule Prevents

In regular chapter files (not SUMMARY.md):

# First Heading

Content...

# Second H1 Heading  <!-- MD025 violation in regular files -->

More content...

SUMMARY.md Structure Rules

Part Headers

  • H1 headings (# Part Name) create part divisions
  • Parts group related chapters
  • Part headers appear in the rendered navigation
  • No limit on number of parts

Special Elements

# Summary                         <!-- Required first line -->

[Prefix Chapter](./preface.md)   <!-- Before numbered chapters -->

# Part Name                       <!-- Part header -->

- [Chapter](./ch.md)              <!-- Numbered chapters -->
  - [Section](./sect.md)          <!-- Nested chapters -->

---                               <!-- Separator -->

[Suffix Chapter](./appendix.md)  <!-- After numbered chapters -->

Configuration

[MDBOOK025]
# This rule has no configuration options
# It automatically applies only to SUMMARY.md

How It Works

This rule:

  1. Detects if the file is SUMMARY.md
  2. Allows multiple H1 headings in SUMMARY.md
  3. Defers to MD025 for all other files
  4. Validates proper SUMMARY.md structure

Common Patterns

Book with Multiple Parts

# Summary

[Preface](./preface.md)

# Part I: Fundamentals

- [Chapter 1](./ch1.md)
- [Chapter 2](./ch2.md)

# Part II: Intermediate

- [Chapter 3](./ch3.md)
- [Chapter 4](./ch4.md)

# Part III: Advanced

- [Chapter 5](./ch5.md)
- [Chapter 6](./ch6.md)

Book without Parts

# Summary

[Introduction](./intro.md)

- [Chapter 1](./ch1.md)
- [Chapter 2](./ch2.md)
- [Chapter 3](./ch3.md)

---

[Conclusion](./conclusion.md)

Mixed Structure

# Summary

- [Getting Started](./start.md)

# Core Concepts

- [Fundamentals](./fundamentals.md)
- [Architecture](./architecture.md)

# Advanced Topics

- [Performance](./performance.md)
- [Security](./security.md)

---

[Glossary](./glossary.md)

Best Practices

  1. Use parts for organization: Group related chapters
  2. Keep part names concise: They appear in navigation
  3. Order matters: Parts appear in sequence
  4. Be consistent: Use similar naming patterns
  5. Consider reader flow: Logical progression through parts

Part Naming Conventions

<!-- Numbered parts -->
# Part I: Introduction
# Part II: Core Concepts
# Part III: Advanced Topics

<!-- Descriptive parts -->
# Getting Started
# User Guide
# API Reference
# Appendices

<!-- Module-based -->
# Core Modules
# Extension Modules
# Utility Modules

Interaction with Other Rules

Works With

  • MDBOOK003: Validates overall SUMMARY.md structure
  • MD022: Headings surrounded by blank lines
  • MD026: No trailing punctuation in headings

Overrides

  • MD025: Multiple top-level headings (in SUMMARY.md only)

When to Disable

Consider disabling this rule if:

  • You use a custom book structure
  • You have a different table of contents format
  • You don't use SUMMARY.md
  • You prefer strict MD025 enforcement everywhere

Disable in Config

# .mdbook-lint.toml
disabled_rules = ["MDBOOK025"]

# Or disable both MDBOOK025 and MD025
disabled_rules = ["MDBOOK025", "MD025"]

Tips

  1. Part headers are optional: Not every book needs parts
  2. Unnumbered chapters: Can exist before first part
  3. Separator sections: Use --- for appendices
  4. Draft chapters: Use [Chapter]() for placeholders
  5. Nested structure: Indent with 2 or 4 spaces consistently
  • MDBOOK003 - SUMMARY.md structure validation
  • MD025 - Multiple top-level headings (general rule)
  • MD001 - Heading levels should increment

References

ADR (Architecture Decision Record) Rules

These rules validate Architecture Decision Records (ADRs) against the Nygard format and MADR 4.0 format, ensuring consistency and completeness in your architectural documentation.

Rules

Structure Rules

Rule IDNameDescription
ADR001adr-title-formatTitle follows appropriate format for ADR type
ADR002adr-required-statusStatus is defined (section or frontmatter)
ADR003adr-required-dateDate is defined (line or frontmatter)
ADR004adr-required-contextContext section is present
ADR005adr-required-decisionDecision section is present
ADR006adr-required-consequencesConsequences section is present (Nygard only)

Validation Rules

Rule IDNameDescription
ADR007adr-valid-statusStatus value is recognized
ADR008adr-date-formatDate follows ISO 8601 format
ADR009adr-filename-matches-numberFilename matches ADR number (Nygard only)

Collection Rules (Multi-Document)

Rule IDNameDescription
ADR010adr-superseded-has-replacementSuperseded ADRs reference replacement
ADR011adr-sequential-numberingADR numbers are sequential with no gaps
ADR012adr-no-duplicate-numbersEach ADR number is unique
ADR013adr-valid-adr-linksLinks to other ADRs point to existing files

Content Quality Rules

Rule IDNameDescription
ADR014adr-non-empty-sectionsRequired sections should have meaningful content
ADR015adr-decision-drivers-formatDecision Drivers should be a bullet list (MADR)
ADR016adr-considered-options-formatConsidered Options should list at least 2 options
ADR017adr-consequences-structureConsequences should distinguish good/bad outcomes (MADR)

Supported Formats

Nygard Format

The original ADR format proposed by Michael Nygard. Key characteristics:

  • Title: # N. Title (e.g., # 1. Record architecture decisions)
  • Date: Date: YYYY-MM-DD line after the title
  • Status: ## Status section with status value
  • Required sections: Context, Decision, Consequences
# 1. Record architecture decisions

Date: 2024-01-15

## Status

Accepted

## Context

We need to record the architectural decisions made on this project.

## Decision

We will use Architecture Decision Records, as described by Michael Nygard.

## Consequences

See Michael Nygard's article for more details.

MADR 4.0 Format

Markdown Any Decision Records (MADR) version 4.0 uses YAML frontmatter for metadata and a different structure:

  • YAML frontmatter with status and date fields
  • Simple H1 title (no number prefix required)
  • Different section names (Context and Problem Statement, Decision Outcome)
---
status: accepted
date: 2024-01-15
decision-makers:
  - Alice Smith
consulted:
  - Bob Jones
---

# Use PostgreSQL for persistence

## Context and Problem Statement

We need to select a database for our application.

## Decision Drivers

* Need ACID compliance
* Team familiarity with SQL

## Considered Options

* PostgreSQL
* MySQL
* MongoDB

## Decision Outcome

Chosen option: PostgreSQL, because it provides ACID compliance
and the team has extensive SQL experience.

### Consequences

* Good, because mature ecosystem
* Bad, because requires operational overhead

Format Detection

The rules automatically detect the ADR format based on:

  1. YAML frontmatter present - MADR 4.0 format
  2. No frontmatter, numbered title - Nygard format
  3. Path contains /adr/ or /adrs/ - Treated as ADR document

Configuration

Configure ADR rules in your .mdbook-lint.toml:

# Enable all ADR rules (they're enabled by default)
[rules]
"ADR*" = true

# Configure valid status values
[ADR007]
valid-statuses = ["proposed", "accepted", "deprecated", "superseded", "rejected"]

# Customize minimum options for Considered Options
[ADR016]
min-options = 2

Why ADR Rules Matter

Architecture Decision Records are critical for:

  1. Knowledge Transfer: New team members understand past decisions
  2. Decision Quality: Forces structured thinking about alternatives
  3. Accountability: Documents who made decisions and why
  4. Reversibility: Makes it clear when to revisit decisions
  5. Consistency: Ensures all ADRs follow the same format

Common Issues

Missing Status

Problem: ADR doesn't indicate its current status.

# 1. Use Rust

Date: 2024-01-15

## Context

We need a language.

Solution: Add a Status section.

# 1. Use Rust

Date: 2024-01-15

## Status

Proposed

Placeholder Content

Problem: Sections contain placeholder text instead of real content.

## Context

TODO: Fill in context

Solution: Write meaningful content or mark the ADR as draft.

Superseded Without Reference

Problem: ADR is superseded but doesn't link to the replacement.

## Status

Superseded

Solution: Reference the new ADR.

## Status

Superseded by [ADR-0005](0005-use-kubernetes.md)

Integration with CI/CD

Validate ADRs in your pipeline:

# .github/workflows/adr-check.yml
name: ADR Validation

on: [push, pull_request]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Install mdbook-lint
        run: cargo install mdbook-lint
      - name: Validate ADRs
        run: mdbook-lint lint docs/adr/*.md --enable "ADR*"

Best Practices

  1. Number ADRs sequentially: Don't reuse numbers, even for rejected ADRs
  2. Keep ADRs immutable: Create new ADRs to supersede old ones
  3. Link related ADRs: Reference related decisions
  4. Include context: Future readers need to understand the situation
  5. Document alternatives: Show what was considered and why it was rejected
  6. Update status promptly: Keep the status current

References

ADR001 - Title Format

ADR titles should follow the appropriate format for the document type.

Why This Rule Exists

Consistent title formatting makes ADRs:

  • Easy to identify and reference
  • Sortable by number
  • Recognizable as ADRs in search results

Formats

Nygard Format

Titles must follow the pattern # N. Title where N is the ADR number:

# 1. Record architecture decisions
# 42. Use PostgreSQL for persistence

MADR Format

Titles should be a simple H1 heading (no number required):

# Use PostgreSQL for persistence

Examples

Incorrect (Nygard)

# Use Rust for implementation

Missing number prefix.

Correct (Nygard)

# 1. Use Rust for implementation

Incorrect (MADR)

Missing H1 title entirely or using wrong heading level.

Correct (MADR)

---
status: accepted
date: 2024-01-15
---

# Use Rust for implementation

Rule Details

  • Rule ID: ADR001
  • Name: adr-title-format
  • Category: Structure
  • Severity: Error
  • Automatic Fix: Not available

Configuration

This rule has no configuration options. Format is auto-detected based on document content.

  • ADR009 - Filename should match ADR number

ADR002 - Required Status

ADRs must have a status indicating the decision's current state.

Why This Rule Exists

Status indicates whether a decision is:

  • Proposed: Under consideration
  • Accepted: Approved and in effect
  • Deprecated: No longer recommended
  • Superseded: Replaced by another ADR

Without status, readers cannot determine if the decision is current.

Formats

Nygard Format

Status is a ## Status section:

## Status

Accepted

MADR Format

Status is in YAML frontmatter:

---
status: accepted
date: 2024-01-15
---

Examples

Incorrect

# 1. Use Rust

Date: 2024-01-15

## Context

We need to choose a language.

Missing Status section.

Correct (Nygard)

# 1. Use Rust

Date: 2024-01-15

## Status

Accepted

## Context

We need to choose a language.

Correct (MADR)

---
status: accepted
date: 2024-01-15
---

# Use Rust

## Context and Problem Statement

We need to choose a language.

Rule Details

  • Rule ID: ADR002
  • Name: adr-required-status
  • Category: Structure
  • Severity: Error
  • Automatic Fix: Not available
  • ADR007 - Status value validation

ADR003 - Required Date

ADRs must include the date when the decision was made.

Why This Rule Exists

Dates provide:

  • Historical context for when decisions were made
  • Timeline for understanding decision evolution
  • Reference for when to revisit decisions

Formats

Nygard Format

Date appears on a line after the title:

# 1. Use Rust

Date: 2024-01-15

MADR Format

Date is in YAML frontmatter:

---
status: accepted
date: 2024-01-15
---

Examples

Incorrect

# 1. Use Rust

## Status

Accepted

Missing date.

Correct (Nygard)

# 1. Use Rust

Date: 2024-01-15

## Status

Accepted

Correct (MADR)

---
status: accepted
date: 2024-01-15
---

# Use Rust

Rule Details

  • Rule ID: ADR003
  • Name: adr-required-date
  • Category: Structure
  • Severity: Error
  • Automatic Fix: Not available
  • ADR008 - Date format validation

ADR004 - Required Context Section

ADRs must have a context section explaining the situation that led to the decision.

Why This Rule Exists

Context provides:

  • Background for understanding the decision
  • The problem being solved
  • Constraints and requirements that influenced the choice

Formats

Nygard Format

A ## Context section:

## Context

We need to choose a programming language for our new microservice.
The team has experience with Java, Python, and Rust.

MADR Format

A ## Context and Problem Statement section:

## Context and Problem Statement

We need to choose a programming language for our new microservice.
What language should we use given our team's skills and project requirements?

Examples

Incorrect

# 1. Use Rust

Date: 2024-01-15

## Status

Accepted

## Decision

We will use Rust.

Missing Context section.

Correct (Nygard)

# 1. Use Rust

Date: 2024-01-15

## Status

Accepted

## Context

We need to choose a programming language for our new microservice.

## Decision

We will use Rust.

Rule Details

  • Rule ID: ADR004
  • Name: adr-required-context
  • Category: Structure
  • Severity: Error
  • Automatic Fix: Not available
  • ADR014 - Context should have meaningful content

ADR005 - Required Decision Section

ADRs must have a decision section stating what was decided.

Why This Rule Exists

The decision section:

  • Clearly states what was chosen
  • Makes the outcome unambiguous
  • Provides the actionable result

Formats

Nygard Format

A ## Decision section:

## Decision

We will use Rust for the new microservice implementation.

MADR Format

A ## Decision Outcome section:

## Decision Outcome

Chosen option: "Rust", because it provides memory safety without garbage collection
and has excellent performance characteristics.

Examples

Incorrect

# 1. Use Rust

Date: 2024-01-15

## Status

Accepted

## Context

We need a language.

## Consequences

Team training needed.

Missing Decision section.

Correct (Nygard)

## Decision

We will use Rust for the new microservice implementation.

Correct (MADR)

## Decision Outcome

Chosen option: "Rust", because it provides memory safety and performance.

Rule Details

  • Rule ID: ADR005
  • Name: adr-required-decision
  • Category: Structure
  • Severity: Error
  • Automatic Fix: Not available
  • ADR014 - Decision should have meaningful content

ADR006 - Required Consequences Section

Nygard-format ADRs must have a consequences section describing the impact of the decision.

Why This Rule Exists

Consequences help teams:

  • Understand trade-offs made
  • Anticipate challenges
  • Plan for implications
  • Make informed future decisions

Format

This rule only applies to Nygard format ADRs. MADR format uses structured consequences under Decision Outcome (see ADR017).

## Consequences

Positive:
- Memory safety without garbage collection
- High performance

Negative:
- Steeper learning curve
- Longer compile times initially

Examples

Incorrect

# 1. Use Rust

Date: 2024-01-15

## Status

Accepted

## Context

We need a language.

## Decision

We will use Rust.

Missing Consequences section.

Correct

# 1. Use Rust

Date: 2024-01-15

## Status

Accepted

## Context

We need a language for the new service.

## Decision

We will use Rust.

## Consequences

Team members will need Rust training.
Build times may be longer initially.
Memory safety issues will be caught at compile time.

Rule Details

  • Rule ID: ADR006
  • Name: adr-required-consequences
  • Category: Structure
  • Severity: Warning
  • Automatic Fix: Not available
  • Applies to: Nygard format only
  • ADR014 - Consequences should have meaningful content
  • ADR017 - Consequences structure (MADR)

ADR007 - Valid Status Value

ADR status must be a recognized value.

Why This Rule Exists

Standard status values ensure:

  • Consistent interpretation across teams
  • Clear lifecycle management
  • Tooling compatibility

Valid Status Values

Default valid statuses (case-insensitive):

  • proposed - Under consideration
  • accepted - Approved and active
  • deprecated - No longer recommended
  • superseded - Replaced by another ADR
  • rejected - Considered but not adopted

Examples

Incorrect

## Status

Maybe

## Status

In Progress

Correct

## Status

Accepted

## Status

Proposed

## Status

Superseded by ADR-0005

Configuration

Customize valid statuses:

[ADR007]
valid-statuses = ["proposed", "accepted", "deprecated", "superseded", "rejected", "draft"]

Rule Details

  • Rule ID: ADR007
  • Name: adr-valid-status
  • Category: Validation
  • Severity: Error
  • Automatic Fix: Not available
  • ADR002 - Status is required
  • ADR010 - Superseded ADRs should reference replacement

ADR008 - Date Format

ADR dates must follow ISO 8601 format (YYYY-MM-DD).

Why This Rule Exists

ISO 8601 format:

  • Is unambiguous internationally
  • Sorts correctly alphabetically
  • Is machine-readable
  • Follows industry standards

Format

Dates must be in YYYY-MM-DD format:

Date: 2024-01-15

Or in MADR frontmatter:

---
date: 2024-01-15
---

Examples

Incorrect

Date: January 15, 2024
Date: 15/01/2024
Date: 01-15-2024
Date: 2024/01/15

Correct

Date: 2024-01-15
Date: 2024-12-31
---
date: 2024-01-15
---

Rule Details

  • Rule ID: ADR008
  • Name: adr-date-format
  • Category: Validation
  • Severity: Error
  • Automatic Fix: Not available

ADR009 - Filename Matches Number

For Nygard-format ADRs, the filename number should match the ADR number in the title.

Why This Rule Exists

Matching filename and title numbers:

  • Makes ADRs easy to find
  • Prevents confusion and mismatch
  • Enables consistent file organization

Format

The filename pattern NNNN-title.md should match the title # NNNN. Title:

FilenameTitle
0001-use-rust.md# 1. Use Rust
0042-database-choice.md# 42. Database Choice

Examples

Incorrect

File: 0005-use-rust.md

# 1. Use Rust

Number in filename (5) doesn't match title (1).

Correct

File: 0001-use-rust.md

# 1. Use Rust

File: 0042-database-choice.md

# 42. Choose PostgreSQL

Rule Details

  • Rule ID: ADR009
  • Name: adr-filename-matches-number
  • Category: Validation
  • Severity: Error
  • Automatic Fix: Not available
  • Applies to: Nygard format only

ADR010 - Superseded ADRs Reference Replacement

ADRs with "Superseded" status should reference the ADR that replaces them.

Why This Rule Exists

When an ADR is superseded:

  • Readers need to know which ADR to follow instead
  • The decision history remains traceable
  • Teams can understand the evolution of decisions

Format

The status section should include a link to the replacement ADR:

## Status

Superseded by [ADR-0005](0005-use-kubernetes.md)

Or in MADR:

---
status: superseded
superseded-by: ADR-0005
---

Examples

Incorrect

## Status

Superseded

No reference to replacement ADR.

Correct

## Status

Superseded by [ADR-0005](0005-use-kubernetes.md)
## Status

Superseded by [0005-use-kubernetes.md](./0005-use-kubernetes.md)

Collection Rule

This rule analyzes multiple ADR documents together to validate cross-references.

Rule Details

  • Rule ID: ADR010
  • Name: adr-superseded-has-replacement
  • Category: Links
  • Severity: Warning
  • Type: Collection rule (multi-document)
  • Automatic Fix: Not available

ADR011 - Sequential Numbering

ADR numbers should be sequential with no gaps.

Why This Rule Exists

Sequential numbering:

  • Makes it easy to identify missing ADRs
  • Provides clear ordering
  • Indicates the decision timeline
  • Helps with navigation

Format

ADRs should be numbered starting from 1 (or 0) without gaps:

0001-record-decisions.md      # ADR 1
0002-use-rust.md              # ADR 2
0003-database-choice.md       # ADR 3

Examples

Incorrect

0001-record-decisions.md      # ADR 1
0003-database-choice.md       # ADR 3 (gap - where is 2?)
0004-use-kubernetes.md        # ADR 4

Correct

0001-record-decisions.md      # ADR 1
0002-use-rust.md              # ADR 2
0003-database-choice.md       # ADR 3
0004-use-kubernetes.md        # ADR 4

Collection Rule

This rule analyzes all ADR documents in a directory together to check for gaps.

Note on Rejected ADRs

Even rejected ADRs should keep their numbers. Don't delete or renumber ADRs:

0001-record-decisions.md      # Accepted
0002-use-java.md              # Rejected (keep it!)
0003-use-rust.md              # Accepted (supersedes thinking in 0002)

Rule Details

  • Rule ID: ADR011
  • Name: adr-sequential-numbering
  • Category: Structure
  • Severity: Warning
  • Type: Collection rule (multi-document)
  • Automatic Fix: Not available
  • ADR009 - Filename matches title number
  • ADR012 - No duplicate numbers

ADR012 - No Duplicate Numbers

Each ADR number must be unique across all ADRs.

Why This Rule Exists

Duplicate numbers cause:

  • Confusion about which ADR to reference
  • Broken cross-references
  • Difficulty navigating ADR history

Examples

Incorrect

0001-record-decisions.md      # ADR 1
0001-use-rust.md              # ADR 1 (duplicate!)
0002-database-choice.md       # ADR 2

Correct

0001-record-decisions.md      # ADR 1
0002-use-rust.md              # ADR 2
0003-database-choice.md       # ADR 3

Collection Rule

This rule analyzes all ADR documents in a directory together to detect duplicates.

Rule Details

  • Rule ID: ADR012
  • Name: adr-no-duplicate-numbers
  • Category: Structure
  • Severity: Error
  • Type: Collection rule (multi-document)
  • Automatic Fix: Not available
  • ADR009 - Filename matches title number
  • ADR011 - Sequential numbering

ADR013 - Valid ADR Links

Links to other ADR documents should point to existing files.

Why This Rule Exists

Broken ADR links:

  • Prevent readers from following decision history
  • Indicate missing or deleted ADRs
  • Create confusion in documentation

Format

Links to other ADRs should reference existing files:

## Status

Superseded by [ADR-0005](0005-use-kubernetes.md)

## Context

This builds on [ADR-0002](0002-container-strategy.md).

Examples

Incorrect

See [ADR-0099](0099-nonexistent.md) for details.

File 0099-nonexistent.md doesn't exist.

Correct

See [ADR-0002](0002-use-rust.md) for context.

File 0002-use-rust.md exists.

Collection Rule

This rule analyzes all ADR documents together to validate cross-references.

What's Checked

  • Links with .md extension in ADR directories
  • Relative paths are resolved from the source document
  • Both filename-only and path references

Rule Details

  • Rule ID: ADR013
  • Name: adr-valid-adr-links
  • Category: Links
  • Severity: Warning
  • Type: Collection rule (multi-document)
  • Automatic Fix: Not available
  • ADR010 - Superseded ADRs reference replacement
  • ADR011 - Sequential numbering (helps identify missing ADRs)

ADR014 - Non-Empty Sections

Required ADR sections should have meaningful content, not placeholders.

Why This Rule Exists

Placeholder content:

  • Provides no value to readers
  • Indicates incomplete documentation
  • May mislead about decision completeness

Placeholder Patterns Detected

  • Empty sections
  • TODO, TBD, To be determined
  • ... (ellipsis)
  • [Insert here], <Insert here>
  • Lorem ipsum
  • Fill in, Add content, Write here
  • Very short content (< 3 characters)

Examples

Incorrect

## Context

TODO: Fill in context later

## Decision

TBD

## Consequences

...

Correct

## Context

We need to choose a programming language for our new microservice.
The team has experience with Java, Python, and Rust. Performance
and memory safety are key requirements.

## Decision

We will use Rust for the new microservice implementation.

## Consequences

Team members will need Rust training, which will take 2-3 weeks.
Build times will be longer initially but runtime performance will improve.

Rule Details

  • Rule ID: ADR014
  • Name: adr-non-empty-sections
  • Category: Content
  • Severity: Warning
  • Automatic Fix: Not available
  • ADR004 - Required context section
  • ADR005 - Required decision section
  • ADR006 - Required consequences section

ADR015 - Decision Drivers Format

In MADR format, the Decision Drivers section should use a bullet list.

Why This Rule Exists

Bullet lists for decision drivers:

  • Clearly enumerate factors
  • Make drivers easy to scan
  • Enable consistent formatting
  • Help readers understand decision criteria

Format

Decision Drivers should be a bullet list:

## Decision Drivers

* Need ACID compliance for data integrity
* Team familiarity with SQL databases
* Strong ecosystem and tooling support
* Active community and long-term viability

Examples

Incorrect

## Decision Drivers

We need ACID compliance and team familiarity with SQL.
Also good tooling is important.

Paragraph text instead of bullet list.

Correct

## Decision Drivers

* Need ACID compliance for data integrity
* Team familiarity with SQL databases
* Strong ecosystem and tooling support

Or with dashes:

## Decision Drivers

- Need ACID compliance
- Team familiarity
- Good tooling

Rule Details

  • Rule ID: ADR015
  • Name: adr-decision-drivers-format
  • Category: Structure
  • Severity: Info
  • Applies to: MADR format only
  • Automatic Fix: Not available
  • ADR016 - Considered Options format

ADR016 - Considered Options Format

In MADR format, the Considered Options section should list at least 2 options.

Why This Rule Exists

Multiple options demonstrate:

  • Alternatives were evaluated
  • Due diligence was performed
  • The decision wasn't predetermined
  • Trade-offs were considered

Format

List at least 2 options as bullets or H3 headings:

## Considered Options

* PostgreSQL
* MySQL
* MongoDB

Or with detailed subsections:

## Considered Options

### PostgreSQL

A mature relational database with excellent SQL support.

### MySQL

A widely-used relational database with good performance.

### MongoDB

A document database for flexible schemas.

Examples

Incorrect

## Considered Options

* PostgreSQL

Only one option listed.

Correct

## Considered Options

* PostgreSQL
* MySQL
* MongoDB

Configuration

Customize minimum options:

[ADR016]
min-options = 2  # Default

Rule Details

  • Rule ID: ADR016
  • Name: adr-considered-options-format
  • Category: Content
  • Severity: Info
  • Applies to: MADR format only
  • Automatic Fix: Not available
  • ADR015 - Decision Drivers format
  • ADR017 - Consequences structure

ADR017 - Consequences Structure

In MADR format, the Consequences section should distinguish good and bad outcomes.

Why This Rule Exists

Structured consequences:

  • Clarify trade-offs explicitly
  • Help teams anticipate challenges
  • Make positive and negative impacts visible
  • Support informed decision-making

Formats

Option 1: Good/Bad Markers

Use "Good, because..." and "Bad, because..." format:

### Consequences

* Good, because it provides ACID compliance
* Good, because team has SQL experience
* Bad, because requires more operational overhead
* Neutral, because licensing costs are similar

Option 2: Separate Sections

Use separate positive/negative sections:

### Positive Consequences

* ACID compliance for data integrity
* Team familiarity reduces learning curve

### Negative Consequences

* Higher operational overhead
* More complex deployment

Examples

Incorrect

### Consequences

* Provides ACID compliance
* Team has SQL experience
* Requires more operational overhead

No distinction between good and bad outcomes.

Correct

### Consequences

* Good, because it provides ACID compliance
* Good, because team has SQL experience
* Bad, because requires more operational overhead

Or:

### Positive Consequences

* ACID compliance
* Team familiarity

### Negative Consequences

* Operational overhead

Rule Details

  • Rule ID: ADR017
  • Name: adr-consequences-structure
  • Category: Content
  • Severity: Info
  • Applies to: MADR format only
  • Automatic Fix: Not available
  • ADR006 - Required consequences (Nygard)
  • ADR014 - Non-empty sections

Content Rules

Content rules look at what a page says rather than how it is formatted. They catch documentation that is unfinished, inconsistent, or hard to follow: text left over from drafting, headings that disagree with each other about capitalization, links whose text says nothing, and terminology that changes from one page to the next.

None of them are enabled by default. They express editorial preferences rather than correctness, so a book adopts the ones it agrees with.

Rules

RuleChecks
CONTENT001TODO, FIXME and similar markers left in prose
CONTENT002Placeholder text such as lorem ipsum
CONTENT003Chapters too short to be worth a page
CONTENT004Heading capitalization that varies within a file
CONTENT005A heading followed straight by a subheading, with nothing in between
CONTENT006Anchor links pointing at headings that are not in the file
CONTENT007The same idea named differently in different places
CONTENT009Headings nested deeper than a reader will follow
CONTENT010Link text that does not say where it goes
CONTENT011Future tense describing what the software already does

Enabling them

[rules]
enabled = ["CONTENT001", "CONTENT010"]

See Configuration for the full syntax.

CONTENT001 - No TODO Comments

TODO, FIXME, and similar work-in-progress markers should be resolved before publishing.

Why This Rule Exists

Markers like TODO, FIXME, XXX, HACK, and WIP are useful while drafting, but left in published documentation they read as unfinished work and erode trust in the content. This rule flags them so they get resolved (or deliberately dismissed) before a book ships.

Examples

Incorrect

TODO: Add more content here.

FIXME: This section needs work.

XXX: Review this section.

WIP: Work in progress section.

<!-- TODO: Add content -->

Correct

This section documents the installation process in full.

<!-- Reviewed and complete -->

BUG is handled by a separate check that looks for comment-style context. It is flagged only when the word is followed by :, (, or [ (whitespace may sit in between), and only when it starts a line or follows whitespace or a comment marker (//, /*, #):

BUG: This needs fixing.
BUG(123): Tracked issue.
BUG[42] Bracket form.
// BUG: In a code comment.

The trailing punctuation is what triggers the match, not the comment marker. A bare // BUG is not flagged, and neither is BUG alone on a line. The check is case-insensitive and does not distinguish prose from anything else, so there is a bug: it crashes is reported. These are not:

This kind of bug can be difficult to track down.
The bug fix was released yesterday.

By default, matches inside inline code spans (`TODO`) and inside fenced code blocks are skipped:

Use `TODO` as a marker in your own commit messages.

```rust
// TODO: This is inside a code block and is not checked by default
```

Configuration

[CONTENT001]
# Additional custom markers to detect, beyond (or instead of) the defaults.
markers = []

# Whether the built-in markers are checked: TODO, FIXME, XXX, HACK, WIP.
# Only takes effect when `markers` is non-empty. Default: true.
include_defaults = true

# Whether markers inside fenced code blocks and inline code spans are also
# checked. Default: false.
check_code_blocks = false

markers

Add project-specific markers such as REVIEW or NEEDSREVIEW:

[CONTENT001]
markers = ["REVIEW"]

include_defaults

Set to false alongside a non-empty markers list to check only the custom markers:

[CONTENT001]
markers = ["REVIEW"]
include_defaults = false

With that configuration TODO: ... is no longer reported and REVIEW: ... is.

Two limits are worth knowing:

  • include_defaults = false on its own does nothing. When markers is empty the rule falls back to the full default set, so every built-in marker is still checked.
  • It does not govern BUG. The contextual BUG check runs unconditionally and cannot be turned off through this option or through markers.

check_code_blocks

Set to true to also flag markers that appear inside fenced code blocks, such as // TODO comments in a Rust example. The same setting governs inline code, so `TODO` in a sentence is reported as well:

[CONTENT001]
check_code_blocks = true

When to Disable

  • Books that intentionally document their own outstanding work (a project's own TODO list rendered as a chapter)
  • Draft content that is not yet meant for publication and is linted alongside finished chapters

Rule Details

  • Rule ID: CONTENT001
  • Aliases: no-todo-comments
  • Category: Content
  • Severity: Warning
  • Auto-fix: No
  • CONTENT002 - Placeholder text such as lorem ipsum
  • CONTENT011 - Future tense describing what the software already does

CONTENT002 - No Placeholder Text

Placeholder text should be replaced with actual content.

Why This Rule Exists

Placeholder text like "Lorem ipsum", "TBD", or "coming soon" is a normal part of drafting, but it should not survive into published documentation. This rule flags a fixed set of common placeholder patterns so they can be caught before a book ships.

Examples

Incorrect

# Installation

Lorem ipsum dolor sit amet.
# Configuration

This feature is TBD.
# Roadmap

This section is coming soon.
# API Reference

Insert content here.

Correct

# Installation

Run `cargo install mdbook-lint` to install the latest release.
# Configuration

Set `output.max_width` to control line wrapping in generated pages.

Other patterns the rule flags: TBA, TBC, under construction, work in progress, N/A, the literal word placeholder, [draft], [pending], foo bar baz, content goes here, your name here, a line that is only XXX, and a line that is only .... example.com in prose is flagged too, including under the default allow_example_urls = true, which exempts it only inside fenced code blocks.

Matches inside fenced code blocks and inline code spans are skipped by default, so a code sample showing TBD as a literal status value is not flagged:

Use `TBD` as the status value until the release date is set.

Configuration

[CONTENT002]
# Whether to scan inside fenced code blocks. Default: false.
check_code_blocks = false

# Whether `example.com` is exempt inside fenced code blocks when those are
# scanned. Has no effect unless check_code_blocks is true. Default: true.
allow_example_urls = true

check_code_blocks

With the default false, text inside fenced code blocks (and inline code spans) is never checked, so example commands or sample output containing words like TBD are left alone. Set it to true to also scan code blocks.

allow_example_urls

With the default true, example.com is still reported when it appears in prose (outside a code block); the option only affects whether it is skipped inside fenced code blocks once check_code_blocks is enabled. Under the default check_code_blocks = false those lines are never scanned anyway, so allow_example_urls does nothing at either value. The exemption also does not cover inline code spans: with check_code_blocks = true, a `https://example.com` span is flagged whichever way this option is set.

When to Disable

  • Working drafts where placeholder markers are intentional and temporary
  • Documentation that legitimately discusses these terms (for example, a writing-style guide that uses "lorem ipsum" as an example of placeholder text)

Rule Details

  • Rule ID: CONTENT002
  • Aliases: no-placeholder-text
  • Category: Content
  • Severity: Warning
  • Auto-fix: No
  • CONTENT001 - TODO, FIXME and similar markers left in prose
  • CONTENT003 - Chapters too short to be worth a page

CONTENT003 - Short Chapters

Chapters should have enough content to be worth their own page.

Why This Rule Exists

A chapter with only a sentence or two is often a stub: a heading was added to SUMMARY.md before the content was written, or a section was split out and never filled in. This rule counts the words in a chapter and flags any that fall below a configurable minimum (50 by default), which surfaces work-in-progress content before it ships in the book.

Examples

Incorrect

# Short Chapter

This is too short.

Correct

# Chapter Title

This is a paragraph with enough content to pass the minimum word count
threshold. We need to write several sentences here to make sure we have at
least fifty words in total. Let me add some more text to ensure we
definitely pass the check. Here is another sentence. And another one. Plus a
few more words to be safe.

What Counts Toward the Word Total

The count is line based. Outside code blocks, a line is dropped from the total when it is a fence line, or when its first non-whitespace characters are #, <!--, or {{#. Every other line contributes all of its whitespace-separated tokens. An ATX heading, an HTML comment that opens and closes on its own line, and an mdBook directive on its own line are therefore not counted:

# This Heading Has Many Words In It

<!-- This HTML comment is also not counted -->

{{#include file.rs}}

Short body.

The chapter above is still flagged: only "Short body." counts, for a total of two words.

Because that is a prefix test on each line rather than an understanding of the markup, three ordinary constructs do count as prose:

  • Setext headings. Only # headings are recognized. A heading underlined with === or --- contributes its own words plus one more for the underline line, so a long setext heading can carry a stub past the threshold. The chapter above, with its heading rewritten as setext, counts ten words rather than two.

  • Multi-line HTML comments. Only the opening line is skipped. Every continuation line, up to and including the one holding -->, is counted, so a stub padded with a long comment can pass the rule.

  • Comments and directives that do not start their line. The line below counts three words, because {{#include and file.rs}} are counted alongside Body.:

    Body. {{#include file.rs}}
    

Code blocks are excluded by default, so a chapter that is mostly a code sample still needs prose around it to pass:

# Chapter

Short intro.

```rust
fn main() {
    // Code comments don't count toward the word total by default
    println!("Hello");
}
```

Configuration

[CONTENT003]
# Minimum word count below which a chapter is flagged. Default: 50.
min_words = 50

# Whether to count words inside code blocks. Default: false.
include_code_blocks = false

min_words

Lower it for books with intentionally terse chapters, or raise it to demand more substantial pages:

[CONTENT003]
min_words = 20

include_code_blocks

With the default false, words inside fenced code blocks (``` or ~~~) are not counted. Set it to true to count code toward the total, which is useful for reference chapters that are mostly runnable examples:

[CONTENT003]
include_code_blocks = true

Even with true, two kinds of line stay out of the total: the fence lines themselves, and code lines beginning with #, since the heading test is applied to code lines too. A shell or Python comment on a line of its own is therefore skipped, but a trailing comment after code is not: the test is on the start of the line, so print("x") # note contributes every token on it.

When to Disable

  • Reference chapters that are intentionally brief, such as a single command's options
  • Landing pages that link out to subsections rather than containing prose themselves

Rule Details

  • Rule ID: CONTENT003
  • Aliases: no-short-chapters
  • Category: Content
  • Severity: Warning
  • Auto-fix: No
  • CONTENT001 - TODO and FIXME markers left in prose
  • CONTENT002 - Placeholder text such as lorem ipsum

CONTENT004 - Heading Capitalization Consistency

Headings should use a consistent capitalization style throughout a document.

Why This Rule Exists

Mixing Title Case and sentence case headings in the same document reads as unpolished, and it's usually accidental: a section gets added later, written by someone else, or pasted from a different source. Consistent heading capitalization makes a document feel like it was written by one voice.

Examples

Incorrect

The first heading establishes Title Case as the document's style, so the lowercase second heading is flagged:

# Getting Started Guide

## installation steps

### More Configuration Options

Correct

All headings follow the same style. Title Case:

# Getting Started Guide

## Installation Steps

### Configuration Options

Sentence case works equally well, as long as it's consistent:

# Getting started guide

## Installation steps

### Configuration options

How Style Is Detected

By default (style = "consistent"), the rule doesn't require Title Case or sentence case specifically. It detects whichever style the first multi-word heading uses and expects every later heading to match it.

A few details affect how a heading is classified:

  • Single-word headings are skipped. # Introduction says nothing about capitalization style either way.
  • Acronyms are ignored when judging a word's case. API, HTTP, and similar all-uppercase words don't count against sentence case.
  • Articles, conjunctions, and short prepositions (a, the, and, of, to, vs, etc.) are excluded from the Title Case check, since Title Case conventionally leaves them lowercase.
  • A heading that's valid under both styles doesn't set or break the baseline. For example, # Agentic SDLC is valid Title Case and valid sentence case at once, because its only non-first word is an acronym. Such a heading is never itself flagged, and if it's the first heading in the document, the rule keeps looking for the next heading to establish the baseline instead of locking onto it.

Headings inside fenced code blocks are not checked.

Configuration

[CONTENT004]
# "consistent" (default): match whatever style the first heading uses.
# "title" / "title_case": require Title Case throughout.
# "sentence" / "sentence_case": require sentence case throughout.
style = "consistent"

style

With "title", every multi-word heading must be Title Case:

# Getting started guide
Heading 'Getting started guide' should use Title Case

With "sentence", every multi-word heading must be sentence case:

# Getting Started Guide
Heading 'Getting Started Guide' should use sentence case

When to Disable

  • Documents that intentionally mix heading styles, such as ones that quote headings from external sources verbatim
  • Books where headings are short enough that capitalization style rarely comes up

Rule Details

  • Rule ID: CONTENT004
  • Category: Content
  • Severity: Warning
  • Auto-fix: No
  • CONTENT005 - A heading followed straight by a subheading
  • CONTENT009 - Headings nested deeper than a reader will follow
  • MD001 - Heading levels should only increment by one level at a time
  • MD003 - Heading style

CONTENT005 - Introductory Paragraph Before Subheading

Chapters should have introductory content before the first subheading.

Why This Rule Exists

A chapter that jumps straight from its title into a subheading gives the reader no framing for what the chapter covers. A short introduction after the H1 orients the reader before the first H2 (or deeper) heading takes over.

Examples

Incorrect

# Chapter Title

## First Section
# Chapter Title

Brief intro.

## First Section

Correct

# Chapter Title

This chapter covers important topics that you need to understand.
We will explore several key concepts in detail below.

## First Section

Only the introduction before the first subheading is checked. Chapters with no subheadings at all, and chapters with no H1, are not checked:

# Chapter Title

This is a simple chapter with no subheadings.
It just has regular paragraphs of content.
## First Section

Some content here.

## Second Section

Fenced code blocks do not count toward the word total, since they are not prose written for this chapter:

# Chapter Title

```rust
// This code block should not count as intro
fn main() {}
```

## First Section

Apart from fenced blocks, the exclusion is a per-line test with no memory of what came before: a line is dropped from the count only when its first non-whitespace characters are #, <!--, or {{#. The last covers mdBook directives such as {{#include}} on a line of their own, and the first covers any line starting with a hash, which includes a second H1 as well as headings below it. A single-line HTML comment is therefore skipped, but in a multi-line comment only the opening line is: every body line and the closing --> count as introduction prose, unless one of them happens to start with a prefix from that same list. Indented (four-space) code blocks are counted too, because each line is trimmed before it is tested and the indentation that made it code is gone by then. Both of these chapters pass the rule despite having no introduction:

# Chapter Title

<!--
This comment body is not prose for the reader but it is counted anyway
-->

## First Section
# Chapter Title

    let x = 1;
    let y = 2;
    let z = 3;
    println!("{} {} {}", x, y, z);

## First Section

Configuration

[CONTENT005]
# Minimum number of words required in the introduction. Default: 10.
min_words = 10

min_words

min_intro_words is accepted as an alias, and both snake_case and kebab-case keys work (min-words, min-intro-words). Words are counted from the line after the H1 up to (but not including) the first subheading, skipping fenced code blocks in full and dropping any remaining line whose first non-whitespace character is # or that begins with <!-- or {{#. As described above, that per-line test does not exclude the body of a multi-line HTML comment or an indented code block.

When to Disable

  • Chapters that are intentionally just a list or a table of links, with the context provided elsewhere in the book
  • Generated reference pages where a subheading immediately following the title is the expected shape

Rule Details

  • Rule ID: CONTENT005
  • Aliases: intro-before-subheading
  • Category: Content
  • Severity: Warning
  • Auto-fix: No
  • CONTENT003 - Chapters too short to be worth a page
  • CONTENT009 - Headings nested deeper than a reader will follow
  • MD022 - Headings should be surrounded by blank lines

CONTENT006 - No Broken Internal Links

Internal anchor links ([text](#anchor)) should point to a heading that actually exists in the same document.

Why This Rule Exists

Markdown lets you link to a heading within the same file using [text](#anchor), where anchor is the heading's generated slug. If the heading is renamed, removed, or the anchor is mistyped, the link silently breaks: mdBook renders it as a dead link with no warning at build time.

CONTENT006 generates the same anchor slugs mdBook does and checks every in-page anchor link against them, so broken links are caught before publish.

Only links of the form (#anchor) are checked. Links to other files (./other.md), links to another file's anchor (./other.md#section), and external URLs are out of scope, since this rule can only see headings in the current document.

Examples

Incorrect

# Getting Started

See [broken link](#nonexistent-section) for more info.

The document has no ## Nonexistent Section heading, so #nonexistent-section does not resolve to anything.

Correct

# Getting Started

See [the introduction](#getting-started) for more info.

## Installation

Check [installation](#installation) instructions.

Each anchor matches a heading's generated slug: # Getting Started becomes getting-started, ## Installation becomes installation.

Duplicate headings

When the same heading text appears more than once, mdBook disambiguates the slugs by appending -1, -2, and so on to the second and later occurrences. CONTENT006 follows the same numbering:

## Topic

Look at its [details](#details).

### Details

Details about the topic.

## Another Topic

Look at its [details](#details-1).

### Details

Details about the other topic.
# Title

```markdown
[example](#nonexistent)
```

Both the fenced code block's content and any heading-like text inside it are skipped, so example snippets don't trigger false positives and can't be used as link targets.

Configuration

This rule has no configuration options.

When to Disable

  • Documents that rely on anchors injected by a template or preprocessor after mdBook-lint runs, which this rule cannot see
  • Books using a non-default slug scheme that doesn't match mdBook's

Rule Details

  • Rule ID: CONTENT006
  • Aliases: no-broken-internal-links
  • Category: Content
  • Severity: Warning
  • Auto-fix: No
  • CONTENT005 - A heading followed straight by a subheading
  • MD042 - Empty links
  • MD051 - Link fragments

CONTENT007 - Consistent Terminology

Terms should be used consistently throughout a document.

Why This Rule Exists

Documentation that switches between variants of the same term, such as "config" and "configuration", or "front-end" and "frontend", reads as unpolished and can make readers wonder whether the two forms mean different things. Settling on one spelling per document keeps prose predictable.

Examples

Incorrect

# Settings

The config file is in the config directory.
Edit the configuration to change settings.

config is used twice and configuration once, so the less common variant is flagged.

Correct

# Settings

The config file is in the config directory.
Edit the config to change settings.

Only one variant of the term is used throughout.

The rule also catches British/American spelling pairs and hyphenation differences:

<!-- Flagged: "color" is used once, "colour" is used twice -->
The color scheme is customizable.
You can change the colour of any element.
The colour picker is easy to use.
<!-- Flagged: "frontend" and "front-end" are each used once, so one is
     reported as inconsistent with the other -->
The frontend handles user interaction.
The front-end is built with React.

Matching is case-insensitive and respects word boundaries, so "reconfiguration" does not match "config". Text inside fenced or inline code spans is ignored.

Configuration

[CONTENT007]
# Groups of terms that should be used consistently. Replaces the built-in
# groups when provided.
term_groups = [
    ["config", "configuration"],
    ["setup", "set up", "set-up"],
    ["email", "e-mail"],
]

# Minimum number of times a less-common variant must appear before it is
# reported. Default: 1.
min_occurrences = 1

term_groups

Each inner array lists variants that should not be mixed within a document. The built-in groups cover common pairs such as config/configuration, login/log in/log-in, email/e-mail, frontend/front-end, grey/gray, colour/color, and similar. Supplying term_groups replaces the defaults entirely rather than extending them.

min_occurrences

Raises the bar before a variant is reported, so a single stray use of an uncommon spelling does not trigger a violation. The default of 1 reports every occurrence of a less-common variant once any inconsistency exists.

When to Disable

  • Documents that intentionally discuss both spellings, such as a style guide explaining the difference between "setup" (noun) and "set up" (verb)
  • Books with contributors from different English locales where enforcing one spelling is not a priority

Rule Details

  • Rule ID: CONTENT007
  • Aliases: consistent-terminology
  • Category: Content
  • Severity: Info
  • Auto-fix: No

CONTENT009 - No Excessive Heading Nesting

Heading nesting should not be too deep (default max: h4).

Why This Rule Exists

Deep heading hierarchies (h5, h6) often signal that a chapter is trying to hold too much at once. Readers lose track of where they are in the document, sidebars and generated tables of contents become hard to scan, and the content is usually better served by splitting it into separate chapters or flattening the structure.

Examples

Incorrect

# Chapter

## Section

### Subsection

#### Details

##### Too Deep

The ##### heading is an h5, one level past the default maximum of h4.

Correct

# Chapter

## Section

### Subsection

#### Details

Headings stay at h4 or shallower, so nothing is flagged.

Configuration

[CONTENT009]
# Deepest heading level allowed. Default: 4.
max_depth = 4

Both max_depth and max-depth are accepted in configuration.

Only a non-negative TOML integer is read. Such a value is clamped to 1-6, so 0 behaves as 1 and 99 behaves as 6. Anything else is discarded without an error and the rule runs at the default of 4: that includes negative integers, so max_depth = -1 does not mean "the strictest setting", it means the same as leaving the option out. Quoted numbers ("2") and floats (2.0) are dropped the same way.

[CONTENT009]
max_depth = 2

With max_depth = 2, an ### Subsection (h3) heading is reported.

When to Disable

  • Reference material (API docs, generated changelogs) that legitimately needs deep, granular subsections
  • Chapters imported from external sources with an existing deep hierarchy you don't want to restructure right now

Rule Details

  • Rule ID: CONTENT009
  • Aliases: no-excessive-nesting
  • Category: Content
  • Severity: Warning
  • Auto-fix: No
  • CONTENT005 - Heading immediately followed by a subheading
  • MD001 - Heading levels should only increment by one level at a time
  • MD003 - Heading style consistency

CONTENT010 - Link Text Quality

Link text should be descriptive, not generic like "click here" or "here".

Why This Rule Exists

Screen readers often present a page's links as a standalone list, out of the surrounding sentence. Link text like "here" or "click here" gives no information in that context, and it forces every reader to hunt through the surrounding paragraph to find out where a link actually leads.

Examples

Incorrect

[Click here](https://example.com) to learn more.

For more information, see [here](./docs.md).

Follow [this link](https://example.com) for details.

[Read more](./article.md)

[Learn more](https://docs.example.com)

See [this](./example.md) for an example.

[Info](./help.md)

For [details](./spec.md), see the specification.

Correct

Check out the [installation guide](./install.md) for details.

See the [API documentation](https://docs.example.com) for more info.

Read [more about configuration](./config.md) here.

The last example is not flagged: the rule matches link text only when it is exactly one of the generic phrases, so "more about configuration" (which merely contains "more") passes.

Configuration

This rule has no configuration options.

When to Disable

  • Content where the surrounding UI text is itself "click here" or similar, and the link text is meant to mirror it
  • Legacy content being migrated incrementally, where rewriting every link is out of scope for the current change

Rule Details

  • Rule ID: CONTENT010
  • Aliases: link-text-quality
  • Category: Content
  • Severity: Warning
  • Auto-fix: No

Flagged Phrases

The rule performs a case-insensitive exact match of a link's text against this list:

  • "click here"
  • "here"
  • "this link"
  • "this page"
  • "this article"
  • "this"
  • "link"
  • "read more"
  • "more"
  • "learn more"
  • "see more"
  • "more info"
  • "more information"
  • "details"
  • "info"

Links inside fenced code blocks are ignored.

  • MD042 - Empty links
  • MD059 - Descriptive link text

CONTENT011 - No Future Tense

Documentation should use present tense instead of future tense.

Why This Rule Exists

Technical documentation describes what software does, not what it will do at some later point. "This function will return an integer" reads as a promise about a future release, while "This function returns an integer" states a fact about the current behavior. Present tense is shorter, more direct, and avoids ambiguity about whether a feature is implemented yet.

Examples

Incorrect

This function will return an integer.

The value will be updated.

This will throw an exception if the input is invalid.

This method is going to create a new file.

These functions are going to process the data.

Correct

This function returns an integer.

The value is updated.

This throws an exception if the input is invalid.

This method creates a new file.

These functions process the data.

The rule only matches "will" directly followed by one of a known set of verbs (see below), so unrelated uses of "will" are left alone:

The user's free will is respected.

Configuration

This rule has no configuration options.

When to Disable

  • Documentation that intentionally describes planned or upcoming behavior, such as a roadmap or changelog "Unreleased" section
  • API references written for a not-yet-released version, where future tense correctly describes functionality that does not exist yet

Rule Details

  • Rule ID: CONTENT011
  • Aliases: no-future-tense
  • Category: Content
  • Severity: Info
  • Auto-fix: No

Flagged Patterns

The rule matches, case-insensitively:

  • will followed directly by one of a fixed list of verbs: be, have, return, throw, create, generate, produce, output, display, show, print, log, emit, trigger, fire, call, invoke, execute, run, start, stop, open, close, read, write, load, save, send, receive, get, set, add, remove, delete, update, change, modify, process, handle, validate, check, verify, parse, convert, transform, format, render, build, compile, install, download, upload, fetch, request, respond
  • is going to <word>
  • are going to <word>

Each violation message includes a suggested present-tense rewrite, for example will return becomes return and will be becomes is.

At most one violation is reported per line, even if the line contains several future-tense phrases.

Fenced code blocks (``` or ~~~) are tracked across lines and skipped in full, so a Rust doc comment like // This function will return a value inside a code sample is not flagged.

HTML comments and mdbook directives are not tracked. The rule skips a line only when the trimmed line begins with <!-- or {{#; it keeps no comment state and does not mask matches within a line. Future tense is still reported inside a multi-line HTML comment, after an inline <!-- ... -->, or on a line where a {{#...}} directive follows other text:

<!-- This whole line will be ignored, because the line starts with "<!--". -->

<!--
This function will return an integer.   <- still flagged
-->

Some text <!-- this will throw an error --> more text.   <- still flagged

See the sample {{#playground example.rs}} and it will create a file.   <- still flagged

{{#playground example.rs}} and this will delete things.   <- skipped, line starts with "{{#"
  • CONTENT001 - TODO, FIXME and similar markers left in prose
  • CONTENT002 - Placeholder text such as lorem ipsum

Configuration Reference

Complete reference for all configuration options in mdbook-lint.

Global Configuration Options

preset

  • Type: string
  • Default: not set (use the complete stable default ruleset)
  • Valid values: "baseline"
  • Description: Select a curated, explicit base rule set. The baseline is a low-noise starting point for incremental CI adoption.
preset = "baseline"

Run mdbook-lint rules --preset baseline to inspect its exact, versioned membership: MD001, MD003, MD009, MD010, MD011, and MD014. The list is intentionally curated rather than derived from rule categories, so newly stable rules are not added implicitly. Removing the setting returns to the complete stable default set.

fail-on-warnings

  • Type: boolean
  • Default: false
  • Description: Exit with error code when warnings are found

fail-on-errors

  • Type: boolean
  • Default: true
  • Description: Exit with error code when errors are found

disabled-rules

  • Type: array<string>
  • Default: []
  • Description: List of rule IDs to disable globally
  • Example: ["MD013", "MD033"]

enabled-rules

  • Type: array<string>
  • Default: []
  • Description: List of rule IDs to explicitly enable. When non-empty this means "run only these rules", so every rule not listed is switched off.
  • Example: ["MD001", "MD002"]

experimental-rules

  • Type: array<string>
  • Default: []
  • Description: Experimental rules to run in addition to the stable defaults. Accepts rule IDs, or "*" for every experimental rule.
  • Example: ["MDBOOK010"] or ["*"]

Experimental rules do not run by default because their diagnostics may still change. Use this option rather than enabled-rules when you want an experimental rule alongside the normal rule set, since enabled-rules would disable everything else:

# Run all stable rules, plus MDBOOK010.
experimental-rules = ["MDBOOK010"]

--enable MDBOOK010 also activates an experimental rule, but follows the "only these rules" meaning of an explicit selection.

Run mdbook-lint rules to see which rules are experimental, or mdbook-lint rules --detailed for a table with a Default column showing whether each rule runs without configuration.

Rule-Selection Precedence

Rule selection uses the following precedence, from highest to lowest:

  1. CLI --enable selects exactly those rules and preserves its existing behavior of clearing configured disabled-rules.
  2. CLI --preset overrides a configured preset or enabled-rules base.
  3. A non-empty configured enabled-rules list selects exactly those rules and takes precedence over preset in the same file.
  4. Configured preset selects its explicit membership.
  5. With none of the above, all stable default rules run.

Configured disabled-rules and CLI --disable subtract from a preset. CLI --preset conflicts with CLI --enable, --standard-only, and --mdbook-only. Categories, experimental-rules, and markdownlint-compatible do not modify an exact preset; mdbook-lint check warns when these ineffective combinations are configured.

enabled-categories

  • Type: array<string>
  • Default: []
  • Description: List of rule categories to enable
  • Valid values: headings, lists, whitespace, code, style, links, mdbook

disabled-categories

  • Type: array<string>
  • Default: []
  • Description: List of rule categories to disable

markdownlint-compatible

  • Type: boolean
  • Default: false
  • Description: Enable markdownlint compatibility mode (disables rules that are disabled by default in markdownlint)

deprecated-warning

  • Type: string
  • Default: "warn"
  • Description: How to handle deprecated rule warnings
  • Valid values: "warn", "info", "silent"

malformed-markdown

  • Type: string
  • Default: "warn"
  • Description: How to handle malformed markdown
  • Valid values: "error", "warn", "skip"

Rules Section Configuration

rules.default

  • Type: boolean
  • Default: true
  • Description: Whether rules are enabled by default

rules.enabled

  • Type: table<string, boolean>
  • Description: Map of rule IDs to enable when default = false

rules.disabled

  • Type: table<string, boolean>
  • Description: Map of rule IDs to disable when default = true

Example:

[rules]
default = false

[rules.enabled]
MD001 = true
MD002 = true
MD009 = true

Rule-Specific Configuration

MD002 - First heading should be a top-level heading

[MD002]
level = 1  # Expected level of first heading (default: 1)

MD003 - Heading style

[MD003]
style = "consistent"  # Options: "consistent", "atx", "atx_closed", "setext"

MD004 - Unordered list style

[MD004]
style = "consistent"  # Options: "consistent", "asterisk", "plus", "dash"

MD007 - Unordered list indentation

[MD007]
indent = 2  # Spaces for indentation (default: 2)
start_indented = false  # Allow first level to be indented

MD009 - Trailing spaces

[MD009]
br_spaces = 2  # Spaces for line breaks (default: 2)
list_item_empty_lines = false  # Allow spaces in empty list items
strict = false  # Strict mode for all trailing spaces

MD010 - Hard tabs

[MD010]
code_blocks = true  # Include code blocks (default: true)
spaces_per_tab = 4  # Spaces per tab for reporting (default: 4)

MD012 - Multiple consecutive blank lines

[MD012]
maximum = 1  # Maximum consecutive blank lines (default: 1)

MD013 - Line length

[MD013]
line_length = 80  # Maximum line length (default: 80)
code_blocks = false  # Check code blocks
tables = false  # Check tables
headings = true  # Check headings
heading_line_length = 80  # Separate limit for headings
strict = false  # Strict length checking
stern = false  # Stern length checking

MD024 - Multiple headings with same content

[MD024]
siblings_only = false  # Only check sibling headings (default: false)

MD025 - Multiple top-level headings

[MD025]
level = 1  # Heading level to check (default: 1)
front_matter_title = true  # Use front matter title

MD026 - Trailing punctuation in heading

[MD026]
punctuation = ".,;:!?"  # Punctuation to check (default: ".,;:!?")

MD029 - Ordered list item prefix

[MD029]
style = "one_or_ordered"  # Options: "one", "ordered", "one_or_ordered", "zero"

MD030 - Spaces after list markers

[MD030]
ul_single = 1  # Spaces after single-line unordered list marker
ul_multi = 1  # Spaces after multi-line unordered list marker
ol_single = 1  # Spaces after single-line ordered list marker
ol_multi = 1  # Spaces after multi-line ordered list marker

MD035 - Horizontal rule style

[MD035]
style = "consistent"  # Style to enforce or "consistent"

MD036 - Emphasis used instead of heading

[MD036]
punctuation = ".,;:!?"  # Punctuation at end (default: ".,;:!?")

MD043 - Required heading structure

[MD043]
headings = ["# Summary", "## Overview"]  # Required headings in order
required_headings = ["# Summary", "## Overview"]  # Alternative name
headers = ["# Summary", "## Overview"]  # Alternative name (deprecated)

MD044 - Proper names should have correct capitalization

[MD044]
names = ["JavaScript", "GitHub", "TypeScript"]  # Proper names
code_blocks = false  # Include code blocks
html_elements = false  # Include HTML elements

MD046 - Code block style

[MD046]
style = "consistent"  # Options: "consistent", "fenced", "indented"

MD048 - Code fence style

[MD048]
style = "consistent"  # Options: "consistent", "backtick", "tilde"

MD049 - Emphasis style

[MD049]
style = "consistent"  # Options: "consistent", "asterisk", "underscore"

MD050 - Strong style

[MD050]
style = "consistent"  # Options: "consistent", "asterisk", "underscore"
[MD051]
# No configuration options
[MD052]
shortcut_syntax = false  # Allow shortcut syntax
[MD053]
ignored_definitions = ["//"]  # Definitions to ignore
[MD054]
# No configuration options

MD055 - Table pipe style

[MD055]
style = "consistent"  # Options: "consistent", "leading_only", "trailing_only", "leading_and_trailing", "no_leading_or_trailing"

MD056 - Table column count

[MD056]
# No configuration options

MD058 - Tables should be surrounded by blank lines

[MD058]
# No configuration options

MD059 - Tables should not have empty cells

[MD059]
allowed_corner_cells = false  # Allow empty corner cells

mdBook-Specific Rules

mdBook-specific rules (MDBOOK001-MDBOOK025) generally don't have configuration options, as they check for mdBook-specific patterns and conventions.

Configuration File Examples

Minimal Configuration

disabled-rules = ["MD013", "MD033"]

Comprehensive Configuration

fail-on-warnings = true
fail-on-errors = true
markdownlint-compatible = false
deprecated-warning = "warn"
malformed-markdown = "error"

enabled-categories = ["headings", "lists"]
disabled-rules = ["MD041"]

[MD002]
level = 1

[MD003]
style = "atx"

[MD007]
indent = 2
start_indented = false

[MD009]
br_spaces = 2
strict = false

[MD013]
line_length = 100
code_blocks = false
tables = false

[MD024]
siblings_only = true

[MD029]
style = "ordered"

[MD030]
ul_single = 1
ol_single = 1

[MD044]
names = ["JavaScript", "TypeScript", "GitHub", "mdBook"]
code_blocks = false

Using Rules Section

[rules]
default = false

[rules.enabled]
MD001 = true
MD002 = true
MD003 = true
MD009 = true
MD047 = true

[MD003]
style = "atx"

[MD009]
br_spaces = 2

Example Configuration

This page provides a complete, fully-commented example configuration file for mdbook-lint.

Quick Start

  1. Copy the configuration below to .mdbook-lint.toml in your project root
  2. Uncomment and modify only the settings you want to change
  3. All settings are optional - mdbook-lint works with sensible defaults

Complete Example Configuration

# mdbook-lint Example Configuration
#
# This is a comprehensive example configuration file for mdbook-lint.
# Copy this file to `.mdbook-lint.toml` in your project root and customize as needed.
#
# All settings shown here are optional - mdbook-lint works with sensible defaults.

# Start with a curated low-noise rule set for incremental CI adoption.
# Remove this line later to use the full stable default set.
# preset = "baseline"
# Uncomment and modify only the settings you want to change.

# ============================================================================
# GLOBAL SETTINGS
# ============================================================================

# Exit with error code if warnings are found
# fail-on-warnings = false

# List of rules to disable globally
# disabled-rules = ["MD013", "MD033"]

# Paths to ignore when linting
# ignore-paths = ["target/", "vendor/", "*.backup.md"]

# Experimental rules to run alongside the stable defaults.
# Experimental rules are off by default because their output may change.
# Accepts rule IDs, or "*" for all of them. Unlike enabled-rules, this adds
# to the default set rather than replacing it.
# This setting is ignored when an exact preset is selected.
# experimental-rules = ["MDBOOK010"]

# ============================================================================
# STANDARD MARKDOWN RULES (MD001-MD059)
# ============================================================================

# ----------------------------------------------------------------------------
# Heading Rules
# ----------------------------------------------------------------------------

# MD001 - Heading levels should only increment by one level at a time
# [MD001]
# No configuration options

# MD002 - First heading should be a top-level heading
# DEPRECATED: Use MD041 instead. This rule is disabled by default.
# [MD002]
# level = 1  # Expected first heading level

# MD003 - Heading style
# [MD003]
# style = "atx"  # Options: "atx", "setext", "atx_closed", "consistent"

# MD018 - No space after hash on atx style heading (auto-fix)
# [MD018]
# No configuration options

# MD019 - Multiple spaces after hash on atx style heading (auto-fix)
# [MD019]
# No configuration options

# MD020 - No space inside hashes on closed atx style heading (auto-fix)
# [MD020]
# No configuration options

# MD021 - Multiple spaces inside hashes on closed atx style heading (auto-fix)
# [MD021]
# No configuration options

# MD022 - Headings should be surrounded by blank lines
# [MD022]
# lines_above = 1  # Blank lines above heading
# lines_below = 1  # Blank lines below heading

# MD023 - Headings must start at the beginning of the line (auto-fix)
# [MD023]
# No configuration options

# MD024 - Multiple headings with the same content
# [MD024]
# siblings_only = false  # Only check sibling headings

# MD025 - Multiple top-level headings in the same document
# [MD025]
# level = 1  # Heading level to check
# front_matter_title = true  # Consider front matter title as heading

# MD026 - Trailing punctuation in heading
# [MD026]
# punctuation = ".,;:!。,;:!"  # Punctuation to check

# ----------------------------------------------------------------------------
# List Rules
# ----------------------------------------------------------------------------

# MD004 - Unordered list style
# [MD004]
# style = "consistent"  # Options: "asterisk", "dash", "plus", "consistent"

# MD005 - Consistent list indentation
# [MD005]
# No configuration options

# MD006 - Consider starting lists at the beginning of the line
# DEPRECATED: This rule conflicts with nested list handling. Disabled by default.
# [MD006]
# No configuration options

# MD007 - Unordered list indentation
# [MD007]
# indent = 2  # Spaces per indentation level
# start_indented = false  # Allow first level to be indented

# MD029 - Ordered list item prefix
# [MD029]
# style = "one_or_ordered"  # Options: "one", "ordered", "one_or_ordered"

# MD030 - Spaces after list markers (auto-fix)
# [MD030]
# ul_single = 1  # Spaces after single-line unordered list marker
# ul_multi = 1   # Spaces after multi-line unordered list marker
# ol_single = 1  # Spaces after single-line ordered list marker
# ol_multi = 1   # Spaces after multi-line ordered list marker

# MD032 - Lists should be surrounded by blank lines
# [MD032]
# No configuration options

# ----------------------------------------------------------------------------
# Whitespace Rules
# ----------------------------------------------------------------------------

# MD009 - Trailing spaces (auto-fix)
# [MD009]
# br_spaces = 2  # Number of spaces for line break
# strict = false  # Strict mode (no line break spaces)
# list_item_empty_lines = false  # Allow empty list items

# MD010 - Hard tabs (auto-fix)
# [MD010]
# code_blocks = true  # Check code blocks
# spaces_per_tab = 4  # Spaces to replace each tab

# MD012 - Multiple consecutive blank lines (auto-fix)
# [MD012]
# maximum = 1  # Maximum consecutive blank lines

# MD027 - Multiple spaces after blockquote symbol (auto-fix)
# [MD027]
# spaces = 1  # Number of spaces after blockquote marker

# MD028 - Blank line inside blockquote
# [MD028]
# No configuration options

# MD047 - Files should end with a single newline character (auto-fix)
# [MD047]
# No configuration options

# ----------------------------------------------------------------------------
# Code Rules
# ----------------------------------------------------------------------------

# MD014 - Dollar signs used before commands without showing output
# [MD014]
# No configuration options

# MD031 - Fenced code blocks should be surrounded by blank lines
# [MD031]
# list_items = true  # Check code blocks in lists

# MD038 - Spaces inside code span elements
# [MD038]
# No configuration options

# MD040 - Fenced code blocks should have a language specified
# [MD040]
# allowed_languages = []  # List of allowed languages (empty = all)
# language_optional = false  # Language tag is optional

# MD046 - Code block style
# [MD046]
# style = "fenced"  # Options: "fenced", "indented", "consistent"

# MD048 - Code fence style
# [MD048]
# style = "backtick"  # Options: "backtick", "tilde", "consistent"

# ----------------------------------------------------------------------------
# Link and Image Rules
# ----------------------------------------------------------------------------

# MD011 - Reversed link syntax
# [MD011]
# No configuration options

# MD034 - Bare URL used (auto-fix)
# [MD034]
# No configuration options

# MD039 - Spaces inside link text
# [MD039]
# No configuration options

# MD042 - No empty links
# [MD042]
# No configuration options

# MD045 - Images should have alternate text (alt text)
# [MD045]
# No configuration options

# MD051 - Link fragments should be valid
# [MD051]
# No configuration options

# MD052 - Reference links and images should use a label that is defined
# [MD052]
# ignored_labels = [" ", "x"]  # Labels never reported as undefined
# shortcut_syntax = false      # Also validate bare [label] shortcut references

# MD053 - Link and image reference definitions should be needed
# [MD053]
# ignored_definitions = []  # Definitions to ignore

# MD054 - Link and image style
# [MD054]
# autolink = true  # Allow autolinks
# inline = true    # Allow inline links
# full = true      # Allow full reference links
# collapsed = true # Allow collapsed reference links
# shortcut = true  # Allow shortcut reference links
# url_inline = true  # Allow URLs as inline links

# MD059 - Link and image reference definitions should be sorted
# [MD059]
# No configuration options (coming soon)

# ----------------------------------------------------------------------------
# Style Rules
# ----------------------------------------------------------------------------

# MD013 - Line length
# [MD013]
# line_length = 80  # Maximum line length
# length_mode = "strict"  # Options: "strict" (count all chars), "visual" (exclude URLs)
# code_blocks = true  # Check code blocks
# tables = true  # Check tables
# headings = true  # Check headings
# ignore_reference_definitions = false  # Skip long [label]: url reference definition lines
# strict = false  # Strict mode (no leniency)
# stern = false  # Stern mode (allow long lines without spaces)

# MD035 - Horizontal rule style
# [MD035]
# style = "---"  # Horizontal rule style

# MD036 - Emphasis used instead of a heading
# [MD036]
# punctuation = ".,;:!?。,;:!?"  # Punctuation at end

# MD037 - Spaces inside emphasis markers
# [MD037]
# No configuration options

# MD041 - First line in a file should be a top-level heading
# [MD041]
# level = 1  # Required heading level
# front_matter_title = true  # Consider front matter title

# MD043 - Required heading structure
# [MD043]
# headings = []  # Required heading structure
# match_case = false  # Case-sensitive matching

# MD044 - Proper names should have the correct capitalization
# [MD044]
# names = []  # List of proper names
# code_blocks = true  # Check code blocks
# html_elements = true  # Check HTML elements

# MD049 - Emphasis style should be consistent
# [MD049]
# style = "asterisk"  # Options: "asterisk", "underscore", "consistent"

# MD050 - Strong style should be consistent
# [MD050]
# style = "asterisk"  # Options: "asterisk", "underscore", "consistent"

# ----------------------------------------------------------------------------
# Table Rules
# ----------------------------------------------------------------------------

# MD055 - Table pipe style
# [MD055]
# style = "leading_and_trailing"  # Options: "leading_only", "trailing_only", "leading_and_trailing", "no_leading_or_trailing"

# MD056 - Table column count
# [MD056]
# No configuration options

# MD057 - Relative links should point to existing files
# [MD057]
# No configuration options

# MD058 - Tables should be surrounded by blank lines
# [MD058]
# No configuration options

# MD060 - Table column alignment style
# [MD060]
# style = "consistent"  # Options: "aligned", "compact", "tight", "any", "consistent"

# ----------------------------------------------------------------------------
# HTML Rules
# ----------------------------------------------------------------------------

# MD033 - Inline HTML
# [MD033]
# allowed_elements = []  # HTML elements to allow

# ============================================================================
# CONTENT RULES
# ============================================================================
# Rules for checking document content quality and structure

# CONTENT001 - TODO/FIXME/XXX comments should be resolved before publishing
# [CONTENT001]
# markers = ["TODO", "FIXME", "XXX"]  # Custom markers to detect
# include_defaults = true             # Also check the built-in markers
# check_code_blocks = false           # Scan inside code blocks

# CONTENT002 - Placeholder text should be replaced with actual content
# [CONTENT002]
# check_code_blocks = false   # Scan inside code blocks
# allow_example_urls = true   # Allow example.com URLs in examples

# CONTENT003 - Chapters should have sufficient content (minimum word count)
# [CONTENT003]
# min_words = 50              # Flag chapters below this word count
# include_code_blocks = false # Count words inside code blocks

# CONTENT004 - Headings should use consistent capitalization
# [CONTENT004]
# style = "consistent"  # Options: "title", "sentence", "consistent"

# CONTENT005 - Chapters should have introductory content before the first subheading
# [CONTENT005]
# min_words = 10  # Minimum words required in the introduction

# CONTENT006 - Internal anchor links should reference valid headings
# [CONTENT006]
# No configuration options

# CONTENT007 - Terms should be used consistently throughout a document
# [CONTENT007]
# term_groups = [["email", "e-mail"]]  # Groups of terms to keep consistent
# min_occurrences = 1                  # Occurrences before reporting

# CONTENT009 - Heading nesting should not be too deep
# [CONTENT009]
# max_depth = 4  # Deepest heading level allowed (1-6)

# CONTENT010 - Link text should be descriptive
# [CONTENT010]
# No configuration options

# CONTENT011 - Documentation should use present tense
# [CONTENT011]
# No configuration options

# ============================================================================
# MDBOOK-SPECIFIC RULES
# ============================================================================

# MDBOOK001 - Code blocks should have language tags for syntax highlighting
# [MDBOOK001]
# No configuration options

# MDBOOK002 - Internal link validation
# [MDBOOK002]
# check_anchors = false  # Validate same-document anchors (#section) against headings
# allow_external = true  # Skip external URLs; false reports them instead
# check_images = false  # Also validate image paths

# MDBOOK003 - SUMMARY.md structure validation
# [MDBOOK003]
# allow_draft_chapters = true  # Allow chapters without links
# require_part_headers = false  # Require at least one part header
# max_depth = 3  # Maximum nesting depth (unset means unlimited)

# MDBOOK004 - No duplicate chapter titles
# [MDBOOK004]
# case_sensitive = true  # Case-sensitive comparison
# ignore_prefixes = ["Chapter", "Part"]  # Prefixes stripped before comparing

# MDBOOK005 - Orphaned files detection
# [MDBOOK005]
# ignore_patterns = ["drafts/**", "*.backup.md"]  # Patterns to ignore
# check_nested = true  # Check subdirectories
# exclude_readme = true  # Don't report README.md

# MDBOOK006 - Cross-reference validation
# [MDBOOK006]
# No configuration options

# MDBOOK007 - Include directive syntax validation
# [MDBOOK007]
# No configuration options

# MDBOOK008 - Rustdoc include validation
# [MDBOOK008]
# No configuration options

# MDBOOK009 - Playground directive syntax
# [MDBOOK009]
# No configuration options

# MDBOOK010 - Preprocessor configuration validation
# [MDBOOK010]
# No configuration options

# MDBOOK011 - Template syntax validation
# [MDBOOK011]
# No configuration options

# MDBOOK012 - Include line range validation
# [MDBOOK012]
# No configuration options

# MDBOOK016 - Rust code blocks should use valid mdBook/rustdoc attributes
# [MDBOOK016]
# No configuration options

# MDBOOK017 - Rust code blocks should use # prefix to hide boilerplate
# [MDBOOK017]
# No configuration options

# MDBOOK021 - {{#title}} directive should appear only once per chapter
# [MDBOOK021]
# No configuration options

# MDBOOK022 - {{#title}} directive should appear near the top of the file
# [MDBOOK022]
# max_line = 10  # Maximum line number for title directive

# MDBOOK023 - Chapter titles in SUMMARY.md should match H1 headers
# [MDBOOK023]
# No configuration options

# MDBOOK025 - Multiple H1 headings allowed in SUMMARY.md
# [MDBOOK025]
# No configuration options

# ============================================================================
# PREPROCESSOR CONFIGURATION
# ============================================================================

# Configuration for mdBook preprocessor mode
# [preprocessor]
# fail-on-warnings = true  # Fail the build on warnings
# renderer = ["html", "pdf"]  # Run for specific renderers

Common Configuration Patterns

Baseline for Incremental Adoption

For an existing documentation set, begin with the maintained low-noise preset:

preset = "baseline"
fail-on-warnings = true

# Optional project-specific subtraction.
# disabled-rules = ["MD014"]

Use mdbook-lint rules --preset baseline to explain the exact membership. Remove the preset setting when the project is ready for every stable default rule. See examples/baseline.mdbook-lint.toml for the standalone example.

Minimal Configuration

For most projects, a minimal configuration is sufficient:

# .mdbook-lint.toml
fail-on-warnings = true
disabled-rules = ["MD013"]  # Disable line length if not needed

Strict Configuration

For projects requiring strict markdown compliance:

# Fail on any issues
fail-on-warnings = true

# Strict whitespace rules
[MD009]
strict = true  # No trailing spaces at all

[MD010]
code_blocks = true  # Check tabs in code blocks

# Require code block languages
[MD040]
language_optional = false

# Strict line length
[MD013]
line_length = 80
strict = true

Documentation Project

For technical documentation or mdBook projects:

# mdBook-specific checks
[MDBOOK002]
check_anchors = true
check_images = true

[MDBOOK005]
ignore_patterns = ["drafts/**", "archive/**"]

# Allow longer lines for documentation
[MD013]
line_length = 100
code_blocks = false  # Don't check code block line length
tables = false  # Don't check table line length

# Require proper code highlighting
[MD040]
language_optional = false

Blog or Content Site

For blogs or content-heavy sites:

# Relaxed rules for content
disabled-rules = [
    "MD013",  # No line length limit
    "MD033",  # Allow inline HTML
    "MD041"   # First line doesn't need to be H1
]

# Allow emphasis for styling
[MD036]
punctuation = ""  # Don't check for punctuation

# Consistent emphasis style
[MD049]
style = "asterisk"

[MD050]
style = "asterisk"

Integration Configurations

GitHub Actions

# For CI/CD pipelines
fail-on-warnings = true

mdBook Preprocessor

[preprocessor]
fail-on-warnings = false  # Warning but don't fail build
renderer = ["html"]  # Only run for HTML output

Rule Categories Quick Reference

Disable All Rules in a Category

# Disable all heading rules
disabled-rules = [
    "MD001", "MD002", "MD003", "MD018", "MD019",
    "MD020", "MD021", "MD022", "MD023", "MD024",
    "MD025", "MD026"
]

# Disable all whitespace rules
disabled-rules = [
    "MD009", "MD010", "MD012", "MD027", "MD028", "MD047"
]

# Disable all list rules
disabled-rules = [
    "MD004", "MD005", "MD006", "MD007", "MD029",
    "MD030", "MD032"
]

Tips

  1. Start minimal: Use preset = "baseline" for an existing corpus, then remove it when ready for the full stable set
  2. Document choices: Comment why certain rules are disabled
  3. Version control: Commit .mdbook-lint.toml to your repository
  4. Team agreement: Discuss and agree on rules with your team

Next Steps

API Documentation

This page provides comprehensive API documentation for mdbook-lint's core libraries and rule implementations.

Core Library (mdbook-lint-core)

The mdbook-lint-core crate provides the foundational infrastructure for markdown linting.

Overview

The mdbook-lint-core crate provides:

  • Plugin-based architecture for extensible rule sets
  • AST and text-based linting with efficient document processing
  • Violation reporting with detailed position tracking and severity levels
  • Automatic fix infrastructure for correctable violations
  • Configuration system for customizing rule behavior
  • Document abstraction with markdown parsing via comrak

Architecture

The core follows a plugin-based architecture where rules are provided by external crates:

┌─────────────────┐
│   Application   │
└────────┬────────┘
         │
┌────────▼────────┐
│  PluginRegistry │ ◄─── Registers rule providers
└────────┬────────┘
         │
┌────────▼────────┐
│   LintEngine    │ ◄─── Orchestrates linting
└────────┬────────┘
         │
┌────────▼────────┐
│     Rules       │ ◄─── Individual rule implementations
└─────────────────┘

Key Components

Document Processing

  • Document - Represents a markdown file with content and metadata
  • Position - Tracks line/column positions for violations
  • NodeContext - Provides AST node information during linting

Rule System

  • Rule trait - Core interface for all linting rules
  • AstRule - Rules that process markdown AST nodes
  • TextRule - Rules that process raw text content
  • RuleProvider - Plugin interface for rule registration

Violation Reporting

  • Violation - Represents a linting issue with location and severity
  • Severity - Error, Warning, or Info levels
  • Fix - Automatic fix information for violations

Engine and Registry

  • LintEngine - Main orchestrator for document linting
  • PluginRegistry - Manages rule providers and configuration

Rulesets Library (mdbook-lint-rulesets)

The mdbook-lint-rulesets crate implements all linting rules.

Rule Implementation

The mdbook-lint-rulesets crate implements the actual linting rules used by mdbook-lint. It provides:

  • 55 standard markdown rules (MD001-MD060) based on the markdownlint specification
  • 18 mdBook-specific rules (MDBOOK001-MDBOOK025) for mdBook project validation
  • 10 content rules (CONTENT001-CONTENT011) for content quality checks
  • Automatic fix support for many rules to correct issues automatically
  • Configurable rules with sensible defaults

Rule Categories

Standard Markdown Rules (MD001-MD059)

These rules cover common markdown style and formatting issues:

  • Heading rules (MD001-MD003, MD018-MD025): Heading hierarchy, style, and formatting
  • List rules (MD004-MD007, MD029-MD032): List formatting, indentation, and consistency
  • Whitespace rules (MD009-MD012, MD027-MD028): Trailing spaces, blank lines, tabs
  • Link rules (MD034, MD039, MD042): URL formatting and link text
  • Code rules (MD038, MD040, MD046, MD048): Code block formatting and fencing
  • Emphasis rules (MD036-MD037, MD049-MD050): Bold and italic formatting

mdBook-Specific Rules (MDBOOK001-MDBOOK012, MDBOOK025)

These rules validate mdBook-specific requirements:

  • MDBOOK001: Code blocks should have language tags for proper syntax highlighting
  • MDBOOK002: Validate internal link paths and anchors
  • MDBOOK003: SUMMARY.md should follow proper mdBook structure
  • MDBOOK005: Detect orphaned files not referenced in SUMMARY.md
  • MDBOOK006: Validate cross-reference links between chapters
  • MDBOOK007: Validate file include syntax and paths
  • MDBOOK008: Check rustdoc_include directive usage
  • MDBOOK009: Validate playground directive syntax
  • MDBOOK010: Check for invalid math block syntax
  • MDBOOK011: Validate template include syntax
  • MDBOOK012: Check file include range syntax
  • MDBOOK025: Ensure proper heading structure in SUMMARY.md

Automatic Fixes

Many rules support automatic fixing to correct violations:

The public Fix coordinate contract is exact and half-open: start is included and end is excluded. Position lines and columns are 1-based, and columns count Unicode scalar values—not UTF-8 bytes, grapheme clusters, or display cells. Use Fix::byte_range(content) to obtain a validated UTF-8 byte range instead of maintaining rule-specific conversions.

Line endings are explicit. A position at a line's end is before its terminator; column 1 of the following line is after the complete LF or CRLF terminator. Therefore:

  • start == end is an insertion and consumes no source text.
  • Replacing line content without its terminator ends at that line's end column.
  • Replacing a complete terminated line ends at column 1 of the next line.
  • CRLF is atomic; no valid position falls between \r and \n.
  • At EOF, a replacement adds a newline only when its replacement text contains one explicitly.

Rule authors can use Fix::insertion, Fix::line_replacement, and Fix::line_range_replacement to make these distinctions explicit.

Whitespace and Formatting

  • MD009: Remove trailing spaces while preserving line breaks
  • MD010: Convert tabs to spaces with configurable spacing
  • MD012: Remove excessive consecutive blank lines
  • MD018: Add space after hash in headings
  • MD019: Remove multiple spaces after hash in headings
  • MD020: Add missing spaces inside closed ATX headings
  • MD021: Remove multiple spaces inside hash-surrounded headings
  • MD023: Remove indentation from headings
  • MD027: Remove spaces after blockquote markers
  • MD030: Ensure proper spacing after list markers
  • MD034: Replace bare URLs with proper link syntax
  • MD047: Ensure files end with single newline

Coming Soon

  • Additional formatting rules
  • More sophisticated content restructuring
  • Context-aware fixes for complex violations

Usage Examples

Basic Library Usage

#![allow(unused)]
fn main() {
use mdbook_lint_core::{PluginRegistry, LintEngine};
use mdbook_lint_rulesets::{StandardRuleProvider, MdBookRuleProvider};

// Create a registry with standard rules
let mut registry = PluginRegistry::new();
registry.register(StandardRuleProvider::new())?;
registry.register(MdBookRuleProvider::new())?;

// Create engine and lint a document
let engine = LintEngine::from_registry(registry);
let document = Document::from_file("README.md")?;
let violations = engine.lint_document(&document)?;
}

Custom Rule Implementation

#![allow(unused)]
fn main() {
use mdbook_lint_core::{Document, AstRule, Violation, Position};
use comrak::nodes::{AstNode, NodeValue};

pub struct MyCustomRule;

impl AstRule for MyCustomRule {
    fn id(&self) -> &'static str { "CUSTOM001" }
    fn description(&self) -> &'static str { "My custom rule" }
    
    fn lint_ast(&self, document: &Document, node: &AstNode) -> Vec<Violation> {
        // Your custom linting logic here
        Vec::new()
    }
}
}

Configuration Usage

#![allow(unused)]
fn main() {
use mdbook_lint_core::Config;

let config = Config::from_file(".mdbook-lint.toml")?;
let engine = LintEngine::from_config(config)?;
}

Individual Rule Documentation

Each rule provides detailed documentation including:

  • Purpose and rationale - Why the rule exists
  • Examples - Correct and incorrect markdown
  • Configuration options - Customizable behavior
  • Automatic fixes - What fixes are available
  • Related rules - Connected or overlapping rules

MD001 - Heading Increment

Ensures heading levels increment sequentially for proper document structure.

MD009 - No Trailing Spaces (Auto-fix)

Removes trailing whitespace while preserving intentional line breaks.

MDBOOK001 - Code Block Language Tags

Ensures code blocks have language tags for proper syntax highlighting in mdBook.

Quick Rule Reference

RuleDescriptionAuto-fix
MD001Heading increment
MD009No trailing spaces
MD010Hard tabs
MD012Multiple blank lines
MD018No space after hash
MD019Multiple spaces after hash
MD020Missing space in closed headings
MD021Multiple spaces in closed headings
MD023Headings start at beginning
MD027Multiple spaces after blockquote
MD030Spaces after list markers
MD034Bare URL used
MD047Files should end with newline

✓ indicates automatic fix support

Full API Reference

For complete API documentation with all types, traits, and functions:

Generate Documentation

# Generate and open documentation
cargo doc --open

# Generate documentation for all features
cargo doc --all-features --open

# Generate documentation without dependencies
cargo doc --no-deps --open

Online Documentation

Integration Patterns

mdBook Preprocessor

# book.toml
[preprocessor.lint]
fail-on-warnings = true

CI/CD Integration

# Fail build on any violations
mdbook-lint lint --fail-on-warnings docs/

# Auto-fix and commit changes
mdbook-lint lint --fix docs/
git add docs/
git commit -m "docs: auto-fix markdown violations"

Editor Integration

Configure your editor to run mdbook-lint on save for real-time feedback.

Tool Comparison: mdbook-lint vs markdownlint

This document analyzes the differences in linting behavior between mdbook-lint and markdownlint when run on the same documentation.

Performance Metrics

Metricmdbook-lintmarkdownlintDifference
Execution Time0.101s0.283smdbook-lint is 2.8x faster
Total Violations1,390818mdbook-lint finds 70% more
Standard Rules Only1,018818mdbook-lint finds 24% more

Rule-by-Rule Analysis

Rules with Similar Detection (±10% difference)

These rules show consistent behavior between both tools:

RuleDescriptionmdbook-lintmarkdownlint
MD022Headings surrounded by blanks177177
MD031Code blocks surrounded by blanks200202
MD032Lists surrounded by blanks181174
MD047Files end with newline3737
MD051Link fragments4747

Major Discrepancies

Rules Where mdbook-lint Finds More Violations

RuleDescriptionmdbook-lintmarkdownlintAnalysis
MD013Line length179115mdbook-lint is 55% stricter
MD007List indentation460markdownlint doesn't check inside code blocks
MD052Reference links240mdbook-lint validates undefined references
MD006Lists start at beginning210mdbook-lint enforces list positioning
MD058Tables surrounded by blanks112mdbook-lint has stricter table detection

Rules Only Detected by mdbook-lint

These standard markdown rules are caught by mdbook-lint but not markdownlint in our docs:

  • MD014 (1): Dollar signs in shell code
  • MD018 (8): No space after hash in headings
  • MD019 (10): Multiple spaces after hash
  • MD020 (14): Missing space in closed headings
  • MD021 (8): Multiple spaces in closed headings
  • MD023 (12): Headings not at line beginning
  • MD027 (4): Multiple spaces after blockquote
  • MD028 (1): Blank line inside blockquote
  • MD030 (1): Spaces after list markers
  • MD033 (6): Inline HTML
  • MD035 (2): Horizontal rule style
  • MD039 (1): Spaces inside link text
  • MD044 (10): Proper names capitalization
  • MD050 (1): Strong style consistency

Root Cause Analysis

1. Code Block Processing (MD007)

Issue: mdbook-lint reports 46 MD007 violations, markdownlint reports 0

Example:

steps:
  - uses: actions/checkout@v4  # mdbook-lint flags this indentation

Analysis: mdbook-lint appears to be checking list indentation rules inside code blocks, which is incorrect. Code blocks should be treated as literal content.

2. Line Length Strictness (MD013)

Issue: mdbook-lint finds 179 violations vs markdownlint's 115

Analysis: Both tools use 80-character default, but mdbook-lint may:

  • Count differently (e.g., including/excluding certain characters)
  • Check more contexts (e.g., inside certain structures)
  • Have different handling of Unicode or special characters

Issue: mdbook-lint finds 24 violations, markdownlint finds 0

Example from contributing.md:335-337:

[ ]  # mdbook-lint flags as undefined reference

Analysis: mdbook-lint validates that reference links actually have definitions, while markdownlint may only check syntax.

4. Whitespace Rules (MD018-MD021, MD023, MD027)

Issue: mdbook-lint finds 42 total violations across these rules, markdownlint finds 0

Analysis: mdbook-lint has more comprehensive whitespace checking around:

  • Heading markers (MD018-MD021)
  • Heading indentation (MD023)
  • Blockquote markers (MD027)

mdBook-Specific Rules

mdbook-lint includes 372 additional violations from mdBook-specific rules:

RuleCountPurpose
MDBOOK002157Internal link validation
MDBOOK00775File include syntax
MDBOOK00546Orphaned files detection
MDBOOK00130Code blocks need language tags
MDBOOK01221File include ranges
MDBOOK00813Rustdoc include validation
Others30Various mdBook features

Recommendations

For mdbook-lint

  1. Fix MD007: Should not check list indentation inside code blocks
  2. Document MD013: Clarify how line length is calculated
  3. Configuration alignment: Consider a markdownlint-compatible mode that matches behavior exactly

For Users

  1. Choose based on needs:
  • mdbook-lint: Better for mdBook projects, stricter checking, faster performance

  • markdownlint: Better for general markdown, more mature, wider ecosystem

  1. Configuration tips:
  • Disable MD007 in mdbook-lint if false positives in code blocks are problematic

  • Adjust MD013 line length if 80 characters is too strict

  • Use markdownlint-compatible flag for closer behavior matching

Conclusion

mdbook-lint is significantly stricter and faster than markdownlint, finding 70% more violations overall. The main differences stem from:

  1. Bug: MD007 checking inside code blocks (should be fixed)
  2. Design: Stricter validation of references, whitespace, and formatting
  3. Feature: mdBook-specific rules add valuable checks for mdBook projects

For mdBook projects, mdbook-lint provides superior coverage. For general markdown, the choice depends on whether stricter checking is desired.

Library API

mdbook-lint is designed as a library-first project, making it easy to integrate markdown linting capabilities into your own Rust applications.

Overview

The core library (mdbook-lint-core) provides a clean, well-documented API for:

  • Creating lint engines with different rule sets
  • Processing markdown documents programmatically
  • Configuring rules and behavior
  • Handling violations and results

Quick Start

Add mdbook-lint to your Cargo.toml:

[dependencies]
mdbook-lint-core = "0.3.0"

Basic usage:

use mdbook_lint_core::{Document, create_engine_with_all_rules};
use std::path::PathBuf;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Create a lint engine with all available rules
    let engine = create_engine_with_all_rules();
    
    // Create a document from content and path
    let content = "# My Document\n\nSome content here.";
    let document = Document::new(content.to_string(), PathBuf::from("example.md"))?;
    
    // Lint the document
    let violations = engine.lint_document(&document)?;
    
    // Process results
    for violation in violations {
        println!("{}:{}: {} - {}", 
            violation.line, 
            violation.column,
            violation.rule_id, 
            violation.message
        );
    }
    
    Ok(())
}

Core Types

Document

Represents a markdown document with content and metadata:

#![allow(unused)]
fn main() {
use mdbook_lint_core::Document;
use std::path::PathBuf;

let document = Document::new(
    "# Title\n\nContent".to_string(),
    PathBuf::from("my-file.md")
)?;
}

LintEngine

The main interface for linting operations:

#![allow(unused)]
fn main() {
use mdbook_lint_core::{LintEngine, create_engine_with_all_rules, create_standard_engine};

// Engine with all rules (standard + mdBook-specific)
let all_engine = create_engine_with_all_rules();

// Engine with only standard markdown rules (MD001-MD059)
let standard_engine = create_standard_engine();

// Engine with only mdBook-specific rules
let mdbook_engine = create_mdbook_engine();
}

Configuration

Control which rules are enabled and configure their behavior:

#![allow(unused)]
fn main() {
use mdbook_lint_core::Config;

let mut config = Config::default();

// Enable specific rules only
config.enabled_rules = vec!["MD001".to_string(), "MD013".to_string()];

// Disable specific rules
config.disabled_rules = vec!["MD002".to_string()];

// Enable specific categories
config.enabled_categories = vec!["structure".to_string()];

// Lint with configuration
let violations = engine.lint_document_with_config(&document, &config)?;
}

Violations

Results from linting operations:

#![allow(unused)]
fn main() {
use mdbook_lint_core::{Violation, Severity};

// Violations contain detailed information about issues found
for violation in violations {
    println!("Rule: {}", violation.rule_id);
    println!("Message: {}", violation.message);
    println!("Location: {}:{}", violation.line, violation.column);
    
    match violation.severity {
        Severity::Error => println!("This is an error"),
        Severity::Warning => println!("This is a warning"),
        Severity::Info => println!("This is informational"),
    }
}
}

Fix coordinates

Fix ranges are exact and half-open (start included, end excluded). Position uses 1-based lines and 1-based Unicode-scalar columns. Embedders that edit UTF-8 strings should call fix.byte_range(content) for the canonical, validated conversion rather than interpreting columns as byte offsets.

The end-of-line position is before the line terminator. Column 1 of the next line is after the entire LF or CRLF terminator, so CRLF cannot be split. An equal start and end is a pure insertion; a complete-line replacement consumes the terminator only when its end is on the following line. EOF never implies a newline.

#![allow(unused)]
fn main() {
use mdbook_lint_core::{Fix, Position};

let content = "café\r\nnext";
let fix = Fix {
    description: "Replace the accented scalar".to_string(),
    replacement: Some("e".to_string()),
    start: Position { line: 1, column: 4 },
    end: Position { line: 1, column: 5 },
};

assert_eq!(fix.byte_range(content), Some(3..5));
}

Advanced Usage

Custom Rule Providers

Create engines with specific rule sets:

#![allow(unused)]
fn main() {
use mdbook_lint_core::{PluginRegistry, StandardRuleProvider};

let mut registry = PluginRegistry::new();
registry.register_provider(Box::new(StandardRuleProvider))?;

let engine = registry.create_engine()?;
}

Error Handling

The library uses anyhow for comprehensive error handling:

#![allow(unused)]
fn main() {
use mdbook_lint_core::error::Result;

fn lint_file(path: &Path) -> Result<Vec<Violation>> {
    let content = std::fs::read_to_string(path)?;
    let document = Document::new(content, path.to_path_buf())?;
    
    let engine = create_engine_with_all_rules();
    engine.lint_document(&document)
}
}

Batch Processing

Process multiple documents efficiently:

#![allow(unused)]
fn main() {
use walkdir::WalkDir;

let engine = create_engine_with_all_rules();
let mut all_violations = Vec::new();

for entry in WalkDir::new("src/") {
    let entry = entry?;
    if entry.path().extension().map_or(false, |ext| ext == "md") {
        let content = std::fs::read_to_string(entry.path())?;
        let document = Document::new(content, entry.path().to_path_buf())?;
        
        let violations = engine.lint_document(&document)?;
        all_violations.extend(violations);
    }
}
}

Rule Categories

Rules are organized into logical categories:

  • Structure: Document structure and hierarchy (MD001, MD003, etc.)
  • Formatting: Code blocks, lists, emphasis (MD004, MD005, etc.)
  • Content: Language, spelling, accessibility (MD044, MD045, etc.)
  • Links: URL validation and formatting (MD034, MD039, etc.)
  • MdBook: mdBook-specific checks (MDBOOK001-004)

Enable categories programmatically:

#![allow(unused)]
fn main() {
let mut config = Config::default();
config.enabled_categories = vec![
    "structure".to_string(),
    "formatting".to_string()
];
}

Integration Examples

mdBook Preprocessor

#![allow(unused)]
fn main() {
use mdbook::preprocess::{Preprocessor, PreprocessorContext};
use mdbook::book::Book;
use mdbook_lint_core::create_engine_with_all_rules;

struct MyLinter {
    engine: LintEngine,
}

impl Preprocessor for MyLinter {
    fn run(&self, ctx: &PreprocessorContext, book: Book) -> mdbook::errors::Result<Book> {
        // Lint each chapter
        // Return book unchanged (linting only)
        Ok(book)
    }
}
}

CI/CD Integration

use mdbook_lint_core::{create_engine_with_all_rules, Severity};

fn main() -> std::process::ExitCode {
    let engine = create_engine_with_all_rules();
    let mut has_errors = false;
    
    // Process all markdown files in repository
    // Set exit code based on results
    
    if has_errors {
        std::process::ExitCode::FAILURE
    } else {
        std::process::ExitCode::SUCCESS
    }
}

API Documentation

For complete API documentation with examples, see the rustdoc documentation:

The rustdoc includes:

  • Complete API reference with examples
  • Module-level documentation
  • Implementation details and internal architecture
  • Links between related types and functions

Performance Considerations

  • Single-pass parsing: Documents are parsed once and reused across all rules
  • Lazy evaluation: Rules are only applied to relevant document sections
  • Memory efficient: Minimal AST retention, streaming for large files
  • Parallel processing: Use rayon or similar for batch operations

Error Types

The library defines specific error types for different failure modes:

  • ConfigError: Configuration parsing and validation issues
  • DocumentError: Document creation and parsing problems
  • RuleError: Rule execution failures
  • IoError: File system access problems

Next Steps

Contributing to mdbook-lint

Thank you for your interest in contributing to mdbook-lint! This guide covers everything you need to know to contribute effectively.

Quick Start

Prerequisites

  • Rust 1.88.0 or later
  • Git

Development Setup

# Fork and clone the repository
git clone https://github.com/YOUR_USERNAME/mdbook-lint.git
cd mdbook-lint

# Set up development environment
cargo build
cargo test
cargo fmt
cargo clippy

Making Your First Contribution

  1. Create a branch: git checkout -b feature/your-change
  2. Make changes: Follow the guidelines below
  3. Test thoroughly: Ensure all tests pass
  4. Submit PR: Use conventional commit format

Project Structure

mdbook-lint/
├── src/
│   ├── main.rs              # CLI entry point
│   ├── lib.rs               # Library entry point
│   ├── engine.rs            # Core linting engine
│   ├── config.rs            # Configuration system
│   ├── document.rs          # Markdown document processing
│   ├── rule.rs              # Rule trait definitions
│   ├── rules/               # Rule implementations
│   │   ├── standard/        # Standard markdown rules (MD001-MD059)
│   │   ├── mdbook001.rs     # mdBook-specific rules
│   │   └── ...
│   └── preprocessor.rs      # mdBook preprocessor integration
├── tests/                   # Integration tests
├── docs/                    # This documentation site
└── scripts/                 # Development utilities

Code Standards

Rust Style

  • Formatting: Use cargo fmt (enforced in CI)
  • Linting: Fix all cargo clippy warnings
  • Error Handling: Use Result<T> types, avoid .unwrap()
  • Documentation: Document all public APIs with rustdoc
  • Testing: Comprehensive unit and integration tests required

Commit Format

We use Conventional Commits:

<type>[scope]: <description>

feat(rules): add MD040 rule for fenced code blocks
fix(cli): handle empty files correctly
docs: update installation instructions
test: add edge cases for rule validation
refactor: simplify config parsing logic

Types: feat, fix, docs, test, refactor, perf, chore, ci Scopes: rules, cli, config, engine, docs, tests

Branch Naming

<type>/<description>

feature/md040-code-block-language
fix/empty-file-handling
docs/contributing-guide
refactor/rule-registry-cleanup

Adding New Rules

Rule Types

Line-based Rules (implement Rule trait):

  • Simple checks on raw text lines
  • Faster execution, lower memory usage
  • Good for formatting rules

AST-based Rules (implement AstRule trait):

  • Complex semantic analysis
  • Full markdown structure access
  • Required for structural rules

Implementation Example

#![allow(unused)]
fn main() {
use crate::rule::{AstRule, RuleCategory, RuleMetadata};
use crate::{Document, violation::{Severity, Violation}};
use comrak::nodes::{AstNode, NodeValue};

pub struct MD999;

impl AstRule for MD999 {
    fn id(&self) -> &'static str {
        "MD999"
    }

    fn name(&self) -> &'static str {
        "example-rule"
    }

    fn description(&self) -> &'static str {
        "Example rule for demonstration"
    }

    fn metadata(&self) -> RuleMetadata {
        RuleMetadata::stable(RuleCategory::Structure)
    }

    fn check_ast<'a>(
        &self,
        document: &Document,
        ast: &'a AstNode<'a>,
    ) -> crate::error::Result<Vec<Violation>> {
        let mut violations = Vec::new();
        
        // Walk AST and check for violations
        for node in ast.descendants() {
            if let NodeValue::Heading(heading) = &node.data.borrow().value {
                if heading.level > 6 {
                    if let Some((line, col)) = document.node_position(node) {
                        violations.push(self.create_violation(
                            "Heading level exceeds maximum".to_string(),
                            line,
                            col,
                            Severity::Error,
                        ));
                    }
                }
            }
        }
        
        Ok(violations)
    }
}

[cfg(test)]

mod tests {
    use super::*;
    use crate::rule::Rule;
    use std::path::PathBuf;

[test]

    fn test_md999_valid_headings() {
        let content = "# H1\n## H2\n### H3\n";
        let document = Document::new(content.to_string(), PathBuf::from("test.md")).unwrap();
        let rule = MD999;
        let violations = rule.check(&document).unwrap();
        assert_eq!(violations.len(), 0);
    }

[test]

    fn test_md999_detects_violations() {
        let content = "####### Invalid heading level";
        let document = Document::new(content.to_string(), PathBuf::from("test.md")).unwrap();
        let rule = MD999;
        let violations = rule.check(&document).unwrap();
        assert_eq!(violations.len(), 1);
        assert_eq!(violations[0].rule_id, "MD999");
    }
}
}

Rule Registration

  1. Add module: Include your rule in src/rules/standard/mod.rs
  2. Register rule: Add to StandardRuleProvider::register_rules()
  3. Add to rule list: Include ID in StandardRuleProvider::rule_ids()

Cross-document rules implement CollectionRule and are registered with register_collection_rule() instead. They live in a separate registry list, so anything that enumerates rules must consult both.

Public surfaces a rule must update

A rule is not just an implementation. Adding or changing one touches several surfaces that are expected to agree, and crates/mdbook-lint-cli/tests/rule_contract_test.rs fails in CI when they drift.

SurfaceLocationRequired
Registrationprovider register_rules and rule_idsAlways
Documentation pagedocs/src/rules/<ruleset>/<id>.mdAlways
Book navigationdocs/src/SUMMARY.mdFor a new page
Example configurationcrates/mdbook-lint-cli/example-mdbook-lint.tomlIf the rule takes options
MetadataRuleMetadata category, stability, introduced_inAlways

The contract test names the exact ID and surface when something is missing, so read the failure rather than guessing.

Two conventions it enforces:

  • Rule names are lowercase kebab-case (heading-increment), distinct from the uppercase ID (MD001).
  • Reserved numbers are never registered. A reserved rule is a placeholder for a markdownlint number that was never implemented here; its module exists but the provider skips it.

New rules should be RuleMetadata::experimental(..) until their diagnostics settle. Experimental rules do not run by default and must be opted into with --enable <RULE> or experimental-rules, which lets them be dogfooded without exposing every user to unstable output. Promote to stable in a later release.

Testing Requirements

Comprehensive testing is required:

  • Test valid cases (no violations)
  • Test violation detection
  • Test edge cases (empty files, very long lines, unicode)
  • Test configuration options (if applicable)
  • Test error conditions

Configuration System

Rule Configuration

Rules can accept configuration through rule_config:

#![allow(unused)]
fn main() {
fn check_ast(&self, document: &Document, ast: &AstNode) -> Result<Vec<Violation>> {
    let config = document.config.rule_config
        .get(self.id())
        .and_then(|v| v.as_object());
        
    let max_length = config
        .and_then(|c| c.get("max-length"))
        .and_then(|v| v.as_u64())
        .unwrap_or(100) as usize;
        
    // Use configuration in rule logic
}
}

Supported Formats

Configuration files can be TOML, YAML, or JSON:

# .mdbook-lint.toml
fail-on-warnings = true
enabled-rules = ["MD001", "MD013"]

[MD013]
line-length = 120
ignore-code-blocks = true

CLI Development

Adding Commands

Add new commands to the Commands enum in src/main.rs:

#![allow(unused)]
fn main() {
[derive(Subcommand)]

enum Commands {
    /// Lint markdown files
    Lint {
        files: Vec<String>,
[arg(short, long)]

        config: Option<String>,
    },
    
    /// Your new command
    NewCommand {
        input: PathBuf,
[arg(long)]

        option: bool,
    },
}
}

Command Implementation

Create handler functions with proper error handling:

#![allow(unused)]
fn main() {
fn run_new_command(input: PathBuf, option: bool) -> Result<()> {
    // Validate input
    if !input.exists() {
        return Err(MdBookLintError::config_error(
            format!("Path does not exist: {}", input.display())
        ));
    }
    
    // Implementation
    Ok(())
}
}

Testing

Running Tests

# Unit tests
cargo test --lib

# Integration tests  
cargo test --test '*'

# All tests
cargo test

# Specific test
cargo test test_name -- --exact

# With output
cargo test test_name -- --nocapture

Test Organization

  • Unit tests: In same file with #[cfg(test)]
  • Integration tests: In tests/ directory
  • CLI tests: Use assert_cmd crate
  • Fixtures: Test data in tests/fixtures/

Writing Good Tests

#![allow(unused)]
fn main() {
[test]

fn test_descriptive_name() {
    // Arrange
    let input = "test input";
    let expected = "expected output";
    
    // Act
    let result = function_under_test(input);
    
    // Assert
    assert_eq!(result, expected);
}
}

Pull Request Guidelines

Before Submitting

  • All tests pass: cargo test
  • Code is formatted: cargo fmt
  • No clippy warnings: cargo clippy
  • Documentation updated (if needed)
  • Commit follows conventional format

PR Template

## Description
Brief description of changes and motivation

## Type of Change
- [ ] Bug fix
- [ ] New feature  
- [ ] Documentation update
- [ ] Refactoring

## Testing
- [ ] Tests pass locally
- [ ] Added tests for new functionality
- [ ] Manual testing completed

## Checklist
- [ ] Code follows project style
- [ ] Self-review completed
- [ ] Documentation updated

Review Process

  1. Automated checks run on all PRs
  2. Maintainer review for code quality
  3. Testing to ensure functionality
  4. Merge when approved and passing

Architecture Overview

Core Components

LintEngine (src/engine.rs):

  • Orchestrates linting process
  • Manages rule execution
  • Aggregates results

Rule System (src/rule.rs, src/rules/):

  • Defines Rule and AstRule traits
  • Implements linting logic
  • Categorizes by type and stability

Document Processing (src/document.rs):

  • Parses markdown using comrak
  • Provides position tracking
  • Handles various formats

Configuration (src/config.rs):

  • Multi-format support (TOML/YAML/JSON)
  • Rule-specific settings
  • Precedence handling

Data Flow

Input Files → Document Parser → Lint Engine → Rules → Violations → Output
     ↓              ↓              ↓          ↓         ↓         ↓
  .md files    AST + Lines    Rule Registry  Checks   Results   CLI/JSON

Common Tasks

Debug Rule Issues

# Enable debug logging
export RUST_LOG=mdbook_lint=debug

# Run with backtrace
export RUST_BACKTRACE=1

# Test specific rule
cargo test md001 -- --nocapture

Profile Performance

# Install tools
cargo install flamegraph

# Profile application
cargo build --release
sudo flamegraph -- ./target/release/mdbook-lint lint large-file.md

Update Dependencies

# Check for updates
cargo outdated

# Update Cargo.lock
cargo update

# Update Cargo.toml versions
cargo upgrade

Project Conventions

Naming Standards

  • Files: snake_case.rs
  • Structs/Enums: PascalCase
  • Functions/Variables: snake_case
  • Constants: SCREAMING_SNAKE_CASE
  • Rules: MD### or MDBOOK###

Documentation Style

  • Simple, clear, and factual
  • Include working code examples
  • No marketing language
  • Professional tone throughout
  • Link to related documentation

Error Messages

  • Clear and actionable
  • Include relevant context
  • Suggest fixes when possible
  • Consistent formatting

Example:

"Missing language tag for code block at line 15, column 1
Consider adding a language identifier: ```rust"

Release Process

Versioning

We use Semantic Versioning:

  • MAJOR: Breaking changes
  • MINOR: New features (backward compatible)
  • PATCH: Bug fixes

Release Workflow

  1. Changes merged to main via PR
  2. Release-please creates release PR automatically
  3. Merge release PR to trigger release
  4. GitHub Actions publishes to crates.io

Getting Help

Resources

Common Questions

Q: How do I add a new rule? A: Follow the "Adding New Rules" section above. Start with the rule template and add comprehensive tests.

Q: Why did my PR fail CI? A: Check that cargo test, cargo fmt, and cargo clippy all pass locally.

Q: How do I test mdBook integration? A: Use the MdBookLint preprocessor in your tests. See existing integration tests for examples.

Q: Can I contribute documentation improvements? A: Absolutely! Documentation improvements are highly valued. Edit the files in docs/src/.

Community Guidelines

  • Be respectful and constructive
  • Help others learn and contribute
  • Follow our professional standards
  • Ask questions when unclear
  • Provide helpful feedback in reviews

Thank you for contributing to mdbook-lint! Your efforts help make documentation better for everyone.

Architecture

This page provides an overview of mdbook-lint's internal architecture and design decisions.

High-Level Architecture

mdbook-lint is built as a modular Rust application with clear separation of concerns:

┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│   CLI Interface │    │  mdBook Plugin  │    │  Library Core   │
└─────────────────┘    └─────────────────┘    └─────────────────┘
         │                       │                       │
         └───────────────────────┼───────────────────────┘
                                 │
                    ┌─────────────────┐
                    │   Lint Engine   │
                    └─────────────────┘
                             │
          ┌──────────────────┼──────────────────┐
          │                  │                  │
  ┌───────────────┐  ┌───────────────┐  ┌───────────────┐
  │ Rule Registry │  │ Configuration │  │ File Parser   │
  └───────────────┘  └───────────────┘  └───────────────┘
          │
     ┌────┴────┐
     │  Rules  │
     └─────────┘

Core Components

Lint Engine

The central component that orchestrates the linting process:

  • Loads and validates configuration
  • Discovers and parses markdown files
  • Applies rules to parsed content
  • Collects and formats results

Rule System

A plugin-like architecture for linting rules:

  • Each rule implements a common Rule trait
  • Rules are automatically registered at compile time
  • Configurable rule parameters
  • Extensible for new rule types

Parser

Handles markdown parsing and AST generation:

  • Uses comrak for CommonMark compliance
  • Maintains source position information
  • Provides structured access to document elements

Configuration

Flexible configuration system:

  • TOML-based configuration files
  • Command-line argument override
  • Environment variable support
  • Validation and error reporting

Data Flow

  1. Input Processing
  • Command-line arguments parsed

  • Configuration files loaded and merged

  • File paths resolved and validated

  1. Document Processing
  • Markdown files parsed into AST

  • Source position tracking maintained

  • Document metadata extracted

  1. Rule Application
  • Enabled rules identified

  • Rules applied to document AST

  • Violations collected with positions

  1. Output Generation
  • Results formatted for display

  • Exit codes determined

  • Statistics calculated

Rule Implementation

Rules follow a consistent pattern:

#![allow(unused)]
fn main() {
pub struct ExampleRule {
    config: ExampleConfig,
}

impl Rule for ExampleRule {
    fn id(&self) -> &'static str {
        "MD001"
    }

    fn description(&self) -> &'static str {
        "Rule description"
    }

    fn check(&self, document: &Document) -> Vec<Violation> {
        // Rule logic here
    }
}
}

Rule Categories

  • Standard Rules (MD001-MD059): Based on markdownlint
  • mdBook Rules (MDBOOK001-004): mdBook-specific checks
  • Custom Rules: Extensible for project-specific needs

Performance Considerations

Parsing Strategy

  • Single-pass parsing per document
  • AST reuse across multiple rules
  • Lazy evaluation where possible

Memory Management

  • Streaming file processing for large projects
  • Minimal AST retention
  • Efficient string handling

Concurrency

  • Parallel file processing
  • Thread-safe rule application
  • Configurable worker threads

Error Handling

Error Categories

  • Configuration Errors: Invalid settings, missing files
  • Parse Errors: Malformed markdown, encoding issues
  • Rule Errors: Internal rule failures
  • IO Errors: File system access problems

Error Recovery

  • Graceful degradation on individual file failures
  • Detailed error context and suggestions
  • Configurable error tolerance levels

Extension Points

Custom Rules

#![allow(unused)]
fn main() {
// Plugin-style rule loading
pub fn register_custom_rule<R: Rule + 'static>(rule: R) {
    RULE_REGISTRY.register(Box::new(rule));
}
}

Output Formats

  • JSON for machine consumption
  • SARIF for integration tools
  • Custom formatters via traits

Configuration Sources

  • Environment variables
  • External configuration services
  • Runtime configuration updates

Testing Architecture

Test Categories

  • Unit Tests: Individual rule logic
  • Integration Tests: End-to-end CLI testing
  • Corpus Tests: Real-world markdown validation
  • Performance Tests: Benchmarking and profiling

Test Infrastructure

  • Automated test case generation
  • Snapshot testing for output validation
  • Property-based testing for edge cases

Dependencies

Core Dependencies

  • comrak: Markdown parsing
  • serde: Configuration serialization
  • clap: Command-line interface
  • anyhow: Error handling

Development Dependencies

  • criterion: Performance benchmarking
  • tempfile: Test file management
  • assert_cmd: CLI testing

Future Architecture Considerations

Planned Enhancements

  • Plugin system for external rules
  • Language server protocol support
  • Real-time linting capabilities
  • Integration with popular editors

Scalability

  • Distributed linting for large repositories
  • Caching and incremental analysis
  • Cloud-based rule execution

Contributing to Architecture

When making architectural changes:

  1. Maintain backward compatibility
  2. Document design decisions
  3. Consider performance implications
  4. Ensure testability
  5. Follow Rust best practices

Next Steps

  • See Contributing for development guidelines
  • Check Rules Reference for rule implementation details
  • Review source code for implementation specifics

Testing Strategy

This document describes mdbook-lint's comprehensive testing approach, including our simplified corpus testing and performance validation framework.

Overview

mdbook-lint uses a multi-layered testing strategy designed for speed, reliability, and comprehensive coverage:

  • Unit Tests: Fast, focused tests for individual components
  • Integration Tests: End-to-end testing of CLI and preprocessor functionality
  • Corpus Tests: Real-world validation using diverse markdown content
  • Performance Tests: Regression testing for known performance issues
  • Property Tests: Fuzzing-style tests ensuring stability on any input

Corpus Testing

Our corpus testing validates correctness and stability using carefully selected real-world content.

Essential Corpus Files

Located in tests/corpus/essential/, these files test key scenarios:

  • empty_file.md: Edge case handling for empty files
  • unicode_content.md: International text and special characters
  • large_file.md: Performance testing with substantial content
  • mixed_line_endings.md: Cross-platform line ending handling
  • known_violations.md: Files with intentional violations for rule validation

Property-Based Testing

We use property-based testing to ensure mdbook-lint never crashes on any input:

#![allow(unused)]
fn main() {
// Example: Test with various malformed markdown
let malformed_cases = [
    "[unclosed link",
    "**unclosed emphasis", 
    "`unclosed code",
    "```\nunclosed code block",
];

for case in &malformed_cases {
    assert_no_crash(case, &format!("Malformed: {}", case));
}
}

This approach tests:

  • Random/invalid UTF-8 sequences
  • Malformed markdown syntax
  • Pathological nesting patterns
  • Mixed valid/invalid content
  • Binary-like content

Performance Testing

Performance tests prevent regressions in known problem areas and ensure consistent speed.

Regression Tests

Located in simple_performance_tests.rs, these tests target historical issues:

MD051 Performance Fix

Tests the O(n²) → O(n) optimization for HTML fragment validation:

#![allow(unused)]
fn main() {
// Previously caused exponential slowdown
let html_content = r##"
<a href="#section1">Link 1</a>
<a href="#section2">Link 2</a>
# Section 1 {#section1}
# Section 2 {#section2}
"##;

assert_completes_quickly(&document, Duration::from_millis(100));
}

MD049 Infinite Loop Fix

Tests the emphasis parsing fix that prevented infinite loops:

#![allow(unused)]
fn main() {
// Previously caused hangs with patterns like wrapping_*
let emphasis_content = r##"
- `wrapping_*` function calls in code
- `checked_*` operations in backticks
- Normal *emphasis* outside code should work
"##;

assert_completes_quickly(&document, Duration::from_millis(50));
}

Performance Targets

  • Small files (< 1KB): Complete in < 50ms
  • Medium files (1KB-10KB): Complete in < 100ms
  • Large files (> 10KB): Complete in < 500ms
  • Pathological cases: Complete in < 200ms

Test Execution

Local Testing

Run the full test suite:

# All tests
cargo test --all-features

# Specific test categories
cargo test --test simple_corpus_tests      # Corpus validation
cargo test --test simple_performance_tests # Performance regressions
cargo test --lib                          # Unit tests

CI Testing

Our CI runs tests across platforms:

  • Ubuntu, macOS, Windows: Cross-platform compatibility
  • Stable, Beta Rust: Forward compatibility
  • Essential corpus tests: Real-world validation
  • Performance benchmarks: Regression detection

Design Philosophy

Simplified Approach

We prioritize accuracy and stability over absolute benchmarks because mdbook-lint is already fast compared to other tools. Our testing focuses on:

  1. Correctness: Rules work as intended
  2. Stability: Never crash on any input
  3. Performance: Prevent regressions
  4. Maintainability: Simple, focused tests

Property Testing Benefits

Property-based testing provides confidence that mdbook-lint handles the unpredictable nature of real-world markdown:

  • User-generated content with encoding issues
  • Generated markdown from various tools
  • Partially corrupted files
  • Mixed content types

Performance Testing Strategy

Rather than complex benchmarking, we use targeted regression tests:

  • Known issues: Test specific problems that were fixed
  • Pathological inputs: Ensure reasonable performance on edge cases
  • Real content: Validate speed on actual documentation

Historical Context

This testing approach was simplified from a more complex framework that included:

  • External corpus downloads (The Rust Book, etc.)
  • markdownlint compatibility testing
  • Extensive generated edge cases
  • Complex nightly CI workflows

The current approach maintains essential coverage while being:

  • 10x faster to run locally
  • No external dependencies
  • Easier to understand and maintain
  • More reliable in CI environments

Contributing to Tests

When adding new functionality:

  1. Add unit tests for the specific feature
  2. Add integration tests if it affects CLI/preprocessor behavior
  3. Add corpus tests if it might affect stability
  4. Add performance tests if it's performance-sensitive

For bug fixes:

  1. Add a regression test that would have caught the bug
  2. Ensure it fails before your fix
  3. Verify it passes after your fix

See Contributing for detailed guidelines.