Self-hosted service
thlorenz/doctoc avatar
thlorenz/doctoc

doctoc: Generating Markdown Tables of Contents That Match GitHub's Anchors

📜 Generates table of contents for markdown files inside local git repository. Links are compatible with anchors generated by github or other sites.

4,468 stars483 forksJavaScriptMIT

At a glance

What is it?
doctoc is a Node.js CLI that inserts and refreshes tables of contents in Markdown files, with renderer flags for GitHub, GitLab, Bitbucket, Node.js and Ghost anchors. It is a small tool with a narrow job, and the interesting parts are the pragma blocks it leaves behind and the options that decide which headings get listed.
Who is it for?
Adopt doctoc if you maintain Markdown documentation in a git repository and want TOC links that resolve on the hosting site you actually publish to, using --github, --gitlab, --bitbucket, --nodejs or --ghost to pick the anchor scheme. Skip it if your headings are generated at build time by a static site generator, or if you need a TOC that reflects content doctoc cannot parse.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 1 day ago.
What is it written in?
Mainly JavaScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 30, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The problem doctoc solves, and who actually has it

Long Markdown files are hard to scan. A README that runs to several hundred lines, or a docs directory where each page has a dozen headings, forces readers to scroll and search. Most Markdown renderers do not add a table of contents automatically, and the ones that do (static site generators, for instance) only do so when the page is built. If you are reading a raw .md file on GitHub or in an editor, there is nothing there.

doctoc fills that gap by writing the TOC into the file itself. It walks a Markdown document, collects the headings, and emits a nested bullet list whose links point at the anchors the hosting site will generate. The README states the tool is for "markdown files inside local git repository", which is the honest scope: it is a local command-line tool, not a service and not a build plugin. The audience is people who keep documentation in the same repository as the code and want the TOC to be part of the committed text, so it shows up in diffs, in code review, and in the rendered view on the hosting platform.

The renderer flags are the part that matters most, because anchor generation is not standardised. GitHub, GitLab, Bitbucket, nodejs.org and Ghost all produce different slugs for the same heading text, and a TOC that links to the wrong slug is worse than no TOC at all. doctoc lets you declare which site you are targeting rather than guessing.

How doctoc decides what goes in the table of contents

The mechanism is straightforward. doctoc parses the Markdown, walks the heading structure, and replaces the region between its pragma comments with a freshly generated list. The pragma is a pair of HTML comments, and the default style is called legacy:

markdown
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
{{toc}}
<!-- END doctoc generated TOC please keep comment here to allow auto update -->

Those comments are the whole contract. Anything between them is owned by the tool and will be overwritten on the next run; anything outside them is untouched. If a file has no pragma, doctoc inserts one. The README also documents a compact pragma style that reduces the block to a bare `<!-- START doctoc -->` and `<!-- END doctoc -->` pair, which is easier to read in a diff but loses the warning text that tells a human not to hand-edit the region.

Heading selection is controlled by several options that interact. `--minlevel` restricts entries to headings at or above a level, and the README notes that only values 1 and 2 are currently supported. `--maxlevel` caps the depth; Markdown headings have no default cap, but headings coming from embedded HTML are limited to 4 levels. By default only headings below the TOC are included, and `--all` overrides that to include every heading regardless of position. Placement is a separate concern: `--toc-location top` puts the TOC at the very top, after any front matter, while `--toc-location before` puts it just before the first heading that appears in the TOC. The README states that top is the default and that v3 will change the default to before, which is worth knowing before you build tooling around the current behaviour.

The parser dependencies listed in package.json tell you what kind of tool this is: `@textlint/markdown-to-ast` for the Markdown AST, `htmlparser2` for embedded HTML, and `anchor-markdown-header` for slug generation. There is no incremental cache and no index. Each run re-parses the file from scratch.

Installing doctoc and generating your first TOC

doctoc is published on npm and the README gives a single global install command. You need Node.js available on the machine; the package declares its binary as `doctoc.js`.

bash
npm install -g doctoc

Once installed, point it at a file. Running it against a single README inserts the pragma block and the generated list near the top of the document, and prints progress at the default info log level.

bash
doctoc README.md

If you publish to a site other than GitHub, add the matching renderer flag so the anchor slugs are generated for that site. The README lists the available modes as `--bitbucket bitbucket.org`, `--nodejs nodejs.org`, `--github github.com`, `--gitlab gitlab.com` and `--ghost ghost.org`.

bash
doctoc README.md --bitbucket

To process a whole tree, pass a directory. The recursive search picks up `.md`, `.markdown` and `.mdx` files, and by default skips `.git` and `node_modules` if they exist.

bash
doctoc .

After the first run, open the file and look for the START and END doctoc comments. If you see them with a bullet list between them, the tool did its job. Re-running the same command updates the list in place rather than adding a second one, which is what makes it safe to put in a pre-commit hook or a lint-staged configuration.

The dry run flag and what it is actually good for

The most useful option for teams is `--dryrun`. It does not write changes; instead it returns an exit code of 1 to signal that files are out of date and should be updated. The README explicitly frames this as useful in CI, where you want to check whether documentation is current as part of the build.

bash
doctoc --dryrun .

This is a cleaner fit for CI than running the tool and then checking `git diff`, because the exit code is the signal. Note the asymmetry the README describes: `--stdout` prints to standard output, but only when you specify a single filename. For a folder or multiple files, the documentation says to use the dry run option instead. That is a real constraint on how you script the tool. If you want to inspect the generated TOC without touching the file, you are limited to one file at a time.

