How to Add a Table of Contents in Markdown That Actually Works

📑 Markdown workflows⏱️ 7 min readFree tool included

Every long README deserves a table of contents, and every table of contents is a pile of anchor links waiting to break. The links look innocent — [Getting Started](#getting-started) — but they only work if they point at the anchor your renderer generated, and renderers generate anchors with their own rules: lowercase, punctuation stripped, duplicates suffixed. Here's the full rulebook with examples, the traps that break TOCs on real documents, and how to generate the whole block correctly instead of typing it.

Advertisement

What does a markdown TOC look like?

A nested list of links, placed near the top of the file, each pointing at a heading's anchor:

## Contents
- [Project Docs](#project-docs)
  - [Getting Started](#getting-started)
    - [Install](#install)

Two spaces of indentation per level is the convention that renders as nested lists everywhere (four spaces can trigger code blocks in some parsers). The list is ordinary markdown — no special syntax — which is why a generator can produce it and why anything that produces wrong anchors is worse than nothing.

How are heading anchors generated?

GitHub's algorithm — the one most tools copy — is a small pipeline:

  1. Lowercase the heading text.
  2. Strip punctuation and emoji (keeping letters, digits, dashes, underscores, and accented characters).
  3. Convert spaces to dashes.
  4. If the result already exists on the page, append -1, then -2, and so on.
HeadingAnchorWhat happened
Hello World#hello-worldlowercase + dash
What's new?#whats-newapostrophe and ? stripped
foo / bar#foo--barslash stripped, two spaces → two dashes
C++#csymbols gone
Über café#%C3%BCber-caf%C3%A9accents kept, URL-encoded in the link
Getting Started (second one)#getting-started-1duplicate suffix

That percent-encoded row surprises people: the anchor itself is über-café, but links in markdown get URL-encoded, so the TOC entry carries #%C3%BCber-caf%C3%A9. Browsers decode it on click. Hand-write the raw characters and some renderers happily link them; others don't. Encoded is the portable form — it's what the doctoc CLI emits.

Why do duplicate headings break TOCs?

Because anchors must be unique per page. Two ## Getting Started headings produce #getting-started and #getting-started-1. A TOC that links both entries to #getting-started sends both clicks to the first section and silently strands the second one. This is the single most common TOC bug on real docs, because documentation loves repeated section names ("Configuration" in every chapter). The fix is mechanical — track duplicates the way the renderer does, in document order — and it's exactly what a generator does for you.

Which headings should the TOC include?

Convention: skip the H1 (there's one, it's the title, the reader is already there) and include H2–H3, sometimes H4. Depth is a readability decision: every level you add doubles the list and pushes content below the fold. For a 300-line README, H2–H3 usually lands between five and fifteen entries — the sweet spot. For API docs, H2–H4 often earns its length.

One subtlety worth knowing: a heading inside a fenced code block (```) is not a heading, and neither is a hand-written HTML <h2>. Scanners that miss the first case list code comments as sections; scanners that include the second produce anchors that don't exist in markdown-land.

How do doctoc markers work?

Once a document grows, you'll edit headings and the TOC goes stale. The doctoc convention solves regeneration: wrap the TOC in HTML comments —

<!-- START doctoc generated TOC … -->
<!-- END doctoc generated TOC … -->

— and any doctoc-compatible tool (the CLI, or a browser generator) replaces exactly what's between them. The comments don't render, so readers never see them, and your TOC becomes a build artifact instead of a maintenance chore. Generate once with markers on, and refreshing after a restructure is one paste.

Generate a correct TOC, free

Runs GitHub's own slug algorithm — duplicates, emoji, accents handled — with doctoc-compatible markers and depth control.

Open the Markdown TOC Generator

Manual or generated?

Type it by hand when the document has five headings, none repeated, all plain ASCII — you'll be right, and it takes a minute. Generate when any of these hold: duplicate heading texts, accents or emoji in headings, more than a dozen headings, or a document that keeps changing. The Markdown TOC Generator runs the same slug library GitHub's ecosystem uses (github-slugger), in your browser, and emits doctoc-compatible markers — its output was verified byte-identical to the doctoc CLI's across duplicate, unicode, and emoji test documents.

While you're structuring a document: a markdown table generator spares you the pipe-counting for data tables, and once the file's finished, markdown to PDF turns it into something shareable outside the repo.

Advertisement