# Turn Your Markdown Previewer Into a Docs Linter (Free Guide)

---
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:

```js
// 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:

```js
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

````markdown
```
// 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:

1. **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.
2. **Check anchors.** `#installation` only 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:

1. **Outline scan** — no skipped heading levels, max three deep.
2. **Fence check** — every code block has a language tag.
3. **Link audit** — know your relative/absolute split; verify anchors after any heading rename.
4. **Render-truth preview** — paste into a GFM-accurate viewer, scan tables and task lists.
5. **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](https://toolkitloop.com/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.
---
