Skip to content

MD080 - Heading anchors must be unique

Aliases: heading-anchor-collision

Opt-in: disabled by default. Enable explicitly (e.g. add MD080 to your config's enabled rules) because the collision is functional under platform auto-suffixing and flagging it changes established lint output.

What this rule does

Flags two or more headings whose generated URL-safe anchor (slug) is identical. The anchor is computed with the configured anchor style; an explicit {#custom-id} takes precedence over the generated slug.

This is deliberately distinct from two neighbouring rules:

  • MD024 flags duplicate heading text. It misses distinct texts that slugify to the same anchor (Setup & Run vs Setup Run, C++ vs C).
  • MD051 flags broken fragment references. MD080 flags ambiguous fragment targets: the link resolves, but not unambiguously.

Why this matters

A [text](#slug) link, and the virtual-page identifier some viewers derive from an H1/H2 title, can only resolve to the first heading that produced a given slug. GitHub and MkDocs paper over the collision by auto-suffixing the later anchor (slug-1), which is functional but surprising: a hand-written #slug link that meant the second heading silently lands on the first.

Configuration

Option Type Default Description
anchor-style string github Slug algorithm: github, kramdown-gfm, kramdown, python-markdown. When unset, follows the active flavor.
levels array of int [1, 2, 3, 4, 5, 6] Heading levels whose anchors must be unique. Set to [1, 2] to check only page-identifier titles.
[MD080]
# Slug algorithm: "github", "kramdown-gfm", "kramdown", or "python-markdown".
anchor-style = "github"
# Heading levels whose anchors must be unique. Use [1, 2] for page ids only.
levels = [1, 2, 3, 4, 5, 6]

Examples

Correct

# Setup

## Configuration

## Usage

Incorrect

# Setup & Run

## Setup Run

Both headings slug to the same GitHub anchor, so #setup--run is ambiguous.

# Intro

## Intro

Same text at different levels still produces one shared #intro anchor.

Automatic fixes

None. Renaming a heading - and every link that targets it - is a semantic decision the linter must not make automatically.