Markdown Coding Convention for Calm, Consistent Docs
Markdown should not become a tiny negotiation in every pull request. A shared formatter keeps the boring parts boring, which is exactly where they belong.
Markdown looks simple until five developers edit the same documents for a few months. Ordered lists, line wrapping, table spacing, and code fences all become tiny review comments.
This is the convention I like for project documentation: define the style once, let tooling apply it, and keep review focused on whether the document is true and useful.
The Full Rule
Here is the complete .rumdl.toml configuration. The notes after it explain how to use it and what it is meant to keep out of review.
# rumdl configuration — https://rumdl.dev (v0.2.16)
#
# Goal: keep Markdown formatting deterministic and consistent across developers.
# The `docs-format.yml` pipeline runs `rumdl fmt .` and opens a PR with the
# normalized result, so every setting below is pinned to a single explicit value
# instead of "consistent" to avoid drift between contributors.
#
# Project rules:
# - Frontmatter is optional; when present, its `title` field is the document H1.
# - No line-length limit (lines may be as long as needed).
# - Ordered lists always use `1.` (no incremental numbering).
# - Link text is unrestricted.
[global]
# Run every built-in rule by default, then turn off the few that conflict with
# the project rules above and additively opt in to extra consistency rules.
disable = [
"MD013", # Line length — no limit; lines may be as long as needed.
"MD059", # Link text descriptiveness — any link text is allowed.
]
# Opt-in rules (off by default) that improve cross-developer consistency.
extend-enable = [
"MD060", # Table column formatting/alignment.
]
# Files and directories that are not authored Markdown.
exclude = [
".git",
"node_modules",
"**/bin/**",
"**/obj/**",
"docs/book", # mdBook build output
"firstmile.patterns/**",
]
respect-gitignore = true
# ---------------------------------------------------------------------------
# Headings
# ---------------------------------------------------------------------------
[MD003]
style = "atx" # `# Heading` form everywhere (no setext/closed-atx).
[MD022]
lines-above = 1 # One blank line above every heading.
lines-below = 1 # One blank line below every heading.
[MD024]
siblings-only = true # Duplicate headings only flagged within the same parent.
allow-different-nesting = false
[MD025]
level = 1
front-matter-title = "title" # `title:` in frontmatter counts as the single H1.
[MD026]
punctuation = ".,;:!" # Trailing punctuation stripped from headings.
[MD041]
level = 1
front-matter-title = "title" # First content must be H1 (or frontmatter `title`).
# ---------------------------------------------------------------------------
# Lists
# ---------------------------------------------------------------------------
[MD004]
style = "dash" # Unordered lists use `-`.
after-marker = 1 # Exactly one space after the marker.
[MD007]
indent = 2 # Two spaces per nested unordered-list level.
[MD029]
style = "one" # Ordered lists always use `1.` (no incremental numbering).
[MD030]
ul-single = 1
ol-single = 1
ul-multi = 1
ol-multi = 1
# ---------------------------------------------------------------------------
# Whitespace
# ---------------------------------------------------------------------------
[MD009]
br-spaces = 2 # Two trailing spaces = intentional hard line break.
strict = false
[MD010]
spaces-per-tab = 4
code-blocks = false # Leave tabs inside code blocks untouched.
[MD012]
maximum = 1 # At most one consecutive blank line.
# ---------------------------------------------------------------------------
# Emphasis / inline style
# ---------------------------------------------------------------------------
[MD035]
style = "---" # Horizontal rules are always `---`.
[MD049]
style = "asterisk" # Italic emphasis uses `*text*`.
[MD050]
style = "asterisk" # Bold/strong uses `**text**`.
[MD044]
names = ["Markdown", "GitHub", "Azure DevOps", "Jira"]
code-blocks = false # Don't enforce capitalization inside code.
# ---------------------------------------------------------------------------
# Code blocks
# ---------------------------------------------------------------------------
[MD046]
style = "fenced" # Always use fenced code blocks (no indented blocks).
[MD048]
style = "backtick" # Fences use ``` (not ~~~).
# ---------------------------------------------------------------------------
# Tables
# ---------------------------------------------------------------------------
[MD055]
style = "leading-and-trailing" # `| cell |` — pipes on both ends.
[MD058]
minimum-before = 1
minimum-after = 1
[MD060]
enabled = true
style = "aligned" # Pad table columns so they line up.How to Use It
For this setup I use rumdl, a Markdown linter and formatter written in Rust. It is fast, configurable, and easy to run locally or in CI.
rumdl check .rumdl fmt .rumdl check . reports issues. rumdl fmt . fixes what it can. Install it through Homebrew, Cargo, npm, pip, uv, mise, or a pinned binary; in CI, pin the version so formatting does not drift between machines.
What the Convention Decides
The configuration above keeps a few choices out of pull requests:
- Frontmatter is optional, and
titlecan count as the document H1. - Line length is not limited; the rendered site controls reading width.
- Ordered lists always use
1.so reordering stays painless. - Link text is unrestricted because usefulness depends on context.
- Headings, lists, emphasis, code fences, and tables use one predictable style.
None of this makes the Markdown perfect. It just makes the boring parts consistent.
What Review Should Do
After formatting is automatic, review can move back to judgment:
- Is the explanation accurate?
- Is the title useful?
- Does the page help the reader do the next thing?
- Are links pointing to the right place?
- Is this exception intentional?
That is the real goal: fewer tiny arguments, cleaner diffs, and docs that feel like the team meant to write them together.