# MD073 - Table of Contents Validation
## Overview
**Rule ID:** MD073
**Aliases:** `toc-validation`
**Category:** Other
**Fixable:** Yes (marker-based TOCs only)
This rule validates that Table of Contents (TOC) sections match the actual document headings. It detects missing entries, stale entries, and text mismatches.
## Configuration
```toml
[MD073]
# Enable the rule (opt-in, disabled by default)
enabled = true
# Minimum heading level to include (default: 2)
min-level = 2
# Maximum heading level to include (default: 4)
max-level = 4
# Whether TOC order must match document order (default: true)
enforce-order = true
# Spaces per indentation level (default: from MD007, or 2)
# indent = 4
```
### Configuration Options
| `enabled` | boolean | `false` | Whether to enable this rule (opt-in) |
| `min-level` | integer | `2` | Minimum heading level to include in TOC |
| `max-level` | integer | `4` | Maximum heading level to include in TOC |
| `enforce-order` | boolean | `true` | Whether TOC entry order must match heading order |
| `indent` | integer | MD007's indent, or `2` | Spaces per indentation level for nested TOC entries |
### Indentation Configuration
By default, MD073 reads the `indent` value from [MD007](md007.md) to ensure TOC indentation matches your list indentation style. This means if you configure:
```toml
[MD007]
indent = 4
```
Then MD073 will automatically use 4-space indentation for nested TOC entries:
```markdown
- [Installation](#installation)
- [npm](#npm)
- [yarn](#yarn)
- [Usage](#usage)
```
You can override this by setting `indent` explicitly in MD073:
```toml
[MD073]
enabled = true
indent = 2 # Use 2-space indent regardless of MD007 config
```
## TOC Detection Methods
### Marker-Based Detection (Recommended)
Use HTML comments to mark the TOC region explicitly:
```markdown
- [Installation](#installation)
- [Usage](#usage)
```
Alternative stop marker: `<!-- /toc -->`
Marker detection is case-insensitive and allows whitespace variations:
- `<!-- toc -->`
- `<!--toc-->`
- `<!-- TOC -->`
**Auto-fix only works for marker-based TOCs** because markers provide clear, unambiguous boundaries.
### Heading-Based Detection
Detects TOC by looking for headings like "Table of Contents", "Contents", or "TOC":
```markdown
## Table of Contents
- [Installation](#installation)
- [Usage](#usage)
## Installation
...
```
The TOC region ends at the next heading or after two consecutive blank lines.
**Note:** Auto-fix is disabled for heading-based TOCs because the boundaries are fuzzy and could lead to unintended changes.
## What This Rule Checks
### Missing Entries
Headings that exist in the document but don't have corresponding TOC entries:
```markdown
- [Installation](#installation)
## Installation
Content.
## Usage <-- Missing from TOC!
More content.
```
### Stale Entries
TOC entries for headings that no longer exist:
```markdown
- [Installation](#installation)
- [Deleted Section](#deleted-section) <-- Heading doesn't exist!
## Installation
Content.
```
### Text Mismatches
TOC entry text differs from the actual heading:
```markdown
- [Install](#installation) <-- Should be "Installation"
## Installation
Content.
```
### Order Mismatches (when enforce-order = true)
TOC entries in wrong order compared to document headings:
```markdown
- [Usage](#usage) <-- Should come after Installation
- [Installation](#installation)
## Installation
...
## Usage
...
```
## Auto-Fix Behavior
When auto-fix is enabled, this rule:
1. **Only fixes marker-based TOCs** - heading-based TOCs are not auto-fixed
2. Generates a new TOC from all headings after the TOC region
3. Respects `min-level` and `max-level` filters
4. Uses GitHub-style anchors for link generation
5. Uses nested indentation based on heading level
6. Preserves markers (`<!-- toc -->...<!-- tocstop -->`)
The fix is idempotent - running it multiple times produces the same output.
## Examples
### Correct TOC with Markers
```markdown
# My Project
- [Installation](#installation)
- [npm](#npm)
- [yarn](#yarn)
- [Usage](#usage)
## Installation
### npm
npm install my-project
### yarn
yarn add my-project
## Usage
import MyProject from 'my-project';
```
### Correct TOC with Heading
```markdown
# My Project
## Contents
- [Installation](#installation)
- [Usage](#usage)
## Installation
...
## Usage
...
```
## Rationale
Table of Contents sections are valuable for navigation in documentation, but they easily drift out of sync with actual headings when documents are refactored. This rule:
- Catches missing TOC entries when new sections are added
- Identifies stale entries when sections are removed or renamed
- Ensures TOC accurately reflects document structure
- Provides auto-fix to regenerate correct TOC
## Special Cases
### Headings in Code Blocks
Headings inside fenced code blocks are ignored:
```markdown
## Real Heading <-- Included in TOC
\`\`\`markdown
## Example Heading <-- Ignored (in code block)
\`\`\`
```
### Headings with Code Spans
Headings containing code spans work correctly:
```markdown
#### \`check [PATHS...]\`
```
Generates anchor: `#check-paths`
### Duplicate Headings
Duplicate headings get numbered anchors:
```markdown
## FAQ <-- anchor: #faq
## FAQ <-- anchor: #faq-1
## FAQ <-- anchor: #faq-2
```
### Custom Anchors
Custom IDs using `{#custom-id}` syntax are respected:
```markdown
## My Section {#my-custom-anchor}
```
Generates anchor: `#my-custom-anchor` instead of `#my-section`
An empty HTML anchor element beside the heading adds a target instead of replacing one. Renderers still generate the slug from the heading text, so both fragments reach the heading and a TOC entry may use either:
```markdown
## <a name="cheatsheets"></a>Cheat Sheets
```
Valid targets: `#cheatsheets` and `#cheat-sheets`
The `id` attribute is preferred over the legacy `name` attribute, and a generated TOC entry uses the explicit anchor. An entry that already reaches its heading keeps the fragment it was written with when the TOC is regenerated. The generated slug still counts towards the numbering of later duplicate headings, so a second `## Cheat Sheets` gets `#cheat-sheets-1`. A heading that carries both a `{#custom-id}` and an anchor element is reached through either of them.
An anchor value that cannot sit in a link destination, such as one containing a space or a parenthesis, is percent-encoded in the generated entry (`<a id="v(2)">` links as `#v%282%29`), and an entry written that way reaches the heading.
Anchor markup inside inline code or an HTML comment, or written with a backslash-escaped `\<`, is heading text and defines no target, and so is an `<a>` written inside another tag's attribute value. Only the element itself leaves the heading text, so `## Foo<a id="alias"></a> Bar` reads `Foo Bar` and an entry written that way is up to date.
### Markers That Render as Text
Marker text inside inline code is documentation, not a marker. A line such as `` Use `<!-- toc -->` to place the table `` neither opens nor closes the TOC region, and neither does a marker written with a backslash-escaped `\<`, which renders as literal text.
## See Also
- [MD051](md051.md) - Link fragments should be valid
- [MD041](md041.md) - First line in file should be a top-level heading