rumdl 0.1.86

A fast Markdown linter written in Rust (Ru(st) MarkDown Linter)
Documentation
# MD051 - Link anchors should exist

Aliases: `link-fragments`

## What this rule does

Ensures that link anchors (the part after `#`) point to actual headings that exist. This includes both same-document anchors (like `#introduction`) and cross-file fragment links (like
`file.md#heading`) when linting multiple files together.

## Why this matters

- **Navigation**: Broken internal links frustrate readers trying to jump to sections
- **Maintenance**: Helps you catch links that break when headings are renamed
- **User experience**: Ensures smooth document navigation

## Examples

### ✅ Correct

```markdown
# Introduction

## Getting Started

See the [Introduction](#introduction) for background.

Jump to [Getting Started](#getting-started) to begin.

## Working with **bold** and *italic*

Link to [formatted heading](#working-with-bold-and-italic)

<!-- Cross-file fragment links are also validated -->
See [external documentation](README.md#setup) for setup instructions.
```

### ❌ Incorrect

<!-- rumdl-disable MD051 -->

```markdown
# Introduction

## Getting Started

[Jump to Installation](#installation) <!-- No "Installation" heading exists -->

[See Overview](#overview) <!-- No "Overview" heading exists -->
```

<!-- rumdl-enable MD051 -->

### 🔧 Fixed

This rule cannot automatically fix missing anchors - you need to either:

- Add the missing heading
- Update the link to point to an existing heading
- Remove the broken link

## Configuration

This rule supports configuring the anchor generation style to match different Markdown processors:

```toml
[MD051]
# Anchor generation style (default: "github")
# - "github": Preserves Unicode, underscores, and consecutive hyphens
# - "kramdown": ASCII-only with normalization, removes underscores
# - "kramdown-gfm" / "jekyll": Kramdown with GFM input (Jekyll/GitHub Pages)
# - "python-markdown" / "mkdocs": Python-Markdown style (collapses separators, ASCII-only)
anchor-style = "github"

# Match link fragments against headings case-insensitively.
# rumdl defaults to true (permissive). markdownlint defaults to false; set
# this to false for strict markdownlint parity.
ignore-case = true

# Optional regex applied to the fragment text (without the leading '#').
# Fragments that match are skipped — useful for runtime-generated anchors
# such as footnote IDs or build-time slugs.
# ignored-pattern = "^fn:"
```

### `ignore-case`

Controls whether link fragments are matched against headings case-insensitively.

- `true` (rumdl default): `[link](#MY-HEADING)` resolves to `# My Heading`.
- `false` (markdownlint default): the fragment must exactly match the generated
  anchor (e.g., `#my-heading`). Useful when targeting strict markdownlint parity.

### `ignored-pattern`

A regular expression matched against the fragment text (without the leading
`#`). Any fragment that matches is skipped — neither validated nor reported.
Use this for runtime-generated anchors that aren't visible in the source:

```toml
[MD051]
ignored-pattern = "^(fn|fnref):"  # Skip footnote anchors
```

An invalid regex is treated as if `ignored-pattern` were unset (no fragment is
silently ignored).

When using `--flavor mkdocs`, the anchor style automatically defaults to `python-markdown` (unless explicitly overridden). This matches MkDocs's use of Python-Markdown's `toc` extension for anchor
generation.

### Anchor style differences

| Heading             | GitHub               | Python-Markdown      | kramdown            |
| ------------------- | -------------------- | -------------------- | ------------------- |
| `Hello World`       | `#hello-world`       | `#hello-world`       | `#hello-world`      |
| `respect_gitignore` | `#respect_gitignore` | `#respect_gitignore` | `#respectgitignore` |
| `The End - yay`     | `#the-end---yay`     | `#the-end-yay`       | `#the-end---yay`    |
| `CI/CD Migration`   | `#cicd-migration`    | `#cicd-migration`    | `#cicd-migration`   |
| `Café au Lait`      | `#café-au-lait`      | `#cafe-au-lait`      | `#cafe-au-lait`     |
| `你好世界`          | `#你好世界`          | (empty / `#_1`)      | `#section`          |

**Key differences**:

- **GitHub**: Preserves Unicode, underscores, and consecutive hyphens (e.g., `---`)
- **Python-Markdown** (MkDocs): ASCII-only, collapses consecutive separators (e.g., `---``-`), preserves underscores
- **kramdown**: ASCII-only, removes underscores, preserves consecutive hyphens

## Automatic fixes

This rule does not provide automatic fixes since it cannot guess which heading you meant to link to.

## How heading anchors work

When you create a heading like `## Getting Started`, Markdown automatically creates an anchor `#getting-started` that you can link to. The conversion follows these rules:

1. Convert to lowercase: `Getting Started``getting started`
2. Replace spaces with hyphens: `getting started``getting-started`
3. Remove special characters: `FAQ's & Tips!``faqs-tips`
4. Strip formatting: `**Bold** Text``bold-text`

## Learn more

- [CommonMark anchors]https://spec.commonmark.org/ - How link anchors work
- [GitHub heading IDs][github-ids] - GitHub's approach to heading anchors

[github-ids]: https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#section-links

## Related rules

- [MD042]md042.md - No empty links
- [MD034]md034.md - URLs should be formatted as links
- [MD039]md039.md - No spaces inside link text