
Word counts lie, heading depth matters, and a missing language tag on a code fence makes your docs look unfinished on GitHub. Your Markdown previewer already computes most of the signal you need — here's how to turn its stats panel into a docs linter.
The stats you're already ignoring
Every decent Markdown previewer shows reading stats: words, characters, headings, links, code blocks. Most people treat this as trivia. It's actually a structural audit of your document, and it catches a class of problems that no spellchecker will:
// the shape of the signal most previewers already compute
const stats = { words, characters, headings, links, codeBlocks };
The trick is reading these numbers like a linter reads an AST — as ratios and thresholds, not absolutes.
Heading depth: the fastest smell test
A document's heading structure is its table of contents, and readers (and search engines) punish incoherent ones. Two checks take seconds:
- Skipped levels. Jumping from
##to####breaks the document outline. Screen readers and GitHub's own TOC rendering both assume a coherent hierarchy. If your previewer's stats show 12 headings, eyeball the outline — most viewers render one — and flatten anything deeper than three levels unless you have a real reason. - Heading-to-content ratio. A README with 14 headings and 300 words isn't documentation, it's an index. Conversely, 2,000 words under a single heading is a wall of text. Neither is a hard rule; both are prompts to look.
A one-liner against the raw source catches skipped levels before you even preview:
const levels = [...source.matchAll(/^#{1,6}(?=\s)/gm)].map(m => m[0].length);
const skips = levels.slice(1).filter((l, i) => l - levels[i] > 1);
Code fences: the language tag everyone forgets
```
// what most people write — no highlighting on GitHub
const x = 1;
```
```js
// what GitHub actually renders well
const x = 1;
```
An untagged fence renders as a gray box; a tagged one gets syntax highlighting, and on GitHub it also feeds the language statistics that make code searchable. When your previewer reports 9 code blocks, verify each has a language tag. This is the single highest-ROI docs fix I know: thirty seconds of work, visibly more professional output.
While you're there, check that inline code uses single backticks and blocks use triple — mixed conventions confuse both renderers and readers.
Links: count them, then audit them
Link density is a maintenance signal. A doc with 40 links is a doc with 40 future 404s. Two things worth doing:
- Separate relative from absolute. Relative links (
./docs/setup.md) are portable within the repo but break the moment the file moves. Absolute links survive moves but rot externally. Neither is wrong — but you should know which kind dominates your doc, because they fail differently. - Check anchors.
#installationonly works if a matching heading exists, and GitHub's anchor slugification (lowercase, hyphens, no punctuation) has bitten everyone at least once. A heading rename silently kills every anchor pointing at it.
A client-side viewer is well placed to help here because it sees both the source and the rendered output. The one I use shows link counts in the stats panel and flags remote asset references with a Remote Asset Guard — rendering a remote image means the reader's browser contacts a third-party server, so the tool warns before fetching rather than silently loading.
Tables and task lists: render-truth checks
GFM tables and task lists are where "looked fine in my editor" goes to die. Editors often render a forgiving dialect; GitHub renders GFM strictly:
- A table row with fewer cells than the header gets empty cells on GitHub — your editor may silently pad the display.
- [x]needs exact spacing; one missing space renders a literal bracket instead of a checkbox.- Escaped pipes (
\|) inside table cells are a classic: they work in some renderers and produce garbage in others.
The fix is unglamorous: preview in a GFM-accurate renderer before you push, and scan every table and checklist in the preview. Ten seconds, and it eliminates the most common class of README embarrassment.
Frontmatter and portability: metadata is not content
Frontmatter (--- blocks) is metadata — a viewer should show a safe, read-only summary, never render it as a heading. And if you draft in Obsidian or Notion, your source is full of app-specific syntax ([[wiki links]], custom callouts) that dies outside its home app. A portability check — I call mine a Markdown Passport — flags non-portable constructs so you know before publishing that they won't survive the move to GitHub.
Putting it together: the 60-second docs lint
Before every push, run this against your Markdown:
- Outline scan — no skipped heading levels, max three deep.
- Fence check — every code block has a language tag.
- Link audit — know your relative/absolute split; verify anchors after any heading rename.
- Render-truth preview — paste into a GFM-accurate viewer, scan tables and task lists.
- Portability pass — flag app-specific syntax and confirm frontmatter isn't rendered as content.
No CI pipeline required. The previewer you already have open computes most of this — you just have to read the stats like a linter.
Disclosure: I built the viewer referenced here — Markdown Viewer, one of 30 free browser-based tools in my ToolkitLoop collection. Paste Markdown or drop in a .md file; GFM preview, reading stats, remote-asset guard, and portability check included, and your text never leaves the browser.
What's your docs-linting checklist? The anchor-slugification one gets me more often than I'd like to admit — curious which of these five bites other people most.
About the author
- Founder of ToolkitLoop (toolkitloop.com) — 30 free online tools that run 100% in your browser. No sign-ups, no uploads, private by design.
Comments
Join the discussionNo comments yet. Be the first to comment.
