markdown toc markdown documentation github toc static site toc doc automation

Markdown Table of Contents That Actually Works

Learn how to build a markdown table of contents that survives GitHub, Hugo, MkDocs, and Docsify, with auto-generation tools and SEO tips.

GitDoc Team
GitDoc Team
Editorial · · 14 min read
Markdown Table of Contents That Actually Works

You ship a polished README, merge a documentation redesign, and move on to the next release. A few weeks later, someone renames a heading, another person adds a section above it, and readers start landing on the wrong place. The markdown table of contents still looks correct in the source file, but several links no longer resolve.

That failure isn’t a Markdown syntax problem. It’s a synchronization problem across heading parsers, anchor rules, editors, and documentation platforms. A TOC that works in GitHub can drift in Hugo, render twice in MkDocs, or point at different IDs in an MDX site. Reliable navigation requires deterministic headings, a generator that matches the target renderer, and automation that checks the result whenever documentation changes.

Table of Contents

When a Markdown Table of Contents Quietly Breaks

A developer I worked with had published a README with a carefully arranged manual TOC. It covered the setup guide, configuration reference, deployment notes, and troubleshooting sections. The document looked finished, and the links worked when it was first reviewed.

Later, a contributor renamed “Configuration Options” to “Environment Configuration” and split the deployment section into two smaller topics. The headings improved, but the TOC did not. Several entries still pointed to the old slugs, and one newly added section never appeared in the list. Nothing in the pull request made the defect obvious because the Markdown remained valid and the rendered page still looked polished.

Practical rule: A valid Markdown file can still contain invalid navigation.

Manual TOCs fail because their visible labels and destination anchors are maintained separately. The first heading edit creates the risk. A later restructure increases it. If the same source is published through GitHub, Hugo, and MkDocs, each renderer can expose a different version of the problem.

Why platform differences matter

A Markdown TOC is deliberately simple. Documentation tools describe it as a Markdown list, which keeps it portable across editors, static-site generators, and repository readmes. That portability doesn’t mean every platform creates identical heading IDs or interprets every TOC convention the same way. Markdown Editor Online’s guidance on maintainable TOCs recommends building from a clean hierarchy, placing the TOC after the introduction, and limiting depth so navigation remains scannable.

GitHub introduced native automatic TOC support for Markdown files on 2021-04-13, generating a TOC in the header when a file contains two or more headings. The GitHub changelog announcement made that a practical milestone for repository documentation, but native support doesn’t solve portability across other renderers.

The reliable fix has several parts:

  • Stable headings: Use descriptive, unique headings and avoid changing wording casually.
  • Known slugs: Confirm how each publishing platform normalizes punctuation, case, and duplicates.
  • Scoped generation: Include only the heading levels that help readers scan the document.
  • Rendered testing: Check links in the exact environment readers will use.
  • Continuous updates: Regenerate the TOC when headings change, not on a delayed schedule.

The rest of the workflow follows that order. First establish the anchor contract, then choose a generator, configure each renderer, and let CI reject stale output before it reaches production.

Anchors, Headings, and the Rules Every TOC Depends On

A TOC link has two components: the text a reader sees and the fragment after the hash. The text can remain readable while the fragment becomes wrong. Treat the heading and its generated ID as one interface, not as separate formatting.

GitHub-Flavored Markdown commonly derives an anchor by lowercasing the heading and replacing word separators with hyphens. A heading such as ## Install Docker commonly becomes #install-docker. That rule is useful, but it isn’t a universal contract. GitLab, static-site generators, themes, and Markdown plugins can normalize punctuation differently, particularly around symbols and dash characters.

Build headings for predictable anchors

Keep the hierarchy shallow and deliberate:

  • # identifies the document title.
  • ## marks major topics.
  • ### divides a major topic into useful subsections.
  • Deeper levels should earn their place because they make a long document easier to scan, not because the source file permits them.

A heading such as ## Docker: Local Setup can expose punctuation differences. A naive generator may preserve or strip the colon differently from the renderer. An em dash can create another mismatch, especially when one platform treats it as punctuation and another folds it into the surrounding words. If the exact wording matters, an explicit ID can remove ambiguity where the platform supports attribute syntax, including Hugo, MkDocs configurations that enable attribute lists, and Docusaurus patterns that preserve custom IDs.

