Skip to content

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

npx -y markdownlint-cli2@0.13.0

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:

npx -y markdownlint-cli2@0.13.0 docs/manual/install.md

lychee

Install lychee from https://lychee.cli.rs (one-liner on macOS: brew install lychee), then:

lychee \
  --config .lychee.toml \
  --no-progress \
  'docs/**/*.md' \
  'README.md' \
  'CHANGELOG.md'

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 on main
  • localhost / 127.0.0.1 / RFC-1918 ranges — private-network URLs from runbooks
  • oxi-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.