A declarative schema-based Markdown documentation validator that helps maintain consistent documentation structure across projects.
This README file itself is an example of how to use mdschema to validate and generate documentation.
mdschema check README.md --schema ./examples/README-schema.yml
β No violations found- Schema-driven validation - Define your documentation structure in simple YAML
- Hierarchical structure - Support for nested sections and complex document layouts
- Template generation - Generate markdown templates from your schemas
- Comprehensive rules - Validate headings, code blocks, images, tables, lists, links, and more
- Frontmatter validation - Validate YAML frontmatter with type and format checking
- Link validation - Check internal anchors, relative files, and external URLs
- Context-aware - Uses AST parsing for accurate validation without string matching
- Fast and lightweight - Single binary with no dependencies
- Cross-platform - Works on Linux, macOS, and Windows
- Editor support - JSON Schema for auto-completion and validation in VS Code, Neovim, and more
brew install jackchuka/tap/mdschemago install github.com/jackchuka/mdschema/cmd/mdschema@latestgit clone https://github.com/jackchuka/mdschema.git
cd mdschema
go build -o mdschema ./cmd/mdschema- Initialize a schema in your project:
mdschema init- Validate your markdown files:
mdschema check README.md docs/*.md- Generate a template from your schema:
mdschema generate -o new-doc.mdCreate a .mdschema.yml file to define your documentation structure:
structure:
- heading:
pattern: "# [a-zA-Z0-9_\\- ]+" # Regex pattern for project title
children:
- heading: "## Features"
optional: true
- heading: "## Installation"
code_blocks:
- { lang: bash, min: 1 } # Require at least 1 bash code block
children:
- heading: "### Windows"
optional: true
- heading: "### macOS"
optional: true
- heading: "## Usage"
code_blocks:
- { lang: go, min: 2 } # Require at least 2 Go code blocks
required_text:
- "example" # Must contain the word "example"
- heading: "# LICENSE"
optional: trueheading- Heading pattern:- String:
"# Title"(literal match) - Regex:
{pattern: "# .*"}(regex match after headings are extracted) - Expression:
{expr: "slug(filename) == slug(heading)"}(dynamic match)
- String:
description- Guidance text shown as HTML comment in generated templatesoptional- Whether the section is optional (default: false)count- Match multiple sections:{min: 1, max: 5}(0 = unlimited)allow_additional- Allow extra subsections not defined in schema (default: false)children- Nested subsections that must appear within this section
Use expr for dynamic heading matching (e.g., match filename to heading):
structure:
- heading:
expr: "slug(filename) == slug(heading)" # my-file.md matches "# My File"
children:
- heading: "## Features" # Static pattern for childrenAvailable functions:
| Function | Description | Example |
|---|---|---|
slug(s) |
URL-friendly slug | slug("My File") β "my-file" |
kebab(s) |
PascalCase to kebab | kebab("MyFile") β "my-file" |
lower(s) / upper(s) |
Case conversion | lower("README") β "readme" |
trimPrefix(s, pattern) |
Remove regex prefix | trimPrefix("01-file", "^\\d+-") β "file" |
trimSuffix(s, pattern) |
Remove regex suffix | trimSuffix("file_draft", "_draft") β "file" |
hasPrefix(s, prefix) |
Check prefix | hasPrefix("api-ref", "api") β true |
hasSuffix(s, suffix) |
Check suffix | hasSuffix("file_v2", "_v2") β true |
strContains(s, substr) |
Check contains | strContains("api-ref", "api") β true |
match(s, pattern) |
Regex match | match("test-123", "test-\\d+") β true |
replace(s, old, new) |
Replace all | replace("a-b-c", "-", "_") β "a_b_c" |
Variables:
filename(without extension)heading(heading text)level(heading level 1-6)
required_text- Text that must appear ("text"for substring or{pattern: "..."}for regex)forbidden_text- Text that must NOT appear ("text"for substring or{pattern: "..."}for regex)code_blocks- Code block requirements:{lang: "bash", min: 1, max: 3}images- Image requirements:{min: 1, require_alt: true, formats: ["png", "svg"]}tables- Table requirements:{min: 1, min_columns: 2, required_headers: ["Name"]}lists- List requirements:{min: 1, type: "ordered", min_items: 3}word_count- Word count constraints:{min: 50, max: 500}
links- Link validation (internal anchors, relative files, external URLs)heading_rules- Heading constraints (no skipped levels, unique headings, max depth)frontmatter- YAML frontmatter validation (required fields, types, formats)
mdschema check README.md docs/**/*.md
mdschema check --schema custom.yml *.md# Generate from .mdschema.yml
mdschema generate
# Generate from specific schema file
mdschema generate --schema custom.yml
# Generate and save to file
mdschema generate -o template.md# Create .mdschema.yml with defaults
mdschema init# Infer schema from existing markdown, output to stdout
mdschema derive README.md
# Save inferred schema to a file
mdschema derive README.md -o inferred-schema.ymlstructure:
- heading:
pattern: "# .*"
children:
- heading: "## Installation"
code_blocks:
- { lang: bash, min: 1 }
- heading: "## Usage"
code_blocks:
- { lang: go, min: 1 }structure:
- heading: "# API Reference"
children:
- heading: "## Authentication"
required_text: ["API key", "Bearer token"]
- heading: "## Endpoints"
children:
- heading: "### GET /users"
code_blocks:
- { lang: json, min: 1 }
- { lang: curl, min: 1 }structure:
- heading:
pattern: "# .*"
children:
- heading: "## Prerequisites"
- heading:
pattern: "## Step [0-9]+: .*"
count:
min: 1 # At least 1 step required
max: 0 # Unlimited steps allowed
code_blocks:
- { min: 1 } # Each step must have a code block
- heading: "## Next Steps"
optional: truestructure:
- heading: "# Project Name"
allow_additional: true # Allow extra subsections not defined in schema
children:
- heading: "## Overview"
- heading: "## Installation"
code_blocks:
- { lang: bash, min: 1 }
# Users can add any other sections like "## FAQ", "## Troubleshooting", etc.# Global rules
frontmatter:
# optional: false is default, meaning frontmatter is required
fields:
- { name: "title" } # required by default
- { name: "date", format: date } # required by default
- { name: "author", optional: true, format: email }
- { name: "tags", optional: true, type: array }
heading_rules:
no_skip_levels: true
max_depth: 3
links:
validate_internal: true
validate_files: true
# Document structure
structure:
- heading:
pattern: "# .*"
children:
- heading: "## Introduction"
word_count: { min: 100, max: 300 }
forbidden_text: ["TODO", "FIXME"]
- heading: "## Content"
images:
- { min: 1, require_alt: true }
code_blocks:
- { min: 1 }
- heading: "## Conclusion"
word_count: { min: 50 }
lists:
- { min: 1, type: unordered }mdschema includes comprehensive validation rules organized into three categories:
| Rule | Description | Options |
|---|---|---|
| Structure | Ensures sections appear in correct order/hierarchy | heading, optional, count, allow_additional, children |
| Required Text | Text/patterns that must appear | "text" (literal) or {pattern: "..."} (regex) |
| Forbidden Text | Text/patterns that must NOT appear | "text" (literal) or {pattern: "..."} (regex) |
| Code Blocks | Code block requirements | lang, min, max |
| Images | Image presence and format | min, max, require_alt, formats |
| Tables | Table structure validation | min, max, min_columns, required_headers |
| Lists | List presence and type | min, max, type, min_items |
| Word Count | Content length constraints | min, max |
links:
validate_internal: true # Check anchor links (#section)
validate_files: true # Check relative file links (./file.md)
validate_external: false # Check external URLs (slower)
external_timeout: 10 # Timeout in seconds
allowed_domains: # Restrict to these domains
- github.com
- golang.org
blocked_domains: # Block these domains
- example.comheading_rules:
no_skip_levels: true # Disallow h1 -> h3 without h2
unique: true # All headings must be unique
unique_per_level: false # Unique within same level only
max_depth: 4 # Maximum heading depth (h4)frontmatter:
optional: true # Set to make frontmatter optional (default: required)
fields:
- { name: "title", type: string } # required by default
- { name: "date", type: date, format: date } # required by default
- { name: "author", optional: true, format: email } # explicitly optional
- { name: "tags", optional: true, type: array }
- { name: "draft", optional: true, type: boolean }
- { name: "version", optional: true, type: number }
- { name: "repo", optional: true, format: url }Field types: string, number, boolean, array, date
Field formats: date (YYYY-MM-DD), email, url
- Documentation Standards - Enforce consistent README structure across repositories
- API Documentation - Ensure all endpoints have required sections and examples
- Tutorial Validation - Verify step-by-step guides follow the expected format
- CI/CD Integration - Validate documentation in pull requests
- Template Generation - Create starter templates for new projects
mdschema provides a JSON Schema for .mdschema.yml files, enabling auto-completion, validation, and hover documentation in editors that support YAML Language Server.
Add this to your .vscode/settings.json:
{
"yaml.schemas": {
"https://raw.githubusercontent.com/jackchuka/mdschema/main/schema.json": ".mdschema.yml"
}
}Or add a schema comment at the top of your .mdschema.yml file:
# yaml-language-server: $schema=https://raw.githubusercontent.com/jackchuka/mdschema/main/schema.json
structure:
- heading: "# My Project"Any editor with YAML Language Server support (Neovim, JetBrains IDEs, etc.) can use the schema URL:
https://raw.githubusercontent.com/jackchuka/mdschema/main/schema.json
go test ./...go build -o mdschema ./cmd/mdschemaContributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change. See CONTRIBUTING.md for more details.
MIT License - see LICENSE for details.