Duplicate headings are another common trap. Renderers often disambiguate repeated text by adding suffixes such as -1 and -2, but the behavior and numbering can vary. “Examples” repeated across a document is less reliable than “Python examples” and “JavaScript examples,” even when both sections discuss similar material.

For API and developer documentation, heading discipline belongs beside endpoint structure and code-example conventions. Teams maintaining those systems can use this API documentation in Markdown resource as a related reference, but the same anchor checks apply to any long-form technical page.

Test the rendered destination

Don’t infer the final ID from the source alone. Render the document in GitHub, the local static-site preview, and the deployed documentation environment when those outputs differ. Then select every TOC entry and verify that it lands on the intended heading.

Anchor checklist: If a TOC link won’t jump to the right section locally, it won’t become more reliable after deployment.

Choosing Between GitHub Native TOC and Auto-Generation Tools

There are three workable approaches, and each optimizes for a different stage of documentation work.

GitHub’s native TOC is the fastest option for repository content that GitHub renders directly. The platform can generate a TOC for Markdown files containing two or more headings, and its plain-text approach fits naturally into readmes and repository documentation. It becomes less useful when the same Markdown must build through Hugo, MkDocs, Docsify, or mdBook, because those systems may apply different heading and layout rules.

Editor extensions improve authoring speed. Markdown All in One and similar VS Code extensions can insert or update a TOC while a writer edits. They’re useful during drafting, especially when a contributor wants immediate navigation without installing a separate command-line tool. Their weakness is workflow consistency. Each contributor can have a different extension, configuration, omission marker, or update habit.

CLI generators are the durable choice for repositories with continuous deployment. Tools such as doctoc, md-toc, and markdown-toc can run in pre-commit hooks or GitHub Actions, where the team can pin the command, depth, marker format, and bullet style. The generator still needs validation against the target renderer, but the process no longer depends on one editor.

MethodSpeedAnchor AccuracyCI FriendlyPlatform Portability
GitHub native TOCVery fast in GitHub-rendered contentStrong for GitHub outputLimited outside GitHub renderingLow to moderate
Editor extensionFast during local editingDepends on extension and rendererInconsistent unless standardizedModerate
CLI generatorFast after setupStrong when tested against the target rendererStrongStronger across static-site builds

Make the decision by publishing target

Use native generation when the document lives primarily in GitHub and you don’t need a committed TOC block. Use an editor extension when local convenience matters and the team has a shared configuration. Choose a CLI generator when the Markdown is an input to multiple builds, especially when pull requests and branch previews must produce repeatable output.

The documentation automation tools resource is useful background for teams evaluating broader automation, but TOC generation should remain narrowly configured. A tool that rewrites prose, changes heading text, or applies broad formatting creates more risk than it removes. The safest generator owns only a clearly marked TOC region.

Configuring TOCs in Hugo, MkDocs, Docsify, Docusaurus, and mdBook

The safest cross-platform design separates the source document from the site’s presentation layer. A committed Markdown TOC can serve GitHub and plain readers, while the site theme can render a side navigation or inline outline. Don’t enable both without checking whether the result duplicates the same headings.

Platform configuration patterns

Hugo provides a built-in .TableOfContents template value. A page template can render selected heading depths and choose an ordered list through site or template logic:

{{ $toc := .TableOfContents }}
{{ with $toc }}
  <nav aria-label="On this page">{{ . }}</nav>
{{ end }}

For finer control, configure the TOC start and end depth in the page or site configuration supported by the Hugo version and theme, then confirm the theme isn’t rendering another outline automatically. Hugo also supports custom heading IDs through suitable Markdown rendering configuration, which is useful when a public anchor must survive wording changes.

MkDocs uses the toc Markdown extension. A typical configuration enables permalinks and limits the generated depth:

markdown_extensions:
  - toc:
      permalink: true
      toc_depth: 3

Material for MkDocs may place the TOC in a right rail, so an inline source TOC can become redundant. Decide whether the repository needs the committed list for GitHub readers before enabling both.

