Markdown TOC Generator
Turn your headings into a Markdown table of contents — GitHub and GitLab anchor styles side by side, entirely on this device.
Where the two anchor styles disagree
Headings that slug differently on GitHub and GitLab are listed here.
Press ? for shortcuts
Do it offline:$
How to use it
- Paste Markdown, or open a .md, .markdown or .txt file from your disk — reading it needs no server, so nothing is uploaded.
- Choose the anchor style your repository is hosted on, then set the shallowest and deepest heading levels worth listing.
- Pick the bullet and the indent step, drop the first H1 when it is only the document title, or turn links off for a plain outline.
- Copy the list, or open the Document tab to take the whole file back with the block written between the toc and tocstop comments.
A table of contents is only as useful as its anchors, and the two big hosts do not spell them the same way. Paste a document here and its headings are read through this site's own Markdown parser, then written out as a list in whichever anchor style you pick — with the GitHub and GitLab spelling of every heading shown beside each other, so a disagreement is visible before you commit.
Examples
# Project
## Install
## Usage
### CLI
## FAQ
- [Install](#install)
- [Usage](#usage)
- [CLI](#cli)
- [FAQ](#faq)Skip first H1 is on, so the title drops out and the remaining H2s become the top row; CLI is an H3 and takes one indent step. Nothing here is contested — all four anchors read the same on either host.
## Intro
### Setup
## Intro
### Setup
- [Intro](#intro)
- [Setup](#setup)
- [Intro](#intro-1)
- [Setup](#setup-1)Repeats are numbered in the order they appear — the second Intro anchors at #intro-1 — and the tool notes that two headings repeat an earlier anchor. Each style counts separately, because two headings can collide on one host and not the other.
## 2024
## C++ & Rust
- [2024](#2024)
- [C++ & Rust](#c--rust)Two disagreements in two lines. GitHub leaves a digits-only heading alone and keeps the dash run that ++ and & leave behind. GitLab writes #anchor-2024 and #c-rust instead: bare numbers get a prefix, dash runs squeeze to one.
# Notes
```sh
# not a heading
echo hi
```
## ???
## Real
- [Notes](#notes)
- ???
- [Real](#real)The hash line inside the shell fence is code, so the parser never offers it as a heading; it is counted in a note instead. The question marks stay a heading but lose every character to the slug rules, so no id exists to link to and the row prints bare.
Every example on this page is executed against the same engine that powers the tool, on every build.
Gotchas worth knowing
Setext, lists and blockquotes are all headings
A title underlined with = or - counts, and so does a heading inside a list item or a blockquote. Their nesting is not carried over: indentation follows the level number alone, so an H2 in a bullet sits where any other H2 sits.
A skipped level is flattened, never filled in
Jump straight from H2 to H4 and the H4 takes one indent step under the H2, named by line in a note. No placeholder H3 is invented, and no gap is left: indented past its parent, a row stops being a list item at all.
Leading YAML front matter is skipped
A --- block at the very top is metadata, not prose, yet a strict CommonMark reader takes its closing --- for an underlined heading. It is set aside before parsing and counted back afterwards, so no phantom row appears and line numbers still match your file.
Updating in place is safe to repeat
When the markers already exist, everything between them is replaced and both are rewritten in their canonical spelling, so a second run gives byte-identical text. An opening marker with nothing closing it gets its partner added, and a second pair further down is reported and left alone.
Common errors
Input is larger than the 4 MB limit for this tool.- The same 4 MB ceiling the site's Markdown parser enforces everywhere. Exactly 4,194,304 characters still go through; one more is refused whole, never trimmed.
This document has 2001 headings — more than the 2000 this tool lists. Split the document, or narrow the heading levels and try again.- Past two thousand headings the list stops being a table of contents. The count is exact and appears in the message, and narrowing the depth range is usually enough to get under it.
The minimum level H4 is deeper than the maximum level H2. Set the minimum to H2 or shallower.- The depth range was inverted, so it is refused rather than quietly returning an empty list. Both levels are named, so it is obvious which one to move.
FAQ
Why do GitHub and GitLab anchors differ?
They run different code. GitHub lowercases, drops anything that is not a letter, mark, number, underscore or dash, and turns spaces into dashes — leaving runs of dashes intact. GitLab also deletes anything outside its own word class, then squeezes repeated dashes to one and prefixes a digits-only result with anchor-. Both are implemented from the sources cited below, re-checked on 13 August 2026 and pinned by golden vectors. Bitbucket, Azure DevOps and Confluence use rules of their own and are not offered.
Will re-running duplicate my table of contents?
No. The Document tab finds the toc and tocstop comment markers and replaces what sits between them, rewriting both in canonical form, so a second run produces the same bytes. Only a file with no markers gets a fresh block, placed after any front matter and after the title heading when you skip it.
Do headings inside code blocks count?
They do not. Headings come from this site's Markdown parser rather than a search for lines starting with a hash, so a hash line inside a fenced block, an indented code block or a raw HTML block is never a heading at all. Fenced ones are still counted in a note, so you can see they were noticed.
Keyboard shortcuts
- ⌘/Ctrl + Enter
- Rebuild the list now, whatever the document size.
- ⌘/Ctrl + S
- Download the whole document with the block inserted, as .md.
- ⌘/Ctrl + C
- Copy the active result tab while nothing is selected.
- ⌘/Ctrl + Shift + C
- Copy the active result tab even over a selection.
- t
- Cycle the theme palette (idle inside a text field).
- ? or ⌘/Ctrl + /
- Open the shortcut overlay.
- Esc
- Close the topmost dialog; the source pane always stays.