rumdl 0.2.75

A fast Markdown linter and formatter written in Rust
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
# MD057 - Check that file links work

Aliases: `existing-relative-links`

## What this rule does

Verifies that relative links to other files in your documentation actually point to files that exist.
This includes both inline links and reference-style link definitions.

A link has to spell its target the way the filesystem stores it: `CHANGELOG.md` does not point at
`changelog.md`, even on macOS and Windows, where opening the file works anyway. Web servers and
Linux checkouts are case-sensitive, so the same link is broken everywhere else.

Wikilinks and wiki embeds (`[[page]]`, `![[image.png]]`) are not checked. They name a vault
entry rather than a path relative to the file that holds them, so the tool that renders them
resolves the name itself.

## Why this matters

- **Prevents frustration**: Broken links waste readers' time and damage trust
- **Maintains quality**: Working links show your documentation is well-maintained
- **Aids navigation**: Readers can confidently explore your documentation
- **Catches typos**: Common mistakes in file paths are caught early

## Examples

<!-- rumdl-disable MD057 MD031 MD040 -->

### ✅ Correct

```markdown
[Installation Guide](install.md)          <!-- File exists -->
[Contributing](../CONTRIBUTING.md)        <!-- File exists -->
[GitHub Repo](https://github.com/org/repo) <!-- External URL -->
[Email Us](mailto:help@example.com)       <!-- Email link -->
[Jump to Section](#configuration)         <!-- Same-file anchor -->

<!-- Reference-style links -->
[readme]: ./README.md                     <!-- File exists -->
[external]: https://example.com           <!-- External URL -->
```

### ❌ Incorrect

```markdown
[Missing Doc](does-not-exist.md)           <!-- File doesn't exist -->
[Bad Path](../missing/guide.md)            <!-- Path doesn't exist -->
[Typo in Name](READNE.md)                 <!-- Should be README.md -->
[Wrong Extension](setup.markdown)          <!-- File is setup.md -->
[Wrong Case](CHANGELOG.md)                 <!-- File is changelog.md -->

<!-- Reference-style links with missing targets -->
[missing]: ./does-not-exist.md            <!-- File doesn't exist -->
[bad-path]: ../missing/doc.md             <!-- Path doesn't exist -->
```

### 🔧 Fixed

This rule cannot automatically fix broken links because it can't know which file you intended to link to. You must manually:

1. Correct the file path
2. Create the missing file
3. Or remove the broken link

<!-- rumdl-enable MD057 MD031 MD040 -->

## Configuration

### `absolute-links`

Controls how absolute links (paths starting with `/`) are handled.

| Value | Behavior |
|-------|----------|
| `ignore` (default) | Skip validation for absolute links |
| `warn` | Report a warning for absolute links |
| `relative_to_docs` | Resolve absolute links relative to MkDocs `docs_dir` and validate |
| `relative_to_roots` | Resolve absolute links relative to one or more configured root directories |

Absolute links like `/api/docs` or `/blog/post.html` are typically routes for published
documentation sites, not filesystem paths. By default, MD057 ignores these because they
can't be validated locally.

If you want to be notified about absolute links in your documentation (perhaps to convert
them to relative links), set `absolute-links = "warn"`.

```toml
# .rumdl.toml
[MD057]
absolute-links = "warn"
```

For MkDocs projects, use `relative_to_docs` to validate absolute links by resolving them
relative to the `docs_dir` configured in `mkdocs.yml`. This finds the `mkdocs.yml` by
walking up from the file being checked, reads the `docs_dir` setting (default: `docs`),
and checks that the linked file exists there.

```toml
# .rumdl.toml
[global]
flavor = "mkdocs"

[MD057]
absolute-links = "relative_to_docs"
```

With this configuration, a link like `[Guide](/getting-started/)` will be validated by
checking if `docs/getting-started/index.md` exists. Extensionless links like
`/getting-started` will also try markdown extensions (e.g., `docs/getting-started.md`).

If no `mkdocs.yml` is found, the behavior falls back to `warn`.

For Hugo, VitePress, or any multi-root content layout, use `relative_to_roots` together
with the `roots` list to specify which directories serve as the site root(s).

```toml
# .rumdl.toml
[MD057]
absolute-links = "relative_to_roots"
roots = ["content/en", "content/zh-cn"]
```

With this configuration, a link like `[Guide](/docs/guide.md)` passes when
`content/en/docs/guide.md` **or** `content/zh-cn/docs/guide.md` exists. A warning is
emitted only when none of the configured roots — or the project root (see below) —
contain the target.

**Project-root fallback.** After every configured root has been tried, the absolute
link is also resolved against the project root. This supports two common link styles
in the same project without extra configuration:

- `[guide](/docs/guide.md)` — relative to a configured root (e.g. resolves under
  `content/en/docs/guide.md`).