Docsify doesn’t treat a source TOC as a complete replacement for its navigation model. A plugin can generate an in-page outline while Docsify renders Markdown content:

<script>
  window.$docsify = {
    markdown: {
      renderer: {
        heading: function(text, level, raw) {
          return '<h' + level + ' id="' + raw.toLowerCase().replace(/\s+/g, '-') + '">' + text + '</h' + level + '>';
        }
      }
    }
  };
</script>
<script src="//cdn.jsdelivr.net/npm/docsify-toc"></script>

Test custom renderer behavior carefully. A hand-written slug function can diverge from the platform’s escaping rules.

Docusaurus exposes TOC behavior through Markdown and MDX configuration. A page can set a depth range in front matter:


---
toc_min_heading_level: 2
toc_max_heading_level: 3

---

The site’s theme can render the outline beside the page, while MDX-specific options control how headings participate in the table of contents. Avoid adding a source TOC when the theme already renders the same H2 and H3 tree.

mdBook uses SUMMARY.md as its navigation source. That file controls chapter ordering and links, while an inline page TOC requires a separate approach supported by the chosen renderer or theme. Keep the summary structure aligned with the document’s actual headings, and don’t mistake chapter navigation for an in-page heading outline.

PlatformConfig Key / FileDepth DefaultAnchor Support
Hugo.TableOfContents, Markdown rendering configurationTheme or template dependentGenerated IDs and configurable custom IDs
MkDocsmarkdown_extensions, toc_depthExtension or theme dependentGenerated IDs with permalink support
Docsifywindow.$docsify, renderer and TOC pluginPlugin dependentRenderer-generated IDs require testing
DocusaurusFront matter and docs theme configurationTheme dependentMDX heading IDs and configurable TOC depth
mdBookSUMMARY.md, theme or renderer settingsNavigation-file dependentChapter links and renderer-generated heading IDs

The exact key matters less than the test. Build the site, inspect the heading IDs, and compare them with the committed links. If a platform already generates an outline, disable the duplicate source or theme version instead of asking readers to choose between two identical lists.

Automating TOC Updates in CI

A TOC check should behave like a link check. The workflow needs to generate the expected block, compare it with the committed file, and fail or propose a change when the two differ. That catches heading renames, inserted sections, duplicate labels, and accidental edits to the navigation list.

A practical GitHub Actions job installs a pinned generator, scans docs/**/*.md, and limits changes to the marked TOC region. The generator should receive explicit settings for heading depth, bullet style, and omission rules. Those settings belong in the repository rather than in a contributor’s local editor.

A diagram illustrating a four-step GitHub Actions workflow to automatically update a markdown table of contents.

A safe workflow shape

Use a generated block with stable markers:

