Skip to content

Configuration recipes

Scenario-based setup for mkdocs-source-links: MkDocs prerequisites, link conventions, plugin options, worked mkdocs.yml examples, and fixed behavior. For the compact options table see Configuration; for forge autodetection see Forges; for caveats see Limitations.

Prerequisites

The plugin rewrites [text](../path) inline links and [ref]: ../path reference definitions during mkdocs build using three layers of MkDocs configuration plus its own options.

MkDocs settings

Setting Role
repo_url Required for rewriting. Without it, all ../ links are left unchanged.
edit_uri When plugin branch and extra.git_branch are unset, the branch name is parsed from edit/ or blob/ in this path (GitLab -/edit/<branch>/… is supported).
extra.git_branch Overrides edit_uri for branch resolution when plugin branch is unset.

repo_url also supplies the forge host for URL building. Autodetection uses hostname labels such as github.com or github.example.com. When the host cannot be identified, set forge explicitly — see Self-hosted GitLab on a neutral hostname and Forges.

Branch resolution order

When plugin branch is not set explicitly, the branch name embedded in forge URLs is resolved in this order:

  1. Plugin branch: config
  2. extra.git_branch in mkdocs.yml
  3. Parsed from edit_uri (edit/<branch>/..., blob/<branch>/..., or Bitbucket src/<branch>/...) — only the first path segment after edit/, blob/, or src/ is used, so branch names containing / (for example feature/my-branch) are not parsed correctly; set branch: or extra.git_branch explicitly instead
  4. Fallback: main

If your default branch is not main, set edit_uri, extra.git_branch, or source-links.branch so links point at the right ref.

pin controls whether that resolved name (or an override) is used in URLs, or whether HEAD is resolved to a commit SHA or exact tag — see pin and ref modes.

Repository root for path resolution

The plugin resolves ../ links relative to each page file on disk, but only within the directory that contains mkdocs.yml (MkDocs config_file_path parent). That directory is the plugin repository root — it is not always the git repository root.

  • mkdocs.yml at the monorepo root with docs_dir: packages/foo/docs → root is the monorepo.
  • mkdocs.yml inside packages/foo/ → root is packages/foo/; paths outside that directory are not rewritten.

The shell working directory for mkdocs build does not change this; only the config file path matters. Use mkdocs build -f path/to/mkdocs.yml when the config is not in the current directory.

Supported targets

Target Source markdown Built HTML
Page in docs/ [runbook](other.md) unchanged (MkDocs handles it)
Repo file outside docs/ [config](../backend/config.py) forge blob URL
Repo directory [scripts](../scripts/) or [scripts](../scripts) forge tree URL
Reference definition [cfg]: ../backend/config.py [cfg]: forge blob URL

Do

