Markdown Table of Contents (TOC) — Complete Guide to Manual & Auto Generation
When you're writing a long document, having a Table of Contents (TOC) lets readers jump straight to the sections they care about. Markdown doesn't have a built-in TOC syntax, but there are several ways to pull it off — and some are simpler than you'd expect.
How Markdown TOC Works
A Markdown TOC is essentially a set of internal links (also called anchor links), each pointing to a heading within the page. The renderer automatically converts your headings into HTML anchors, so you just need to link to them.
The basic format looks like this:
## Table of Contents
- [Introduction](#introduction)
- [Installation](#installation)
- [Usage](#usage)
- [FAQ](#faq)
## Introduction
Introduction content here...
## Installation
Installation steps...The key is understanding how headings become anchors — the rules differ across platforms, and once you get that figured out, writing a TOC manually is straightforward.
Anchor Generation Rules Across Platforms
This is something a lot of people overlook: the same heading can produce completely different anchors on GitHub, GitLab, and Bitbucket. I once wrote a TOC manually for a project's README that worked fine on our internal GitLab, but after pushing to GitHub, every single link was broken. Took me a while to track down — turned out the anchor rules were different.
GitHub Anchor Rules
GitHub's rules are the most commonly used and fairly intuitive:
| Rule | Example |
|---|---|
| Convert to lowercase | ## Hello World → #hello-world |
| Spaces become hyphens | ## Getting Started → #getting-started |
| Remove punctuation | ## What's New? → #whats-new |
| Merge consecutive hyphens | ## Cost ($) Analysis → #cost--analysis |
| Duplicate headings get numbered | Second ## Example → #example-1 |
GitLab Anchor Rules
GitLab's rules are largely similar to GitHub's, but with one key difference — GitLab supports the [[_TOC_]] tag to automatically generate a table of contents without any manual work:
## Document Title
[[_TOC_]]
## Section One
Content...
## Section Two
Content...[[_TOC_]] generates a clickable TOC based on all headings in the page. This feature has been available since GitLab 14.x and works in both wikis and README files.
Bitbucket Anchor Rules
Bitbucket is a bit special — it prefixes all anchors with markdown-header-:
- [Introduction](#markdown-header-introduction)
- [Installation](#markdown-header-installation)If your document is only used on Bitbucket, remember to add this prefix.
Handling Non-ASCII Characters
For headings with non-ASCII characters (Chinese, Japanese, Korean, etc.), GitHub and GitLab generally use URL-encoded anchors. In practice, you can often link directly using the original characters:
## Table of Contents
- [Quick Start](#quick-start)
- [Configuration](#configuration)
## Quick Start
## ConfigurationMost modern Markdown renderers handle non-ASCII anchors correctly. But if you find that a particular platform doesn't support it, you can use an HTML <a> tag to add a manual anchor:
<a name="custom-anchor"></a>
## Some Heading
Then link to it in your TOC:
- [Some Heading](#custom-anchor)Creating a TOC Manually
If your document isn't too long, or the headings rarely change, writing a TOC by hand is the most direct approach.
Basic Steps
- Write all your headings first
- Add a "Table of Contents" section at the top (or wherever you want it)
- Use list + link syntax to point to each heading
## Table of Contents
- [Features](#features)
- [Installation](#installation)
- [npm Install](#npm-install)
- [Manual Install](#manual-install)
- [Usage](#usage)
- [License](#license)
## Features
...
## Installation
...
### npm Install
...
### Manual Install
...
## Usage
...
## License
...Use indentation for nested headings so the TOC structure mirrors the document structure.
The thing about manual TOCs is that the hard part isn't writing them — it's maintaining them. Every time you change a heading, you have to remember to update the TOC too. For long documents that change frequently, I'd recommend using an auto-generation tool instead.
Auto-Generating TOC with VS Code Extensions
If you write Markdown in VS Code (which a lot of people do), installing an extension gives you one-click TOC generation with auto-update on save.
Markdown All in One
This is the most recommended VS Code Markdown extension, developed by Yu Zhang, with over 6 million installs on the VS Code Marketplace.
Installation and Usage:
- Search for
Markdown All in Onein the VS Code Extensions marketplace and install it - Open your
.mdfile and place the cursor where you want the TOC - Press
Ctrl+Shift+Pto open the Command Palette, typeMarkdown: Create Table of Contents - The TOC is generated automatically
The generated TOC is a plain Markdown list, identical in format to a manually written one, so it works on GitHub, GitLab, and anywhere else.
Auto-Update Feature:
By default, the extension updates the TOC every time you save the file. If you don't want this (say you have custom content near the TOC), you can disable it in VS Code settings:
{
"markdown.extension.toc.updateOnSave": false
}I've been using this extension for almost two years now, and it's generally great. One caveat: if your document mixes HTML tags with Markdown headings, the extension can sometimes misidentify them — worth manually checking the generated TOC in that case.
Auto Markdown TOC
Another option is the Auto Markdown TOC extension by huntertran. It's similar but lighter weight. After installing, a "Markdown TOC" option appears in the right-click context menu, and the workflow is intuitive.
Auto-Generating TOC with CLI Tools
If you need batch processing in a CI/CD pipeline, or prefer not to depend on a specific editor, command-line tools are the way to go.
doctoc — The Most Popular TOC Generator
doctoc is a Node.js tool with over 4,000 stars on GitHub, making it the most widely used Markdown TOC generator.
# Install
npm install -g doctoc
# Generate TOC for all Markdown files in current directory
doctoc .
# Process a single file
doctoc README.mddoctoc inserts special comment markers in your file. When you run doctoc again, it automatically updates the existing TOC instead of duplicating it:
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
**Table of Contents**
- [Introduction](#introduction)
- [Installation](#installation)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->The marker mechanism is clever — you shouldn't edit the TOC content directly. Just re-run doctoc whenever you change headings.
markdown-toc — A Flexible Node.js Tool
Another Node.js tool by jonschlinkert, used by NASA, Prisma, Mocha, Prettier, and other well-known projects.
# Install
npm install -g markdown-toc
# Generate TOC and output to terminal
markdown-toc README.md
# Save to a file
markdown-toc README.md > TOC.mdUnlike doctoc, markdown-toc outputs to stdout by default, giving you flexibility — paste it into a file, pipe it to another tool, whatever works.
gh-md-toc — A Shell Script Built for GitHub
If you don't want to install Node.js, this pure shell script does the job.
# Download (Linux)
wget https://raw.githubusercontent.com/ekalinin/github-markdown-toc/master/gh-md-toc
chmod a+x gh-md-toc
# Download (macOS)
curl https://raw.githubusercontent.com/ekalinin/github-markdown-toc/master/gh-md-toc -o gh-md-toc
chmod a+x gh-md-toc
# Usage
./gh-md-toc README.mdIt can also process remote GitHub files and even GitHub Wiki pages:
./gh-md-toc https://github.com/user/repo/blob/main/README.mdIn auto-insert mode, place markers in your file:
<!--ts-->
<!--te-->Then run ./gh-md-toc --insert README.md, and the TOC will be inserted between the two markers. Every time you change headings, just run the command again.
md-toc — For Python Users
If you're more comfortable in the Python ecosystem, md-toc is a solid choice:
pip install md-toc
md_toc --in-place github --header-levels 6 README.mdIt supports both GitHub and GitLab anchor generation modes and lets you control which heading levels to include.
Online TOC Generators
If you don't want to install anything, online generators are the most convenient:
- BitDownToc (bitdowntoc.derlin.ch): Supports multiple platform profiles including GitHub, GitLab, dev.to, and hashnode. Paste your Markdown content and get a platform-specific TOC. Free and open source.
Tool Comparison: Which One to Pick?
| Tool | Type | Platform Support | Auto-Update | Dependencies |
|---|---|---|---|---|
| Manual | — | All | None | None |
| Markdown All in One | VS Code extension | All | On save | VS Code |
| Auto Markdown TOC | VS Code extension | All | Manual trigger | VS Code |
| doctoc | CLI | GitHub/GitLab | Re-run to update | Node.js |
| markdown-toc | CLI | GitHub | Outputs to stdout | Node.js |
| gh-md-toc | Shell script | GitHub | Updates between markers | curl/wget |
| md-toc | CLI | GitHub/GitLab | Updates between markers | Python |
| BitDownToc | Online tool | Multi-platform | Manual copy | Browser |
GitLab [[_TOC_]] | Platform native | GitLab only | Automatic | None |
My recommendations:
- Writing a GitHub README? The VS Code extension or doctoc is all you need
- GitLab only? Just use
[[_TOC_]]— nothing beats that simplicity - Need CI automation? Use a CLI tool with a script
- One-time use? An online tool is the quickest option
Auto-Maintaining TOC with GitHub Actions
If you have a documentation repository and don't want to manually run tools every time you change a heading, GitHub Actions can fully automate the process.
The following configuration automatically updates your TOC when you push Markdown files:
name: Update TOC
on:
push:
branches: [main]
paths: ['**/*.md']
jobs:
toc:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- run: |
curl https://raw.githubusercontent.com/ekalinin/github-markdown-toc/master/gh-md-toc -o gh-md-toc
chmod a+x gh-md-toc
./gh-md-toc --insert --no-backup README.md
rm gh-md-toc
- uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: "Auto update markdown TOC"Every time you push code, Actions checks if the Markdown files' TOC needs updating and auto-commits a new version if it does.
Handling Duplicate Headings and Special Characters
When writing real-world documentation, you'll run into some edge cases.
Duplicate Headings
If a document has two identical headings, GitHub automatically adds a number to the second one:
## Example
This is the first example...
## Example
This is the second example...The corresponding anchors are #example and #example-1. When linking in your TOC, keep this in mind:
- [Example (first)](#example)
- [Example (second)](#example-1)Special Characters
When headings contain characters like $, (), or ?, GitHub strips them out:
## Version 2.0 (Major Update)The anchor becomes #version-20-major-update — note the period is kept, but the parentheses are gone.
Duplicate Headings Across Platforms
| Platform | Duplicate Heading Handling |
|---|---|
| GitHub | Second gets -1, third gets -2, and so on |
| GitLab | Same numbered suffix approach |
| Bitbucket | Same numbered suffix, but with markdown-header- prefix |
If you're unsure what anchor a heading generates, there's a handy trick on GitHub: hover over the heading, and a link icon appears — click it to see the actual anchor in your browser's address bar.
FAQ
Does GitHub README support the [TOC] tag?
No. GitHub doesn't recognize the [TOC] marker (unlike some Markdown editors). On GitHub, you need to use the methods described above — either write it manually or generate it with a tool.
Why aren't my TOC links jumping to the right headings?
The most common cause is incorrect anchor formatting. Check these:
- Is the anchor all lowercase?
- Are spaces converted to hyphens?
- Are there leftover punctuation marks?
- On Bitbucket, did you forget the
markdown-header-prefix?
How do I show only specific heading levels in the TOC?
With the Markdown All in One extension for VS Code, you can control this in settings:
{
"markdown.extension.toc.levels": "2..4"
}This shows only H2 through H4 headings. With CLI tools, doctoc uses the --maxlevel flag, and md-toc uses --header-levels.
What's the difference between GitHub's auto-generated TOC and a manual one?
Since April 2021, GitHub automatically displays an "Outline" menu in the header of Markdown files that lists all headings. This feature is generated by GitHub's frontend and doesn't require anything in your file. However, it only appears on the GitHub website and doesn't affect your Markdown file's content.
So if you're writing documentation on GitHub, the manually or tool-generated TOC in your file works everywhere (including when cloned locally), while GitHub's built-in Outline is an extra layer of navigation on the web only.