To add a table of contents to a markdown file, paste it below and press Generate. You get a linked list — - [Getting Started](#getting-started) style — using GitHub's exact anchor rules: lowercase, punctuation and emoji stripped, spaces to dashes, and duplicate headings numbered -1, -2. Accented headings come out percent-encoded (#%C3%BCber-caf%C3%A9), exactly like the doctoc CLI emits. Copy the block into your README, or use Insert into document to place it between doctoc-compatible markers so it can be regenerated later. Everything runs locally.

Generator

Markdown input
Table of contents

The example document includes duplicate, accented, and emoji headings — the cases where naive TOCs break.
Advertisement

GitHub Anchor Rules, With Examples

Heading textGitHub anchorRule at work
Hello World#hello-worldlowercase, spaces → dashes
Getting Started (2nd occurrence)#getting-started-1duplicates get -1, -2…
C++#c+ is punctuation, stripped
50% off#50-off% stripped, digits kept
What's new?#whats-newapostrophes and ? stripped
1. Introduction#1-introductionperiod stripped, spaces → dashes
foo / bar#foo--barslash stripped, both spaces dash
A_B#a_bunderscores survive
Über café#%C3%BCber-caf%C3%A9accents kept, URL-encoded in links
😂 tada#-tadaemoji stripped, its space still dashes
The `config` object#the-config-objectinline code backticks stripped

Every row above is generated by the same github-slugger library this tool runs — the exact algorithm GitHub's renderer uses — not a hand-written approximation.

How the Markdown TOC Generator Works

Two open-source pieces do the real work, both running in your browser. The heading scan follows the conventions of doctoc, the long-standing CLI for this job: it reads ATX headings (## Like this), setext headings (underlined with === or ---), skips anything inside fenced code blocks, and nests entries two spaces per level below the shallowest heading in range. Anchor generation uses github-slugger — the library built to replicate GitHub's slug algorithm — so every link points at the anchor GitHub will actually assign, including the duplicate-suffix behavior and URL-encoding.

A worked example

Take a short README with the classic troublemakers:

# Project Docs · ## Getting Started · ### Install · a second ## Getting Started · ### Über café & co · ## 😂 tada

The generated TOC (title "Contents"):

- [Project Docs](#project-docs)
  - [Getting Started](#getting-started)
    - [Install](#install)
  - [Getting Started](#getting-started-1)
    - [Über café & co](#%C3%BCber-caf%C3%A9--co)
  - [😂 tada](#-tada)

Four things to notice: the second "Getting Started" links to #getting-started-1, not #getting-started — link both naively and the second click lands on the first section. The accented heading's anchor is percent-encoded, matching what doctoc produces and what browsers expect. The emoji heading keeps its dash prefix. And nesting is two spaces per level, which renders cleanly as nested lists everywhere.

Markers and regeneration

With markers on, the TOC ships wrapped in <!-- START doctoc generated TOC … --> and <!-- END … --> comments. Generate again with Insert into document and the tool replaces what's between the markers instead of stacking a second copy — the same contract the doctoc CLI uses, so files stay compatible whether you regenerate here or in a terminal.

Limits worth knowing

Frequently Asked Questions

How do I add a table of contents to a markdown file?

Paste your markdown above, press Generate, and copy the TOC block to the top of your file (or use the insert button, which replaces any existing TOC between the markers). Each entry links to its heading with an anchor like #getting-started, which works on GitHub, GitLab, and most markdown renderers.

How does GitHub generate anchor links for headings?

Lowercase everything, strip punctuation and emoji, and turn spaces into dashes. Punctuation like +, %, ?, and / disappears; accented letters (ü, é) stay; underscores stay. If the result collides with an earlier anchor on the page, GitHub appends -1, -2: two headings called Docs become #docs and #docs-1. This generator runs GitHub's own slug algorithm, so the links it emits are the anchors GitHub actually assigns.

Why do my duplicate headings break the TOC?

Because every heading on a page must have a unique anchor. When two headings have the same text, the second one's anchor gets a numeric suffix — Getting Started appears twice becomes #getting-started and #getting-started-1 — and a TOC that links both to #getting-started sends both clicks to the first section. The generator tracks duplicates the same way GitHub does and emits the right suffix automatically.

What are the doctoc markers for?

The START/END HTML comments mark the generated region so it can be replaced wholesale later. The doctoc command-line tool uses exactly these markers; this generator is compatible with them — run it on a file that already has a marked TOC and it swaps in a fresh one instead of growing a second copy.

Why is my anchor percent-encoded, like %C3%BC?

Accented and non-ASCII characters in anchors get URL-encoded in links — a heading like Über café produces #%C3%BCber-caf%C3%A9 -- co. That's correct and it's what doctoc emits: browsers decode it back to the characters when you click. Copy the link as-is; don't hand-decode it.

Does the TOC work outside GitHub?

Mostly. GitLab, Bitbucket, VS Code's markdown preview, and most static site generators use the same or compatible anchor rules. Some platforms differ in edge cases (emoji, capitalization, or symbols), so if a link doesn't jump on your renderer, compare the anchor your renderer assigned (hover the heading's link icon) with what the TOC emits.

Advertisement