Documentation & DX / Tutorial
Docs as Code, With Teeth: Linting, Link Checks, and Preview Builds in CI
Wire documentation into CI so quality is enforced, not hoped for: prose linting that respects writers, link checking without false alarms, and previews on every pull request.

By the end of this tutorial you will know how to put three enforcement mechanisms into a docs pipeline — prose linting, link checking, and preview builds — and, more importantly, how to tune each one so your team keeps trusting it. Docs as code fails socially before it fails technically: the YAML is easy, and a pipeline that cries wolf or blocks writers on trivia gets bypassed within a month. Every choice below is aimed at a pipeline strict enough to matter and calm enough to survive.
The pipeline at a glance
A pull request that touches documentation should run, in this order:
- Build the site from the PR branch. If it does not build, nothing else matters.
- Lint the prose on changed files, with errors separated from suggestions.
- Check links — internal links on every run; external links on a schedule, not on every PR.
- Publish a preview and post its URL as a PR comment.
- Run any structural tests: frontmatter validation, image checks, redirect checks.
That ordering is deliberate. Cheap, deterministic checks go first so failure feedback is fast, and the preview — the thing humans actually review — exists regardless of lint opinions.
Prose linting that writers don’t learn to hate
The tool most docs teams reach for is Vale, and the tool is not the hard part; the ruleset is. Vale lints prose against a style vocabulary — terminology, capitalisation, banned phrases, heading case — using YAML rules plus an optional style package such as the Microsoft or Google style guides. A minimal, honest starting configuration looks like this:
# .vale.ini
StylesPath = styles
MinAlertLevel = suggestion
[*.md]
BasedOnStyles = Vale, Company
# Start with terminology only. Earn the right to add more.
Company.Terminology = YESAnd a terminology rule is a swap table — your product’s correct names against their common misspellings and stale variants:
# styles/Company/Terminology.yml
extends: substitution
message: "Use '%s' instead of '%s'."
level: error
swap:
command line interface: CLI
config file: configuration fileThree tuning decisions determine whether linting lives or dies:
- Errors must be genuinely wrong. A red build for a terminology error is defensible; a red build for a passive-voice suggestion is how you teach a whole team to write
vale offcomments. Set contested style rules tosuggestionorwarningforever, and let humans argue about them in review instead. - Lint changed lines, not the whole repo. Turning on a ruleset against an existing corpus produces a wall of legacy findings, and the first PR drowns. Run the linter in a mode scoped to the PR diff for enforcement, and run full-corpus reports as a non-blocking job so the backlog is visible and burnable.
- Every rule needs an owner and a reason. When a writer hits a rule they believe is wrong, there must be a fast, legitimate path to change it — a PR to the styles folder, reviewed like code. A linter nobody can appeal becomes a linter everybody routes around.
Link checking without the false-alarm fatigue
Link rot is the most visible docs quality failure and the easiest to automate badly. Split the problem in two, because the halves have opposite failure profiles.
Internal links are deterministic. Your build knows every route it generates, so a broken internal link is a true positive essentially every time. Check them on every PR — most static site generators either validate links at build time or pair with a checker that crawls the built output. Treat failures as build failures. There is no excuse for a merged PR to introduce a 404 inside your own site.
External links are weather, not physics. Third-party sites rate-limit, geo-block, bot-wall, and intermittently 404 to automated checkers while serving humans fine. If you gate PRs on external checks, you will train your team to re-run CI until it passes, which is worse than not checking. Instead:
- Run external checks on a nightly schedule against the published site, and open an issue — or a single rolling ticket — listing the dead links.
- Configure the checker with a sensible retry policy and an allowlist for domains that block bots; a domain on the allowlist gets human verification on a rota, not silent trust.
- Report trends, not incidents. One dead link is a fix; a rising dead-link count in one section is a maintenance signal about that section’s sources.
For Markdown sources, a checker such as lychee handles both halves and lets you separate them by flag:
# In CI, per PR: internal only, against built output
lychee --offline --root-dir ./dist ./dist
# Nightly: full external sweep, results to a report file
lychee --cache --max-retries 2 --exclude-path ./dist/archive ./dist > link-report.txtPreview builds: the feature that sells the whole pipeline
If linting is the pipeline’s conscience, previews are its charisma. A preview build renders the PR branch to a unique URL and comments it on the pull request. This changes review from reading diffs — where nobody can judge a table, a callout, or a code block’s line breaks — to reading the page as a reader will. Reviewers catch structural problems in previews that diffs hide completely: the tutorial step that renders in the wrong order, the screenshot that overflows on mobile width, the heading hierarchy that reads fine as Markdown and terribly as a page.
Two implementation details matter. First, previews must build the whole site, not the changed page in isolation, because docs bugs are disproportionately cross-page: navigation, cross-links, and shared components. Second, preview URLs must be posted automatically and predictably; a preview that reviewers have to hunt for in CI logs will not be opened. Most hosting platforms used for docs offer PR previews natively; if yours does not, a CI job that deploys the build artifact to a per-PR path achieves the same thing.
Structural checks worth adding once the basics hold
| Check | What it catches | Gate the PR? |
|---|---|---|
| Frontmatter schema validation | Missing descriptions, bad tags, wrong content type | Yes |
| Image existence and alt text presence | Broken figures, accessibility gaps | Yes |
| Redirect coverage for renamed files | Moved pages without redirects in the same PR | Yes |
| Code sample extraction and compile/test | Samples that no longer run against current APIs | Where feasible, yes |
| Word count on descriptions and titles | SEO and rendering truncation surprises | Warning only |
Add these one at a time, each after the previous one has run quietly for a few weeks. A pipeline that arrives all at once, with thirty new ways to fail, will be resented as a unit and removed as a unit.
Rollout: the first month, sequenced
The teams that succeed at this do not install the pipeline in a heroic weekend. They sequence it so each stage earns trust before the next one asks for more:
Week one: build and preview only. Get the site building in CI and previews commenting on pull requests. Nothing can fail that could not fail before; the pipeline is pure gift. Let the team fall in love with previews first, because that affection is the political capital everything else spends.
Week two: internal links and frontmatter. Turn on the deterministic checks as blocking. These fail rarely and are never wrong, so the team’s first experience of a red docs build is a fair one — a genuinely broken link, fixed in a minute, with mild gratitude that CI caught it.
Week three: linting as warnings. Run the prose linter in report-only mode on PRs. Writers see the suggestions, fix the ones they agree with, and argue about the rest in the open. Promote to blocking only the rules that nobody contested during the warning period — in practice, terminology and product names — and leave the rest as warnings permanently.
Week four: the nightly sweeps. External link checking and full-corpus lint reports start running on a schedule, filing their findings where the team triages async work. Nobody’s Friday PR ever again depends on whether a third-party site feels like answering a bot.
Notice what this sequence protects: the belief that the pipeline is on the writers’ side. Every stage is introduced as help, proves itself, and only then gains the authority to block. Reverse the order — begin with blocking style rules — and the same tooling becomes an occupying force, and your engineers will help the writers circumvent it, because teams protect their own against processes that waste their time.
Verdict
Docs as code earns its keep when the pipeline behaves like a good editor: fast, predictable, strict about facts and silent about taste. Lint terminology and correctness as errors; lint style as advice. Gate PRs on internal links and builds, sweep external links on a schedule, and make the preview impossible to miss. Do that, and quality stops depending on who happened to review carefully — it becomes a property of the system your docs are built by.


