A fast, single-binary linter, autofixer, and formatter for a Hugo site's
markdown content and data files, driven by built-in and user-defined
custom rules. Run it as a pre-deploy gate — like golangci-lint, but for content.
Hugo sites accumulate ad-hoc content checks (a script for a rendering gotcha,
another for frontmatter/SEO). doclint replaces them with one tool: simple
rules are declared in YAML (no recompile), complex rules are built in, and
everything shares one config, one autofix engine, and one output format.
go install github.com/openserbia/doclint/cmd/doclint@latestOr download a prebuilt binary from the Releases page.
Point doclint at the directories you want linted (typically content and
data):
# Report findings (no changes); non-zero exit on errors
doclint lint content data
# Apply safe autofixes in place
doclint lint --fix content
# Also apply unsafe fixes (may change meaning)
doclint lint --fix --unsafe-fixes content
# List files whose fixes would change them, without writing
doclint lint --diff content
# Normalize markdown spacing (idempotent)
doclint fmt content
doclint fmt --check content # CI gate: non-zero if any file would change
# Output formats: human (grouped, default) | compact (flat, CI/grep) | json
doclint lint --format compact content
doclint lint --format json content
# Scaffold a starter .doclint.yaml in the current directory (--force to overwrite)
doclint init
# Discover rules (explain tab-completes rule names)
doclint list
doclint explain details-blank-line
# Shell completion (bash|zsh|fish|powershell) — interactive install, or pipe the script
doclint completion zsh # in a terminal: offers to install, or shows manual steps
source <(doclint completion zsh) # or load the raw script directlydoclint walks every file under the paths you pass, so scope it to your content
and data directories (or use ignore globs in config).
| Rule | Severity | Fix | Description |
|---|---|---|---|
Blank line after (details-blank-line) |
error | safe | require a blank line after so inner markdown renders |
Consistent table columns (table-column-count) |
error | — | require every table row to match the header's column count |
Space after heading hashes (no-missing-space-atx) |
error | safe | require a space after the # of an ATX heading so it renders |
Heading at the left margin (heading-start-left) |
warning | safe | ATX headings should start at the left margin (no leading indentation) |
Blank lines around code fences (blanks-around-fences) |
warning | safe | fenced code blocks should be surrounded by blank lines |
Blank lines around lists (blanks-around-lists) |
warning | safe | lists should be surrounded by blank lines |
Blank lines around headings (blanks-around-headings) |
warning | safe | headings should be surrounded by blank lines |
Code fence language (fenced-code-language) |
warning | — | fenced code blocks should specify a language for syntax highlighting |
Image alt text (no-alt-text) |
warning | — | images should have non-empty alt text for accessibility and SEO |
Trailing whitespace (no-trailing-spaces) |
warning | safe | remove stray trailing spaces while preserving the two-space hard line break |
Valid in-page anchor links (no-broken-anchor) |
warning | — | in-page anchor links must point at a heading in the same page |
List item body indentation (list-marker-indent) |
warning | unsafe | list item bodies must indent to the marker's content column |
Each rule has a full rationale and a before/after example in docs/rules/, generated by the doclint docs command.
Define rules in .doclint.yaml with no recompile. Supported types: required,
length, not_equal, match, deny — scoped by a path glob, optionally
skipping drafts.
doclint discovers .doclint.yaml by walking up from the working directory:
default: standard # all | standard | none
enable: [] # force-enable specific rules by name
disable: [] # force-disable specific rules by name
paths: # default lint/fmt targets when none are passed on the CLI
- content
- data
settings:
details-blank-line:
severity: error
ignore:
- "node_modules/**"
custom:
- id: frontmatter-description-required
type: required
glob: "content/**/*.md"
field: description
skip_drafts: true
severity: error
- id: seo-description-length
type: length
glob: "content/**/*.md"
field: description
min: 120
max: 160
severity: warningenable force-enables specific rules by name regardless of default; disable force-disables them.
<!-- doclint-disable-next-line details-blank-line -->Unused suppressions are reported as warnings.
Fixes are tagged safe or unsafe (inspired by Ruff). lint --fix applies
safe fixes only; --unsafe-fixes opts into the rest. Plain lint never mutates.
--format human (default — findings grouped by file, colored, each row
click-to-jump in editors), --format compact (one flat path:line:col line per
finding, for CI and grep), or --format json. The default human format
auto-falls back to compact when stdout is not a terminal, so piped/CI output
stays parseable and color-free. Each auto-fixable finding is marked (* safe,
~ unsafe), the summary reports how many are fixable with --fix, and the human
output ends with a learn how to fix: list linking each rule that fired to its
reference page (also printed by explain, and a doc_url field in JSON). A
malformed .doclint.yaml fails preflight with a clear, actionable message. Exit
0 when clean, 1 on error-severity findings
(warnings are advisory; use --max-warnings N to tighten), 2 on a
configuration or internal error.
MIT — see LICENSE.