Skip to content

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.

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

✅ Correct

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

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

❌ Incorrect

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

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

Configuration

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

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

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

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

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.

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

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

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

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

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

[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