Doc lint¶
oxi enforces two documentation quality checks on every PR that touches docs/ or any .md file:
- lychee — broken-link detection (internal files and external URLs)
- markdownlint-cli2 — markdown style consistency (heading order, code-fence languages, list markers)
Both run in the doc-lint GitHub Actions workflow. They only fire when docs change, so a code-only PR never pays the latency.
Running locally¶
markdownlint¶
Rules and ignore patterns live in .markdownlint-cli2.jsonc at the repo root. Run from the repo root — the config is picked up automatically.
To lint a single file:
lychee¶
Install lychee from https://lychee.cli.rs (one-liner on macOS: brew install lychee), then:
The .lychee.toml config at the repo root controls timeouts, cache location, excluded URL patterns, and accepted status codes. Read the inline comments there before adding new exclusions.
What markdownlint checks¶
The .markdownlint-cli2.jsonc runs markdownlint with default: true (all rules on) then disables specific rules that conflict with oxi's prose style. Key rules that remain on and are load-bearing:
| Rule | What it catches |
|---|---|
| MD001 heading-increment | Skipped heading levels (H1 → H3 without H2) |
| MD007 ul-indent | Inconsistent list indentation |
| MD009 no-trailing-spaces | Invisible trailing whitespace |
| MD010 no-hard-tabs | Tabs in markdown source |
| MD014 commands-show-output | Shell blocks with $ prefix but no output |
| MD022 blanks-around-headings | Missing blank lines around headings |
| MD031 blanks-around-fences | Missing blank lines around code fences |
| MD040 fenced-code-language | Missing language tag on fenced blocks |
Rules that are explicitly off (with rationale in .markdownlint-cli2.jsonc): MD013 (line length), MD033 (inline HTML), MD034 (bare URLs), MD041 (first-line H1), MD051 (link fragments).
What lychee checks¶
lychee checks every URL it finds in the scanned files. Internal markdown links (e.g. [install](../install.md)) are resolved relative to the file. External URLs are fetched with a short timeout.
False-positive suppression — the .lychee.toml excludes:
github.com/escotilha/oxi/edit/— edit-this-page links generated by the docs theme; they don't resolve until the file is onmainlocalhost/127.0.0.1/ RFC-1918 ranges — private-network URLs from runbooksoxi-tick-screenshot.png— the placeholder screenshot referenced in the README before it's been captured
Transient errors — HTTP 429, 503, 504 are accepted (not failures). These appear in the log but don't fail CI.
Caching — lychee caches external responses for 7 days in .lycheecache/. Add .lycheecache/ to .gitignore if running locally; the CI cache is keyed by commit SHA with a lychee-cache- restore-key fallback.
Adding a new URL exclusion¶
If a URL consistently returns a bot-blocking 4xx despite being valid, add a regex pattern to the exclude list in .lychee.toml with a comment explaining why:
exclude = [
# ...existing entries...
# example.com returns 403 to all non-browser UAs.
"^https://example\\.com/",
]
Do not add exclusions for URLs that are genuinely broken — fix the link instead.
Adding a new markdownlint rule exception¶
If a rule fires on a false positive that can't be fixed in the source, add a disable/enable pair around the specific lines:
<!-- markdownlint-disable MD014 -->
$ command-with-no-output-shown
<!-- markdownlint-enable MD014 -->
Prefer inline suppression over adding a file-level ignores entry or disabling a rule globally. Global disables belong in .markdownlint-cli2.jsonc only when the rule conflicts with an intentional oxi-wide style choice.