Pattern Example Notes
Inline parent-directory link [config](../backend/config.py) File → forge blob URL
Inline directory link [scripts](../scripts/) Directory → forge tree URL
Reference definition [cfg]: ../backend/config.py Usages [text][cfg] pick up the forge URL
Titled link [x](../path "title") Title preserved
Angle-bracket destination [x](<../path with spaces>) Spaces percent-encoded in output
Line fragment [x](../src/foo.py#L10-L20) Translated per forge — see Limitations
In-docs/ page link [other](other.md) Unchanged; MkDocs resolves it

Don't (left unchanged)

Pattern Example Why
No ../ prefix [x](src/foo.py), [x](./foo.py) Only complete inline and reference links with ../ targets match
HTML link <a href="../foo"> Markdown-only rewrite
Absolute / hard-coded blob URL https://github.com/.../blob/old/src/foo.py Pass through — use for one-off historical refs
Missing on-disk target [x](../not-there.py) Not rewritten; warning by default
Outside plugin root [x](../../outside) when root is a subdirectory Path outside mkdocs.yml directory
Virtual page Generated page with no source file No abs_src_path to resolve from

Shared root files (GitHub, IDE, and built site)

Documentation often lives in docs/ while governance files (README.md, CONTRIBUTING.md, ROADMAP.md) sit at the repository root. Relative ../ links work on GitHub and in the IDE; on the built site you can include root markdown with pymdownx snippets instead of duplicating content:

exclude_docs: snippets/*

markdown_extensions:
  - pymdownx.snippets:
      base_path: ["."]
      check_paths: true

In a doc page:

# Contributing

Thanks for your interest in improving mkdocs-source-links!

This project follows the [Code of conduct](https://filipchristiansen.github.io/mkdocs-source-links/code-of-conduct/). By
participating, you agree to uphold it.

## Development setup

This project uses [uv](https://docs.astral.sh/uv), [pre-commit](https://pre-commit.com), and a `Makefile`.

```bash
make install   # install Python 3.10, sync all groups, set up pre-commit hooks

When editing .github/scripts/: install ShellCheck locally — brew install shellcheck (macOS) or sudo apt install shellcheck (Linux). CI installs it automatically.

Dependencies

Runtime dependencies are declared in pyproject.toml under [project]. Development and documentation dependencies live in [dependency-groups] (dev, docs).

Dependencies are resolved and pinned in uv.lock using uv. Install them with make sync or uv sync --all-groups. CI and publish workflows use uv sync --frozen so builds always match the lockfile.

Selection: Prefer well-maintained packages with permissive licenses. Runtime dependencies are kept minimal (currently MkDocs only). Dev tools are chosen for ecosystem fit (ruff, mypy, pytest, etc.).

Updates: Dependabot (.github/dependabot.yml) opens one weekly grouped PR per ecosystem — uv lockfile (versioning-strategy: lockfile-only; constraints in pyproject.toml are unchanged) and GitHub Actions. Maintainers review and merge after CI passes.

Workflow

  1. Create a branch with a conventional prefix (feat/, fix/, chore/, docs/, refactor/, test/).
  2. Make your change and add tests. The suite must stay at 100% coverage.
  3. Run the full pre-PR suite locally:
make ci      # lint, audit, tests with coverage, docs

Individual targets are also available:

make lint    # pre-commit hooks (ruff, mypy, pydoclint, pylint, markdownlint, ...)
make audit   # pip-audit dependency vulnerability scan
make test    # pytest with coverage
  1. Open a pull request targeting main with a conventional title (for example fix: ...). CI runs the same lint and test suite across Python 3.10-3.13. The Docs workflow builds with Python 3.12 (a representative version from that matrix).

  2. Include Signed-off-by: Your Name <email@example.com> in each commit message (see Developer Certificate of Origin). Use git commit -s to add this automatically. The DCO workflow enforces a matching sign-off on every PR commit; fix existing commits with git rebase --signoff main.

Coding standards

  • Code is linted and formatted with ruff, type-checked with mypy, and checked with pylint and pydoclint.
  • Public functions use NumPy-style docstrings.
  • These are all enforced by pre-commit and CI; running make lint before pushing avoids surprises.

Static analysis

Before merge, CI runs pre-commit hooks including ruff (with bandit security rules), mypy, pylint, and pydoclint. See Makefile, .pre-commit-config.yaml, and .github/workflows/ci.yml in the repository.

Releases

Releases are cut by maintainers (see Maintainers). First, curate the ## [Unreleased] section of CHANGELOG.md by hand (the tooling never generates release notes). Then:

  1. make release-prep VERSION=X.Y.Z — bumps version in pyproject.toml, rolls [Unreleased] into a dated ## [X.Y.Z] section with updated compare links, runs make ci, and opens the release PR.
  2. After the PR is merged, make release-tag VERSION=X.Y.Z — creates a signed annotated tag vX.Y.Z and pushes it. The publish workflow verifies the tag signature, builds and uploads to PyPI, and creates the GitHub release with SLSA provenance (.intoto.jsonl) and distribution archives in a single gh release create (required for immutable releases — do not enable SLSA upload-assets, which pre-creates an empty release and breaks asset upload). Release notes on GitHub are taken from the matching CHANGELOG.md section with a compare link footer; a post-release job verifies provenance with slsa-verifier.

Signed tags and commits

main requires signed commits; release tags are created with git tag -s. The publish workflow rejects unsigned or unverified tags before PyPI upload. Configure commit/tag signing locally (GPG or SSH) and add the public key to your GitHub account so tags and merges show as Verified. See GitHub’s commit signature verification guide.

Root files stay editable in one place; the built page renders the included content. Snippet setup is
MkDocs configuration, not a plugin option. See [Contributing](contributing.md) for this project's
workflow.

## Plugin options reference

| Option | Type | Default | When to use |
| ------ | ---- | ------- | ----------- |
| `enabled` | bool | `true` | Turn rewriting off for local preview, or gate with an environment variable. |
| `pin` | string | `branch` | `branch` — normal docs; `commit` — permalink to `HEAD` SHA; `tag` — exact tag at `HEAD` (release builds). |
| `branch` | string | resolved | Override branch resolution. To pin URLs to a tag name without a tag checkout, see [Hardcoded tag without tag checkout](#hardcoded-tag-without-tag-checkout) — not safe on all forges. |
| `forge` | string | autodetected | Host without a forge hostname label (e.g. `git.mycompany.com` running GitLab). |
| `warn_on_missing` | bool | `true` | `false` to silence missing-target warnings; `true` with `mkdocs build --strict` for CI. |
| `log_rewrites` | `false` \| `summary` \| `verbose` | `false` | Opt-in rewrite statistics at INFO level; `summary` for one build line, `verbose` for per-page counts. |

Valid `forge` values: `github`, `gitlab`, `bitbucket`, `gitea`, `azure`. See [Forges](forges.md) for the
support matrix, autodetection limits, and **unsupported** hosts (Bitbucket Server, SourceHut, etc.).

### `enabled`

Skip rewriting without editing config on each serve:

```yaml
plugins:
  - source-links:
      enabled: !ENV [SOURCE_LINKS, true]
SOURCE_LINKS=false mkdocs serve

When enabled: false, markdown is untouched and no git lookup runs.

pin

plugins:
  - source-links:
      pin: branch   # default — resolved branch name in URLs
      # pin: commit # HEAD SHA (permalink)
      # pin: tag    # exact tag at HEAD (release CI)

branch

plugins:
  - source-links:
      branch: develop

Override the resolved branch name (for example develop instead of main). See Hardcoded tag without tag checkout when you need a tag name without checking out that tag.

forge

plugins:
  - source-links:
      forge: gitlab

Only when autodetection cannot identify the host. Do not set forge: bitbucket on Bitbucket Server/Data Center — see Forges.

warn_on_missing

plugins:
  - source-links:
      warn_on_missing: false

log_rewrites

Verify the plugin is rewriting links after first setup or when auditing ../ conventions:

plugins:
  - source-links:
      log_rewrites: summary   # one line at end of build
      # log_rewrites: verbose # per-page lines plus summary

Example build output with summary:

INFO    -  mkdocs_source_links: Rewrote 2 links across 1 page

With verbose, pages with rewrites log first (guide.md: rewrote 3 links), then the summary. Paths are relative to the docs directory (same as warn_on_missing warnings), without a docs/ prefix. Requires repo_url; leave at false (default) for quiet CI builds. Note that mkdocs build -q suppresses INFO output, so rewrite statistics are hidden even when log_rewrites is enabled.

pin and ref modes

Every rewritten link uses the same git ref for the whole build. There is no per-link override; for a single historical link use a full forge blob URL in markdown (Limitations).

pin Forge URL ref git required? Fallback
branch Resolved branch (or branch: override) No
commit git rev-parse HEAD Yes Resolved branch
tag git describe --tags --exact-match Yes Resolved branch

When pin: commit/tag falls back to a branch

commit and tag resolution shells out to git from the mkdocs.yml directory and falls back to the resolved branch with a build warning in these cases:

  • The mkdocs.yml directory is not inside a git worktree (for example a release archive or a shallow export with no .git).
  • git is not installed or not on PATH.
  • For pin: tag only, HEAD is not exactly tagged (git describe --tags --exact-match fails) — for example mid-development commits between releases.

A mkdocs.yml in a subdirectory of a git worktree is not a failure case: git discovers the repository by walking up from that directory, so nested configs (see Nested mkdocs.yml) still resolve the commit SHA or tag. The plugin repository root for ../ path resolution is a separate concern from where git metadata is read.

pin: branch

Default. Forge URLs use the resolved branch name (main, develop, etc.). Works in environments without git.

pin: commit

Embeds the current HEAD commit SHA in blob/tree URLs (permalink). If git is unavailable or fails, falls back to the resolved branch.

Link targets are still validated against the working tree at build time only — the plugin does not check whether a path existed at the pinned commit. Build from the checkout you want reflected.

pin: tag

For release documentation builds: check out the release tag in CI, then:

plugins:
  - source-links:
      pin: tag

If HEAD is exactly tagged (e.g. v1.2.3), URLs use that tag name. If HEAD is not exactly tagged, falls back to the resolved branch.

As with pin: commit, only the URL ref comes from git — missing-target checks still use the working tree at build time, not historical tag snapshots.

Hardcoded tag without tag checkout

When you cannot check out the tag but need tag-shaped URLs on GitHub, GitLab, or Bitbucket Cloud (for forges where branch and tag share the same path pattern), use pin: branch with an explicit override:

plugins:
  - source-links:
      pin: branch
      branch: v1.2.3

This always emits a branch ref in URLs. It is not forge-neutral: on Gitea/Forgejo/Codeberg and Azure DevOps it produces /src/branch/v1.2.3/… or version=GBv1.2.3 instead of the correct tag URL shape. On those forges, check out the release tag and use pin: tag (below), or paste a full forge blob URL for one-off historical links. See Limitations — Hardcoded tag names.

Example CI pattern (release from tag)

# .github/workflows/docs.yml (illustrative — adapt checkout and Python setup to your CI)
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.ref }}  # tag ref for release workflow
      - uses: astral-sh/setup-uv@v7
      - run: uv sync
      - run: uv run mkdocs build --strict

With pin: tag in mkdocs.yml, published links point at vX.Y.Z on the forge.

Common recipes

Each recipe assumes a layout like the integration test fixtures: docs/, backend/config.py, scripts/, and mkdocs.yml at the plugin repository root unless noted.

Default GitHub / main

site_name: My project
repo_url: https://github.com/you/your-repo
edit_uri: edit/main/docs/

plugins:
  - source-links

Before (source markdown in docs/index.md):

[config](../backend/config.py)

After (built HTML href):

https://github.com/you/your-repo/blob/main/backend/config.py

develop branch

Option A — derive branch from edit_uri:

edit_uri: edit/develop/docs/

Option B — explicit plugin override (wins over edit_uri):

plugins:
  - source-links:
      branch: develop

Local serve without rewriting

plugins:
  - source-links:
      enabled: !ENV [SOURCE_LINKS, true]
SOURCE_LINKS=false mkdocs serve

Built HTML during serve still rewrites by default; use enabled: false or the env var when you want ../ links untouched.

Release from tag

Check out the release tag, then:

plugins:
  - source-links:
      pin: tag

Default warn_on_missing: true plus:

mkdocs build --strict

Warnings for missing ../ targets fail the build.

plugins:
  - source-links:
      warn_on_missing: false

Use when some ../ links intentionally point at paths not present in every checkout.

Self-hosted GitLab on a neutral hostname

Autodetection matches forge names as hostname labels (gitlab.example.com), not arbitrary domains. https://git.mycompany.com/org/repo is left unchanged unless forge is set.

site_name: Internal docs
repo_url: https://scm.internal.example/org/repo
edit_uri: -/edit/main/docs/

plugins:
  - source-links:
      forge: gitlab

Before:

[config](../backend/config.py)

After:

https://scm.internal.example/org/repo/-/blob/main/backend/config.py

See Forges for autodetection vs explicit forge.

GitHub Enterprise

Hosts such as github.example.com autodetect as GitHub (whole hostname label github). No forge needed:

repo_url: https://github.example.com/org/repo
edit_uri: edit/main/docs/

plugins:
  - source-links

Hosts where github is only part of a label (for example github-internal.corp) are not autodetected; set forge: github explicitly for those.

Monorepo with docs_dir at repo root

my-monorepo/
  mkdocs.yml
  README.md
  packages/foo/
    docs/
      index.md
    src/
      foo.py
site_name: Foo package
repo_url: https://github.com/you/my-monorepo
edit_uri: edit/main/docs/
docs_dir: packages/foo/docs

plugins:
  - source-links

From packages/foo/docs/index.md:

Source markdown Resolves to repo path Built link path segment
[src](../src/foo.py) packages/foo/src/foo.py .../blob/main/packages/foo/src/foo.py
[readme](../../README.md) README.md .../blob/main/README.md

repo_url is always the git repository URL (monorepo root), not a package subdirectory.

Nested mkdocs.yml (subdirectory config)

my-monorepo/
  README.md
  packages/foo/
    mkdocs.yml
    docs/
      index.md
    src/
      foo.py

Build with:

mkdocs build -f packages/foo/mkdocs.yml

Plugin repository root is packages/foo/. A link [readme](../../README.md) resolves outside that root and is not rewritten. Prefer mkdocs.yml at the monorepo root with docs_dir, or accept that only paths under packages/foo/ are linkable via ../.

Fixed behavior

Summary of behavior that does not change with configuration. Details in Limitations.

Topic Behavior
Rewrite stage on_page_markdown — markdown only, not HTML (on_page_content is not used).
Code blocks Fenced blocks (``` or ~~~) and inline code spans are skipped. Indented (4-space) blocks are not — complete inline parent-directory links there still rewrite; lonely link suffixes in prose do not match. Use fences for literal examples.
Virtual pages Pages without a backing markdown file are unchanged.
Line anchors #L10, #L10-L20 translated per forge; Azure DevOps omits line fragments.
Targets Only existing files/directories under the plugin root at build time; directory targets treat trailing slash as optional.
Global ref One ref per build from pin / branch; no per-link ref override.

Full example

Commented mkdocs.yml combining common settings (generic placeholders):

site_name: My project
site_url: https://you.github.io/your-repo/   # optional; site URL for MkDocs

# Required for source-links rewriting
repo_url: https://github.com/you/your-repo
repo_name: you/your-repo

# Branch for forge URLs when plugin branch / extra.git_branch unset
edit_uri: edit/main/docs/

# Alternative branch override (second priority after plugin branch:)
# extra:
#   git_branch: develop

# Monorepo: point docs_dir at package docs while mkdocs.yml stays at repo root
# docs_dir: packages/foo/docs

exclude_docs: snippets/*   # if using pymdownx snippets for root markdown

theme:
  name: material

plugins:
  - search
  - source-links:
      enabled: !ENV [SOURCE_LINKS, true]   # SOURCE_LINKS=false mkdocs serve
      pin: branch                            # branch | commit | tag
      # branch: develop                       # override resolved branch
      # forge: gitlab                         # neutral hostname only
      warn_on_missing: true                  # false to silence; true + --strict for CI
      # log_rewrites: summary                # opt-in rewrite stats; false by default

markdown_extensions:
  - admonition
  - toc:
      permalink: true
  - pymdownx.highlight
  - pymdownx.superfences
  - pymdownx.snippets:          # optional: include root markdown in docs
      base_path: ["."]
      check_paths: true

Decision tree

Use this flow for pin and forge; adjust the other options as needed:

  • enabled: false or SOURCE_LINKS=false — skip rewriting for local iteration.
  • branch: — override resolved branch. For a hardcoded tag name without a tag checkout, see Hardcoded tag without tag checkout (GitHub/GitLab/Bitbucket Cloud only; use pin: tag on Gitea/Azure).
  • warn_on_missing: false — allow missing ../ targets without warnings.
  • log_rewrites: summary or verbose — confirm rewriting during setup; keep false in CI.

  • repo_url set? If no → links unchanged. If yes → continue.

  • Release build at an exact tag checkout? If yes → pin: tag. If no → continue.
  • Need permalink to HEAD SHA? If yes → pin: commit. If no → pin: branch (default).
  • Host autodetects forge? If no → set forge explicitly (see Forges). If yes → build.

Further reading: Configuration · Usage · Forges · Limitations