/writing
Custom markdownlint Rules for Docs Conventions
· 3 min read
A docs site I worked on had conventions that mattered to us and to nobody else. Every page needed a slug and a title in its frontmatter. Code tabs needed a blank line above and below. Some old URL shapes were no longer allowed. Custom admonition components had to be written a particular way. TODO markers were not supposed to reach published pages.
None of this is anything off-the-shelf markdownlint cares about. All of it was written down in a contributor doc, and all of it was enforced by whoever happened to be reviewing.
Rules that encode project decisions
I added a small set of project-specific rules to the markdownlint config. Each one captured a decision the team had already made, in a form CI could check.
The frontmatter rule failed any file missing a slug or title. The link rule blocked URL patterns we had moved away from. The admonition rule required our custom syntax to open and close the way the renderer expected. The codetabs rule required blank lines around the wrapper so the parser resolved it. The TODO rule caught notes writers left themselves and forgot to remove.
Each rule was tiny. Together they turned the things we cared about into something a machine checks, which meant people stopped having to remember them.
module.exports = {
names: ['docs-no-todo'],
description: 'TODO markers must not reach published content',
tags: ['docs'],
function: (params, onError) => {
params.lines.forEach((line, index) => {
const marker = /\b(TODO|FIXME)\b/.exec(line);
if (!marker) return;
onError({
lineNumber: index + 1,
detail: `Found ${marker[1]}`,
context: line.trim().slice(0, 60),
});
});
},
};
The scale that makes it worth it
Seven rules, over five thousand pages, and more than a hundred people who have committed to them. That ratio is the whole argument. A convention explained in a contributor doc has to be read by every one of those people and remembered by all of them at once. A rule has to be written once.
The people who benefit most are the ones who never read the doc, which is most of them, and who now find out in CI in seconds rather than in a review comment a day later.
The allowlist conversation
The hardest rule was the one that blocked certain image paths but allowed aliased imports. Real projects always have exceptions, and the trick is not to pretend otherwise. The rule reads an explicit allowlist, and adding an entry is a small pull request with the path and a one-line reason. A reviewer sees the exception and accepts it on purpose, instead of the linter quietly being lenient.
What I learned
A linter is not the place for clever rules. It is the place for boring ones: the conventions that came out of a conversation you have already had three times. Writing them down as rules is how you avoid having it a fourth time.
The saving compounds. Every new contributor gets the conventions for free, and reviewers spend their attention on the things only a person can catch.
More writing
Deploying Swagger UI for Bacen's Pix API
A pull request to the Brazilian central bank that was closed without ever being merged, and shipped anyway. Rendering an OpenAPI spec as docs, from CI.
· 2 min read
Audit Scripts for a Design System
How a style guide became a suite of audit scripts behind one umbrella command, grouped by what they protect, with parallel execution and an explicit drift-resolution workflow.
· 3 min read
Rebuilding SentiNEO as a Desktop
Why I rebuilt an asteroid tracker from a scrolling list into a desktop of draggable windows, and why the phone version had to be a different interface.
· 3 min read