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:
- Plugin
branch:config extra.git_branchinmkdocs.yml- Parsed from
edit_uri(edit/<branch>/...,blob/<branch>/..., or Bitbucketsrc/<branch>/...) — only the first path segment afteredit/,blob/, orsrc/is used, so branch names containing/(for examplefeature/my-branch) are not parsed correctly; setbranch:orextra.git_branchexplicitly instead - 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.ymlat the monorepo root withdocs_dir: packages/foo/docs→ root is the monorepo.mkdocs.ymlinsidepackages/foo/→ root ispackages/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.
Choosing link styles¶
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¶
- Create a branch with a conventional prefix (
feat/,fix/,chore/,docs/,refactor/,test/). - Make your change and add tests. The suite must stay at 100% coverage.
- 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
-
Open a pull request targeting
mainwith a conventional title (for examplefix: ...). 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). -
Include
Signed-off-by: Your Name <email@example.com>in each commit message (see Developer Certificate of Origin). Usegit commit -sto add this automatically. The DCO workflow enforces a matching sign-off on every PR commit; fix existing commits withgit 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 lintbefore 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:
make release-prep VERSION=X.Y.Z— bumpsversioninpyproject.toml, rolls[Unreleased]into a dated## [X.Y.Z]section with updated compare links, runsmake ci, and opens the release PR.- After the PR is merged,
make release-tag VERSION=X.Y.Z— creates a signed annotated tagvX.Y.Zand 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 singlegh release create(required for immutable releases — do not enable SLSAupload-assets, which pre-creates an empty release and breaks asset upload). Release notes on GitHub are taken from the matchingCHANGELOG.mdsection with a compare link footer; a post-release job verifies provenance withslsa-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.ymldirectory is not inside a git worktree (for example a release archive or a shallow export with no.git). gitis not installed or not onPATH.- For
pin: tagonly,HEADis not exactly tagged (git describe --tags --exact-matchfails) — 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
Strict CI (catch broken links)¶
Default warn_on_missing: true plus:
mkdocs build --strict
Warnings for missing ../ targets fail the build.
Silent missing links¶
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: falseorSOURCE_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; usepin: tagon Gitea/Azure).warn_on_missing: false— allow missing../targets without warnings.-
log_rewrites: summaryorverbose— confirm rewriting during setup; keepfalsein CI. -
repo_urlset? If no → links unchanged. If yes → continue. - Release build at an exact tag checkout? If yes →
pin: tag. If no → continue. - Need permalink to
HEADSHA? If yes →pin: commit. If no →pin: branch(default). - Host autodetects forge? If no → set
forgeexplicitly (see Forges). If yes → build.
Further reading: Configuration · Usage · Forges · Limitations