- `[guide](/content/en/docs/guide.md)` — literal path from the project root.

Both pass as long as the target file exists somewhere on the configured-or-implicit
search path. A warning is emitted only when no resolution finds the file.

The project root is discovered by walking up from rumdl's invocation directory looking
for the first directory that contains `.git`, `.rumdl.toml`, `pyproject.toml`, or
`.markdownlint.json`. This means absolute links resolve consistently whether you run
rumdl from the project root or from any subdirectory.

**Directory links.** `relative_to_roots` resolves against the filesystem, so a link to
a directory that exists passes however it is written: `/adir`, `/adir/` and
`/adir/#section` all name the same directory, and none of them requires an `index.md`.
That matches what relative links already do. The `index.md` requirement belongs to
`relative_to_docs`, where a URL is a route MkDocs has to serve.

URL-encoded paths (e.g., `/foo%20bar.md`) are decoded before the filesystem check.
Fragment suffixes (e.g., `/page.md#section`) are stripped. Roots may be absolute paths
or paths relative to the project root; trailing slashes are normalized.

When `roots` is empty, validation falls through to the project-root resolution alone —
useful for single-root projects where every absolute link is meant to resolve from the
project root directly.

### `compact-paths`

When enabled, warns about relative links that contain unnecessary path traversal.
Disabled by default.

| Value | Behavior |
|-------|----------|
| `false` (default) | No compact-paths warnings |
| `true` | Warn when a shorter equivalent path exists |

For example, in `doc/sub_dir/file2.md`, the link `[text](../sub_dir/file1.md)` goes up
to `doc/` then back into `sub_dir/` — the same directory the file is already in. The
shorter equivalent is just `file1.md`.

```toml
# .rumdl.toml
[MD057]
compact-paths = true
```

When enabled, this rule also provides auto-fix support: running `rumdl check --fix` will
replace unnecessarily long paths with their shorter equivalents.

| Original | Simplified |
|----------|-----------|
| `../sub_dir/file.md` (from `sub_dir/`) | `file.md` |
| `./file.md` | `file.md` |
| `./sub/../file.md` | `file.md` |
| `../../a/sub/file.md` (from `a/sub/`) | `file.md` |

Paths that are already optimal are not flagged:

| Link | Why it's already optimal |
|------|------------------------|
| `file.md` | No traversal |
| `../sibling/file.md` (from `other/`) | Cannot be shortened |
| `../../file.md` (from `a/b/`) | Necessary parent traversal |

Fragment (`#section`) and query (`?v=1`) suffixes are preserved in the suggested fix.

### `self-referential-links`

When enabled, warns about a relative link that points at the file it is written in.
Disabled by default.

| Value | Behavior |
|-------|----------|
| `false` (default) | No self-referential-link warnings |
| `true` | Warn when a link resolves to the file holding it |

```toml
# .rumdl.toml
[MD057]
self-referential-links = true
```

Following such a link reloads the page the reader is already on. In `test.md`:

| Link | Reported as |
|------|-------------|
| `[text](test.md#level-2-heading)` | Can be simplified to `#level-2-heading` |
| `[text](test.md)` | Points to the file it is in |
| `[text](../docs/test.md)` (from `docs/`) | Points to the file it is in |
| `[text](#level-2-heading)` | Not reported, this is the form to use |
| `[text](other.md)` | Not reported |

The fragment form is auto-fixed: `test.md#level-2-heading` becomes
`#level-2-heading`, which reaches the same heading without leaving the page. A
link to the whole file has no equivalent shorter form, so it is reported without
a fix and is left for you to reword or remove.

A link is matched against the file under check the same way link targets are
resolved elsewhere in this rule, so `test`, `./test.md` and `../docs/test.md`
from `docs/test.md` are all recognized. When `compact-paths` is also on, a
self-referential link is reported only once, as self-referential: a shorter path
to the same file would still be a link the reader should not follow.

Images are not checked. `![alt](test.md)` is not a link a reader follows, and
detaching the fragment from an image source would not address the same content.

### `search-paths`

Additional directories to search when a relative link target is not found relative to the
file's directory. Paths are resolved relative to the discovered project root (the first
parent directory containing `.git`, `.rumdl.toml`, `pyproject.toml`, or
`.markdownlint.json`), so they work consistently from any subdirectory.

```toml
# .rumdl.toml
[MD057]
search-paths = ["assets", "images", "attachments"]
```

With this configuration, a link like `![photo](photo.png)` will first be checked relative
to the markdown file's directory. If not found there, MD057 will also look in `assets/photo.png`,
`images/photo.png`, and `attachments/photo.png`.

**Obsidian users:** When `flavor = "obsidian"` is set in the global config, the attachment
folder is auto-detected from `.obsidian/app.json`, so this option is typically not needed.
Use it for custom setups or non-Obsidian tools with similar asset directory conventions.