The companion flag is `--update-only` (or `-u`), which only refreshes existing TOCs and leaves files that have no TOC alone. The README recommends it for use with lint-staged, and the reasoning holds: in a pre-commit hook you want to update TOCs that already exist, not silently introduce a new section into a file the author did not intend to change.

Where doctoc stops being the right tool

The clearest limitation is the one the README states without dressing it up: doctoc processes your document as a text processor. The note on `--document-lines-min` says that images are not counted any differently from plain text, and that repeated newlines count too. So if you set a minimum line threshold to avoid adding TOCs to short files, you are measuring raw lines, not rendered length or content substance. A file padded with blank lines can clear the threshold while a dense file with a few long paragraphs does not.

The second limitation is that doctoc only sees what a Markdown parser sees. If your headings are produced at build time by a static site generator, or come from included partials, or are generated from front matter, doctoc will not find them and the TOC will be incomplete. The tool works on the text of the file it is given.

The third is scope control. The `--minlevel` option supports only 1 and 2, so you cannot, for example, restrict entries to level 3 headings only. And the default placement behaviour is scheduled to change in v3, from top to before. Anyone who has written scripts that assume the TOC sits at the very top of the file should treat that as a migration item rather than a stable assumption. The README does not document a rollback path for the pragma format change, so pinning a version is the practical answer.

How doctoc compares to a build-time TOC plugin

The obvious alternative is a plugin in whatever static site generator you already use, or a documentation framework that builds a sidebar from the heading structure. The difference in approach is where the TOC lives. A build-time plugin generates navigation at render time and never writes it into the source file, so the .md file on disk contains only prose. doctoc does the opposite: it commits the TOC into the file, which means the TOC appears in git diffs, in code review, and in any plain-text view of the repository.

That trade-off cuts both ways. Committed TOCs go stale and need a CI check to stay current, which is exactly what `--dryrun` exists to provide. Generated-at-build TOCs can never go stale because they are recomputed every time, but they also do not exist when someone reads the raw file on GitHub or in an editor. If your readers only ever see the rendered site, a build-time plugin is the lower-maintenance choice. If your readers include people browsing the repository, doctoc is the only one of the two that helps them.

A second alternative is writing the TOC by hand, which is what most people do until the document gets long enough that manual anchor slugs become error-prone. doctoc's renderer flags exist precisely because hand-maintained slugs break when the heading text changes.

Maintenance status, licence and upgrade cost

The repository is not archived, and the last push was on 2026-08-04. That is recent enough that the project is not abandoned, and the release history supports the same reading: v2.5.0 on 2026-06-12, v2.4.1 on 2026-04-15, and v2.4.0 on 2026-04-06. Three releases in roughly two months is a normal cadence for a tool of this size.

The dependency surface is small, which keeps upgrade cost low. package.json lists five runtime dependencies: `@textlint/markdown-to-ast`, `anchor-markdown-header`, `htmlparser2`, `loglevel` and `minimist`. The dev dependency is `tap`. None of these are heavy frameworks, and the tool has no server component, no database and no configuration file of its own. Upgrading means changing a version number and re-running the tool across your documents.

The licence is MIT. In practical terms that permits commercial and private use, modification and redistribution, subject to the terms of the licence text itself. This is not legal advice; read the LICENSE file in the repository if the distinction matters to your organisation.

The upgrade cost that is not about dependencies is the v3 placement change mentioned in the README. If your workflow depends on the TOC being at the top of the file, that default is documented as changing to before. Running `doctoc --toc-location top .` explicitly makes your intent visible in the command rather than relying on a default that is scheduled to move.

Editorial conclusion

Adopt doctoc if you maintain Markdown documentation in a git repository and want TOC links that resolve on the hosting site you actually publish to, using --github, --gitlab, --bitbucket, --nodejs or --ghost to pick the anchor scheme. Skip it if your headings are generated at build time by a static site generator, or if you need a TOC that reflects content doctoc cannot parse. Before rolling it out across a repository, run doctoc --dryrun . in CI to see which files are out of date, then check how the legacy pragma comments interact with your existing front matter and any Markdown linter.

Frequently asked questions

What does doctoc do?

doctoc generates a table of contents for Markdown files in a local git repository and writes it into the file between pragma comments. The README states that the links are compatible with anchors generated by GitHub or other sites when you pass the matching renderer flag.

How do I install doctoc?

The README gives a single command, npm install -g doctoc, which installs the package globally from npm. The package declares doctoc.js as its binary, so the doctoc command becomes available on your PATH.

Which file types does doctoc process?

When given a directory, doctoc performs a recursive search and processes only files with the .md, .markdown and .mdx extensions. By default the recursive search ignores the .git and node_modules directories if they exist.

How do I check in CI whether my table of contents is out of date?

Use the --dryrun option, which does not write changes and instead returns an exit code of 1 to indicate files are out of date. The README describes this as useful in CI environments where you want to check that your docs are current as part of the build.

Can I print the generated table of contents to stdout?

Yes, with the -s or --stdout option. The README notes this is only applicable when specifying a single filename; for a folder or multiple files, the dry run option should be used instead.

Official sources

  1. License: MIT
  2. Project website
  3. README
  4. Releases
  5. thlorenz/doctoc on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/thlorenz-doctoc.svg)](https://hysenlabs.com/projects/thlorenz-doctoc)