CI for your docs
Docs rot in ways no reviewer catches: a page gets renamed and links to it 404 for months, an anchor stops matching after a heading edit, a code example keeps calling an API that changed two releases ago. leadtype lint makes each of those a failing PR check instead of a slow leak — a bare leadtype lint in CI verifies your whole docs system, config included, against the same resolution rules generate builds with.
Everything below was tuned on leadtype's own docs, where it found real, shipped breakage: anchors broken on the live site, a JSX component in a ts fence, and over a hundred type-level issues in "carefully written" examples.
What it catches
The link graph — every internal link verified against your real routes (mounts, collections, and generated pages included):
- Links to routes that don't exist, including relative
./siblinglinks (invalid-link) — and when redirect tracking is on, a link to a renamed page tells you where it moved. #anchorsthat match no heading on the target page, validated with the exact slugger the site TOC uses (invalid-anchor).- Dead links the config itself owns — curated
navigationentries,llms.sectionslinks, feed prefixes, staleredirects.removedpaths (config-link). Lint catches at PR time whatgeneratewould crash on at build time. - Cross-framework links and unresolved
{framework}placeholders (cross-framework-link,unresolved-placeholder).
Code snippets — examples that stay true:
- Every fenced block with a known language must parse — TS/JS, JSON/JSONC, YAML (
snippet:parse). Common docs idioms (bare signatures, config excerpts,...ellipses) parse without any annotation. - Opt in to
snippet:typesand module-shapedts/tsxexamples typecheck against your installed packages — when your API changes, every doc example still calling the old one fails CI. See Lint reference for the twoslash directives (// @filename:,// ---cut---,// @noErrors).
The boring-but-vital layer — frontmatter schema violations, unknown fields, unparseable frontmatter or meta.json, components that would leak raw JSX into agent markdown, invalid JSON-LD, and the structural GEO checks (heading hierarchy, code-fence languages, image alt text).
External URLs — checked on a schedule, never in your merge gate (network flakiness doesn't belong in PR CI). Run the external-link rule weekly:
Live URLs are cached between runs (default 7 days), failures never are, rate-limited and bot-gated hosts are skipped rather than failed, and lint.externalLinks.ignore mutes known-flaky prefixes.
Lint discovers your docs config the same way generate does, so a bare leadtype lint lints the configured source tree with mounts, custom schemas, and lint.rules severity overrides applied — no flags needed.
See Lint reference for the full rule list and how to extend the schema.
GitHub Actions
Use --format github so violations render as inline annotations on the PR:
--error-unknown upgrades unknown-field warnings to errors — strict mode. --max-warnings 0 makes any remaining warning fail the job.
Other CI providers
Use --format json for any CI that doesn't speak the GitHub annotations format:
The JSON output includes per-file violations and summary counts. Pipe it into your provider's reporter, post a PR comment, or upload as an artifact.
GitLab CI
CircleCI
Local pre-push hook
Catch issues before they reach CI by running lint in a husky pre-push hook:
Keep it under a second by limiting the scan to changed files when you have many pages. The CLI accepts repeated --ignore globs to skip stale or generated paths.
Run before generate
When leadtype lint and leadtype generate both run in the same job, lint first:
Generate fails noisily on unknown groups or broken includes. Lint fails specifically on content schema problems with file/line context — much easier to debug.
What to fix first
When CI fails on a lot of violations, fix them in this order:
parse-error— frontmatter is broken; nothing else can validate.schema— missing or wrong-typed required fields.unresolved-placeholder— content bug, not a config bug.invalid-linkandcross-framework-link— usually a stale link after a docs move.unknown-field— last; either delete the field or extend the schema.