<!-- toc -->
- [Install](#install)
- [Configure](#configure)
<!-- tocstop -->

The job can run a Node-based generator such as markdown-toc or doctoc, then inspect git diff. On a feature branch, it can open a pull request with the regenerated block. On a protected main branch, it can run as a freshness check and reject stale output instead of modifying the branch unexpectedly.

A deterministic commit or pull request message, such as chore(toc): regenerate documentation navigation, makes automation easy to identify. The important behavior is scope. The generator should not rewrite prose, reorder unrelated content, or modify manually maintained anchors outside the TOC block.

What CI should validate

  • Heading selection: Confirm that only the intended H2 and H3 levels enter the list.
  • Anchor resolution: Render the target site and test every fragment against the generated IDs.
  • Duplicate handling: Fail on duplicate headings unless the renderer’s suffix behavior is explicitly supported.
  • Marker integrity: Check that every generated block has one opening and one closing marker.
  • Branch behavior: Use pull requests for automatic repairs and status checks for merge protection.

Teams integrating documentation checks into repository workflows can also review this guide to sync documentation with GitHub. A CI job won’t prevent every renderer difference, but it creates a repeatable point where drift becomes visible before publication.

Accessibility, SEO, and AI-Readability Checks

An automatically generated TOC can still be inaccessible, noisy, or difficult for search systems to interpret. Generation solves synchronization. It doesn’t decide whether the heading hierarchy makes sense, whether the links have meaningful labels, or whether the outline appears in the initial HTML.

Screen reader users often move through a document by headings, so the TOC must reinforce the document’s semantic structure rather than compensate for a damaged one. Use one clear H1, logical H2 and H3 progression, descriptive heading text, and link labels that match the destination heading closely. Avoid decorative headings that add noise without helping readers locate content.

Make navigation semantic

Wrap the list in a <nav> element with an accessible label such as On this page. Use a real ordered or unordered list. Don’t replace list semantics with nested <div> elements, and don’t turn every minor label into a TOC entry.

For SEO, prefer server-rendered or build-time navigation in the initial HTML. A client-side JavaScript widget may work in a browser while leaving crawlers, text extraction tools, and users with restricted scripts without the same structural outline. Each fragment should resolve to a stable heading ID, and the visible link text should accurately describe the destination.

A checklist infographic titled Accessibility, SEO, and AI-Readability Checks for auditing on-page navigation and web content structure.

Keep machine-authored documents readable

AI-generated Markdown often introduces repeated headings, skipped levels, and overly granular subsections. A filter that includes only H2 and H3 can make the result usable, but it shouldn’t hide a broken hierarchy. Review the source tree, remove duplicate labels, and keep TOC entries short enough to function as an outline.

Run Lighthouse and axe against a preview, then test keyboard focus and at least one screen reader path. Teams that need a broader accessibility testing workflow can use this ADA compliance testing software guide to compare audit approaches, but automated checks should complement human navigation testing, not replace it.

  • Structure: Verify one clear H1 and a logical heading sequence.
  • Targets: Confirm every TOC link reaches an exact heading ID.
  • Output: Inspect the initial HTML instead of relying only on client-side injection.
  • Navigation: Test keyboard focus, skip behavior, and screen reader announcements.
  • Retrieval: Keep the TOC near the introduction and avoid unnecessary nesting.

A TOC is useful when it helps a human decide where to go next. The same concise structure also gives search engines and language models a reliable outline of the document.

Keeping the TOC Honest with Auto-Sync Docs

CI catches stale output when a repository build runs. Auto-sync documentation extends that idea to the moment a source change arrives. The useful unit isn’t a nightly regeneration. It’s the same commit or review event that changes the heading.

The flow is straightforward. A developer edits ## Authentication to clarify the section, a webhook receives the push event, a service compares the old and new heading trees, and the generator rewrites only the marked TOC block. The documentation build then consumes the updated source, so the heading, anchor, and navigation change together.

A four-step diagram illustrating an automated process for syncing a table of contents with documentation updates.

Why event-driven sync wins

A cron-based generator can lag behind feature branches. During that gap, reviewers see one heading tree while the preview site may expose another. Event-driven synchronization keeps the correction close to the source change and gives reviewers a visible diff.

Atomic updates also make approval easier. The reviewer sees the renamed heading, its generated link, and the resulting documentation preview in one change set. That review model fits the broader discipline described in this guide to building a content approval system, where ownership, review state, and publication behavior remain explicit.

Use this repository checklist:

  • Define scope: Generate only H2 and H3 entries unless deeper levels serve a clear navigation purpose.
  • Choose anchors: Document the slug behavior of every renderer that publishes the file.
  • Mark ownership: Delimit the generated TOC so automation never rewrites surrounding prose.
  • Validate output: Render previews and test every fragment after heading edits.
  • Protect merges: Require a fresh TOC check before accepting documentation changes.
  • Review accessibility: Confirm semantic navigation, heading order, keyboard access, and screen reader behavior.
  • Sync on events: Trigger regeneration from the source commit or pull request, not a delayed schedule.

A markdown table of contents becomes dependable when it lives inside that loop. The generator, renderer, preview, and review process should all agree on the same heading tree.


GitDocAI turns a GitHub repository, OpenAPI specification, website crawl, or uploaded documentation files into a branded site that stays synchronized with repository changes. Connect your repo through GitDocAI to generate and review documentation updates, including navigation changes, through a commit-aware workflow instead of maintaining every TOC by hand.