# 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.

### `check-frontmatter`

When enabled, path-shaped values in the document's frontmatter are checked the
same way body links are. Disabled by default.

| Value | Behavior |
|-------|----------|
| `false` (default) | Frontmatter values are not checked |
| `true` | A fragment in a frontmatter value must name an existing anchor |

```toml
# .rumdl.toml
[MD051]
check-frontmatter = true
```

It is off by default because frontmatter has no syntax marking a value as a
link: a path-shaped value is only a guess at one. Static site generators also
resolve frontmatter paths from the site root rather than the document's own
directory, so checking them like body links reports working paths as broken.
Enable it for projects whose frontmatter links really are relative to the
document, and list the keys that are not under `ignore-frontmatter-fields`.

A fragment-only value is resolved against this document's own headings. A value
carrying a path resolves against the target file, exactly like a cross-file body
link:

```yaml
---
anchor: '#setup'          # must match a heading in this document
guide: ./install.md#setup # must match a heading in install.md
canonical: https://example.com/a.md#setup  # external, not checked
title: Node.js            # prose, not read as a link
---
```

`ignore-case` and `ignored-pattern` apply to frontmatter fragments exactly as
they do to body links.

### `ignore-frontmatter-fields`

Top-level frontmatter keys whose values are never checked. Matched
case-insensitively, and a parent key excludes its whole subtree. Applies only
when `check-frontmatter` is enabled.

```toml
# .rumdl.toml
[MD051]
check-frontmatter = true
ignore-frontmatter-fields = ["image", "cover"]
```

Use it for the keys a generator resolves from somewhere other than the
document's directory, such as theme assets or site-root media paths.

### 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