```toml
# .rumdl.toml — Obsidian auto-detection (no search-paths needed)
[global]
flavor = "obsidian"
```

Obsidian supports 4 attachment location modes configured via `attachmentFolderPath` in
`.obsidian/app.json`:

| Setting | `attachmentFolderPath` | Resolution |
|---------|----------------------|------------|
| Vault folder (root) | `""` (empty) | `<vault-root>/` |
| Specified folder | `"Attachments"` | `<vault-root>/Attachments/` |
| Same folder as file | `"./"` | Same directory as the markdown file |
| Subfolder under file | `"./assets"` | `<file-dir>/assets/` |

### `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 path-shaped frontmatter value must point at an existing file |

```toml
# .rumdl.toml
[MD057]
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.
Markdown-looking strings inside frontmatter, such as notification templates
containing `[workflow]({run_url})`, are data and are never validated or indexed
as body links. This applies in every flavor.

Under the `gh-aw` flavor, a complete output placeholder used as a link
destination, such as `{run_url}`, is also template data rather than a path.
MD057 skips it wherever an Agentic Workflow constructs output Markdown, while
continuing to validate ordinary relative links in the workflow body.
Enable it for projects whose frontmatter paths really are relative to the
document, and list the keys that are not under `ignore-frontmatter-fields`.

A value is read as a destination when it has no whitespace and either names a
markdown file, or contains a `/` together with a leading `/`, `./`, `../`, `~/`
or a file extension. In `docs/page.md`:

| Value | Reported as |
|-------|-------------|
| `template: ./missing.md` | `Relative link './missing.md' does not exist` |
| `image: ../img/logo.png` | Reported when `docs/img/logo.png` is missing |
| `template: ./guide.md` | Not reported when `docs/guide.md` exists |
| `canonical: https://example.com/a.md` | Not reported, an external URL |
| `anchor: '#section'` | Not reported, a fragment is checked by MD051 |
| `title: Node.js` | Not reported, prose is not a destination |
| `tags: ci/cd` | Not reported, no leading `./` and no extension |

Only existence is checked. The `compact-paths` and `self-referential-links`
suggestions stay out of frontmatter: both rewrite a destination, and a
frontmatter value is only ever a guess at being one. Absolute values follow
`absolute-links` exactly as body links do.

### `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
[MD057]
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.

### Build-Generated Files

Documentation sites often compile markdown files to HTML during build. MD057 automatically
checks if a corresponding markdown source file exists when a link points to a `.html` or
`.htm` file.

For example, `[Guide](guide.html)` will pass if `guide.md` exists, even though `guide.html`
doesn't exist in your source tree.

### Handling Complex Generator Patterns

For documentation generators that place source files in different locations (e.g., mdBook's
`src/` directory), MD057 checks for markdown sources in the same directory as the HTML file.
If your generator uses a different structure, you can disable MD057 for affected directories
using `per-file-ignores`:

```toml
[per-file-ignores]

## mdBook projects - HTML links in book/ point to book/src/*.md sources

"book/**/*.md" = ["MD057"]

## Jekyll projects - HTML links in _posts/ point to generated files

"_posts/**/*.md" = ["MD057"]
"_docs/**/*.md" = ["MD057"]

## Hugo projects - HTML links in content/ point to generated files

"content/**/*.md" = ["MD057"]
```

MD057 will still check for markdown sources in the same directory automatically.
Use `per-file-ignores` only when sources are in different locations.

## Special cases

Paths inside a template shortcode tag (`{{< figure src="diagram.png" >}}`,
`{{% note [t](page.md) %}}`) are not checked. A shortcode's arguments are
resolved by the site generator's own rules - Hugo looks a figure's `src` up in
page resources and static assets, not relative to the file holding the tag - so
rumdl cannot say whether the target exists. Links in the body between a paired
opening and closing shortcode are checked as usual.

## Automatic fixes

Broken links cannot be automatically fixed because the rule cannot determine which file
you intended to link to. They must be corrected manually.

When `compact-paths = true`, unnecessary path traversal can be auto-fixed with
`rumdl check --fix`. The fix replaces long paths with their shorter equivalents while
preserving any fragment or query suffix.

When `self-referential-links = true`, a link into the current file that carries a
fragment is auto-fixed to the fragment alone. A link to the whole file is reported
without a fix, since removing it would change what the document says.

## Learn more

- [Markdown Guide: Links]https://www.markdownguide.org/basic-syntax/#links
- [Writing good documentation]https://www.writethedocs.org/guide/writing/beginners-guide-to-docs/

## Related rules

- [MD051 - Fix broken link fragments]md051.md
- [MD042 - Ensure links have content]md042.md
- [MD034 - Format bare URLs properly]md034.md