mdbookkit

mdbookkit hero image

crates.io documentation MIT/Apache-2.0 licensed

Quality-of-life plugins for your mdBook project.

  • mdbook-rustdoc-link

    rustdoc-style linking for Rust APIs: write types and function names, get links to docs.rs

  • mdbook-link-forever

    Permalinks for your source tree: write relative paths, get links to GitHub.

Installation

If you are interested in any of these plugins, visit their respective pages for usage instructions, linked above.

If you want to install all of them:

cargo install mdbookkit --all-features

Precompiled binaries are also available from GitHub releases.

License

This project is released under the Apache 2.0 License and the MIT License.

mdbook-rustdoc-link

rustdoc-style linking for mdBook (with the help of rust-analyzer).

You write:

The [`option`][std::option] and [`result`][std::result] modules define optional and
error-handling types, [`Option<T>`] and [`Result<T, E>`]. The [`iter`][std::iter] module
defines Rust's iterator trait, [`Iterator`], which works with the `for` loop to access
collections. [^1]

You get:

The option and result modules define optional and error-handling types, Option<T> and Result<T, E>. The iter module defines Rust's iterator trait, Iterator, which works with the for loop to access collections. 1

mdbook-rustdoc-link is an mdBook preprocessor. Using rust-analyzer, it converts type names, module paths, and so on, into links to online crate docs. No more finding and pasting URLs by hand.

screen recording of mdbook-rustdoc-link during mdbook build

Overview

To get started, simply follow the quickstart guide!

If you would like to read more about this crate:

For writing documentation

For adapting this crate to your project

For additional usage information

Happy linking!

License

This project is released under the Apache 2.0 License and the MIT License.


  1. Text adapted from A Tour of The Rust Standard Library

Getting started

Follow these steps to start using mdbook-rustdoc-link in your book project!

Install

You will need to:

  1. Have rust-analyzer:

    • If you already use the VS Code extension: this crate automatically uses the server binary that comes with it, no extra setup is needed!
    • Otherwise, install rust-analyzer (e.g. via rustup) and make sure it's on your PATH.
  2. Install this crate:

    cargo install mdbookkit --features rustdoc-link
    

    Or you can grab precompiled binaries from GitHub releases.

Configure

Configure your book.toml to use it as a preprocessor:

[book]
title = "My Book"

[preprocessor.rustdoc-link]
# mdBook will run `mdbook-rustdoc-link`
after = ["links"]
# recommended, so that it can see content from {{#include}} as well

Write

In your documentation, when you want to link to a Rust item, such as a type, a function, etc., simply use its name in place of a URL, like this:

Like [`std::thread::spawn`], [`tokio::task::spawn`] returns a
[`JoinHandle`][tokio::task::JoinHandle] struct.

The preprocessor will then turn them into hyperlinks:

Like std::thread::spawn, tokio::task::spawn returns a JoinHandle struct.

This works in both mdbook build and mdbook serve!

screen recording of mdbook-rustdoc-link during mdbook build

To read more about this project, feel free to return to Overview.

important

It is assumed that you are running mdbook within a Cargo project.

If you are working on a crate, and your book directory is within your source tree, such as next to Cargo.toml, then running mdbook from there will "just work".

If your book doesn't belong to a Cargo project, refer to Workspace layout for more information on how you can setup up the preprocessor.

tip

mdbook-rustdoc-link makes use of rust-analyzer's "Open Docs" feature, which resolves links to documentation given a symbol.

Items from std will generate links to https://doc.rust-lang.org, while items from third-party crates will generate links to https://docs.rs.

So really, rust-analyzer is doing the heavy-lifting here. This crate is just the glue code :)

Motivation

rustdoc supports linking to items by name, a.k.a. intra-doc links. This is awesome for at least two reasons:

mdBook doesn't have the luxury of accessing compiler internals yet, so you are left with manually sourcing links from docs.rs. Then one of two things could happen:

  • APIs are mentioned without linking to reference docs.

    This is probably fine for tutorials and examples, but it does mean readers of your docs won't be able to move from guides to references as easily.

  • You do want at least some cross-references, but it is cumbersome to find and copy the correct links, and even more so to maintain them.

    Links to docs.rs often use latest as the version, which could become out-of-sync with your code, especially if they point to third-party or unstable APIs.

mdbook-rustdoc-link is the tooling answer to these problems. Effortless, correct, and good practice — choose all three!

note

That being said, sometimes manually specifying URLs is the best option.

Most importantly, writing links by name means they won't be rendered as such when your Markdown source is displayed elsewhere. If your document is also intended for places like GitHub or crates.io, then you should probably not use this preprocessor.

Name resolution

mdbook-rustdoc-link resolves items in the context of your crate's "entrypoint", which is usually your lib.rs or main.rs (the specific rules are mentioned below).

tip

If you use Cargo workspaces, or if your source tree has special layout, see Workspace layout for more information.

An item must be in scope in the entrypoint for the proprocessor to generate a link for it.

Let's say you have the following as your lib.rs:

use anyhow::Context;
/// Type that can provide links.
pub trait Resolver {}
mod env {
    /// Options for the preprocessor.
    pub struct Config {}
}

Items in the entrypoint can be linked to with just their names:

[`Resolver`] — Type that can provide links.

This crate also uses the [`Context`] trait from [`anyhow`].

Resolver — Type that can provide links.

This crate also uses the Context trait from anyhow.

This includes items from the prelude (unless you are using #![no_implicit_prelude]):

[`FromIterator`] is in the prelude starting from Rust 2021.

FromIterator is in the prelude starting from Rust 2021.

Though technically not required — to make items from your crate more distinguishable from others in your Markdown source, you can write crate::*:

[Configurations](configuration.md) for the preprocessor is defined in the
[`Config`][crate::env::Config] type.

Configurations for the preprocessor is defined in the Config type.

For everything else, provide its full path, as if you were writing a use declaration:

[`JoinSet`][tokio::task::JoinSet] is analogous to `asyncio.as_completed`.

JoinSet is analogous to asyncio.as_completed.

tip

In short, write links the way you use an item in your lib.rs or main.rs.

The preprocessor will emit a warning if an item cannot be resolved:

warning emitted when an item cannot be resolved

Formatting of diagnostics powered by miette

This is something to remember especially if you are including doc comments as part of your Markdown docs. Only rustdoc has the ability to resolve names from where the comments are written, so links that work in doc comments may not work when using this preprocessor!

Feature-gated items

To link to items that are gated behind features, use the cargo-features option in book.toml.

For example, clap is known for providing guide-level documentation through docs.rs. The tutorial for its Derive API is gated behind the unstable-doc feature. To link to such items, configure the necessary features:

[preprocessor.rustdoc-link]
cargo-features = ["clap/unstable-doc"]

Then, specify the item as normal:

[Tutorial for clap's Derive API][clap::_derive::_tutorial]

Tutorial for clap’s Derive API

Which entrypoint

For this preprocessor, the "entrypoint" is usually src/lib.rs or src/main.rs.

  • If your crate has multiple bin targets, it will use the first one listed in your Cargo.toml.
  • If your crate has both lib and bins, it will prefer lib.
  • If your crate has custom paths in Cargo.toml instead of the default src/lib.rs or src/main.rs, it will honor that.

How it works

note

The following are implementation details. See rustdoc_link/mod.rs.

mdbook-rustdoc-link parses your book and collects every link that looks like a Rust item. Then it synthesizes a Rust function that spells out all the items, which looks roughly like this:

fn __ded48f4d_0c4f_4950_b17d_55fd3b2a0c86__ () {
    Result::<T, E>;
    core::net::Ipv4Addr::LOCALHOST;
    std::vec!();
    serde::Serialize!();
    <Vec<()> as IntoIterator>::into_iter;
    // ...
}

Note that this is barely valid Rust — Result::<T, E>; is a type without a value, and you wouldn't use serde::Serialize as a regular macro.

This is where language servers like rust-analyzer excel — they can provide maximally useful information out of badly-shaped code.

The preprocessor appends this fake function to your lib.rs or main.rs (in memory, it doesn't modify your file) and sends it to rust-analyzer. Then, for each item that needs to be resolved, the preprocessor sends an external documentation request.

{
  "jsonrpc": "2.0",
  "method": "experimental/externalDocs",
  "params": {
    "textDocument": { "uri": "file:///src/lib.rs" },
    "position": { "line": 6, "character": 17 }
  }
}

Hence item names in your book must be resolvable from your crate entrypoint!

This process is as if you had typed a name into your source file and used the "Open Docs" feature — except it's fully automated.

the Open Docs option in VS Code

Supported syntax

This page showcases all the syntax supported by mdbook-rustdoc-link.

Most of the formats supported by rustdoc are supported. Unsupported syntax and differences in behavior are emphasized below.

In general, specifying items as you would when writing Rust code should "just work".

Sections

tip

This page is also used for snapshot testing! To see how all the links would look like in Markdown after they have been processed, see supported-syntax.snap and supported-syntax.stderr.snap.

Types, modules, and associated items

Module [`alloc`][std::alloc] — Memory allocation APIs.

Module alloc — Memory allocation APIs.

Every [`Option`] is either [`Some`][Option::Some][^1] and contains a value, or
[`None`][Option::None][^1], and does not.

Every Option is either Some1 and contains a value, or None1, and does not.

[`Ipv4Addr::LOCALHOST`][core::net::Ipv4Addr::LOCALHOST] — An IPv4 address with the
address pointing to localhost: `127.0.0.1`.

Ipv4Addr::LOCALHOST — An IPv4 address with the address pointing to localhost: 127.0.0.1.

Generic parameters

Types can contain generic parameters. This is compatible with rustdoc.

[`Vec<T>`] — A heap-allocated _vector_ that is resizable at runtime.

Vec<T> — A heap-allocated vector that is resizable at runtime.

| Phantom type                                       | variance of `T`   |
| :------------------------------------------------- | :---------------- |
| [`&'a mut T`][std::marker::PhantomData<&'a mut T>] | **in**variant     |
| [`fn(T)`][std::marker::PhantomData<fn(T)>]         | **contra**variant |
Phantom typevariance of T
&'a mut Tinvariant
fn(T)contravariant

This includes if you use turbofish:

`collect()` is one of the few times you’ll see the syntax affectionately known as the
"turbofish", for example: [`Iterator::collect::<Vec<i32>>()`].

collect() is one of the few times you’ll see the syntax affectionately known as the "turbofish", for example: Iterator::collect::<Vec<i32>>().

Functions and macros

To indicate that an item is a function, add () after the function name. To indicate that an item is a macro, add ! after the macro name, optionally followed by (), [], or {}. This is compatible with rustdoc.

Note that there cannot be arguments within (), [], or {}.

[`vec!`][std::vec!][^2] is different from [`vec`][std::vec], and don't accidentally
use [`format()`][std::fmt::format()] in place of [`format!()`][std::format!()][^2]!

vec!2 is different from vec, and don't accidentally use format() in place of format!()2!

The macro syntax works for attribute and derive macros as well (even though this is not how they are invoked).

There is a [derive macro][serde::Serialize!] to generate implementations of the
[`Serialize`][serde::Serialize] trait.

There is a derive macro to generate implementations of the Serialize trait.

Implementors and fully qualified syntax

Trait implementors may supply additional documentation about their implementations. To link to implemented items instead of the traits themselves, use fully qualified paths, including <... as Trait> if necessary. This is a new feature that rustdoc does not currently support.

[`Result<T, E>`] implements [`IntoIterator`]; its
[**`into_iter()`**][Result::<(), ()>::into_iter] returns an iterator that yields one
value if the result is [`Result::Ok`], otherwise none.

[`Vec<T>`] also implements [`IntoIterator`]; a vector cannot be used after you call
[**`into_iter()`**][<Vec<()> as IntoIterator>::into_iter].

Result<T, E> implements IntoIterator; its into_iter() returns an iterator that yields one value if the result is Result::Ok, otherwise none.

Vec<T> also implements IntoIterator; a vector cannot be used after you call into_iter().

note

If your type has generic parameters, you must supply concrete types for them for rust-analyzer to be able to locate an implementation. That is, Result<T, E> won't work, but Result<(), ()> will (unless there happen to be types T and E literally in scope).

Disambiguators

rustdoc's disambiguator syntax prefix@name is accepted but ignored:

[`std::vec`], [`mod@std::vec`], and [`macro@std::vec`] all link to the `vec` _module_.

std::vec, mod@std::vec, and macro@std::vec all link to the vec module.

This is largely okay because currently, duplicate names in Rust are allowed only if they correspond to items in different namespaces, for example, between macros and modules, and between struct fields and methods — this is mostly covered by the function and macro syntax, described above.

If you encounter items that must be disambiguated using rustdoc's disambiguator syntax, other than the "special types" listed below, please file an issue!

Special types

warning

There is no support on types whose syntax is not a path; they are currently not parsed at all:

references &T, slices [T], arrays [T; N], tuples (T1, T2), pointers like *const T, trait objects like dyn Any, and the never type !

Note that such types can still be used as generic params, just not as standalone types.

Struct fields

warning

Linking to struct fields is not supported yet. This is incompatible with rustdoc.

All Markdown link formats supported by rustdoc are supported:

Linking with URL inlined:

[The Option type](std::option::Option)

The Option type

Linking with reusable references:

[The Option type][option-type]

[option-type]: std::option::Option

The Option type

Reference-style links [text][id] without a corresponding [id]: name part will be treated the same as inline-style links [text](id):

[The Option type][std::option::Option]

The Option type

Shortcuts are supported, and can contain inline markups:

You can create a [`Vec`] with [**`Vec::new`**], or by using the [_`vec!`_][^2] macro.

You can create a Vec with Vec::new, or by using the vec!2 macro.

(The items must still be resolvable; in this case Vec and vec! come from the prelude.)

Linking to page sections

To link to a known section on a page, use a URL fragment, just like a normal link. This is compatible with rustdoc.

[When Should You Use Which Collection?][std::collections#when-should-you-use-which-collection]

When Should You Use Which Collection?


  1. rust-analyzer's ability to generate links for enum variants like Option::Some was improved only somewhat recently: before #19246, links for variants and associated items may only point to the types themselves. If linking to such items doesn't seem to work for you, be sure to upgrade to a newer rust-analyzer first! ↩2

  2. As of rust-analyzer 2025-03-17, links generated for macros don't always work. Examples include std::format! (seen above) and tokio::main!. For more info, see Known issues. ↩2 ↩3

Workspace layout

As mentioned in Name resolution, the preprocessor must know where your crate's entrypoint is.

To do that, it tries to find a Cargo.toml by running cargo locate-project, by default from the current working directory.

If you have a single-crate setup, this should "just work", regardless of where your book directory is within your source tree.

If you are using Cargo workspaces, then the preprocessor may fail with the message:

Error: Cargo.toml does not have any lib or bin target

This means it found your workspace Cargo.toml instead of a member crate's. To use the preprocessor in this case, some extra setup is needed.

Sections

Using the manifest-dir option

In your book.toml, in the [preprocessor.rustdoc-link] table, set the manifest-dir option to the relative path to a member crate.

For example, if you have the following workspace layout:

my-workspace/
├── crates/
│   └── fancy-crate/
│       ├── src/
│       │   └── lib.rs
│       └── Cargo.toml
└── docs/
    ├── src/
    │   └── SUMMARY.md
    └── book.toml

Then in your book.toml:

[preprocessor.rustdoc-link]
manifest-dir = "../crates/fancy-crate"

important

manifest-dir should be a path relative to book.toml, not relative to workspace root.

Placing your book inside a member crate

If you have a "main" crate, you can also move your book directory to that crate, and run mdbook from there:

my-workspace/
└── crates/
    ├── fancy-crate/
    │   ├── docs/
    │   │   ├── src/
    │   │   │   └── SUMMARY.md
    │   │   └── book.toml
    │   ├── src/
    │   │   └── lib.rs
    │   └── Cargo.toml
    └── util-crate/
        └── ...

Documenting multiple crates

If you would like to document items from several independent crates, but still would like to centralize your book in one place — unfortunately, the preprocessor does not yet have the ability to work with multiple entrypoints.

A possible workaround would be to turn your book folder into a private crate that depends on the crates you would like to document. Then you can link to them as if they were third-party crates.

my-workspace/
├── crates/
│   ├── fancy-crate/
│   │   └── Cargo.toml
│   └── awesome-crate/
│       └── Cargo.toml
├── docs/
│   ├── Cargo.toml
│   └── book.toml
└── Cargo.toml
# docs/Cargo.toml
[dependencies]
fancy-crate = { path = "../crates/fancy-crate" }
awesome-crate = { path = "../crates/awesome-crate" }
# Cargo.toml
[workspace]
members = ["crates/*", "docs"]
default-members = ["crates/*"]
resolver = "2"

Using without a Cargo project

If your book isn't for a Rust project, but you still find a use in this preprocessor (e.g. perhaps you would like to mention std) — unfortunately, the preprocessor does not yet support running without a Cargo project.

Instead, you can setup your book project as a private, dummy crate.

my-book/
├── src/
│   └── SUMMARY.md
├── book.toml
└── Cargo.toml
# Cargo.toml
[dependencies]
# empty, or you can add anything you need to document

Caching

By default, mdbook-rustdoc-link spawns a fresh rust-analyzer process every time it is run. rust-analyzer then reindexes your entire project before resolving links.

This significantly impacts the responsiveness of mdbook serve — it is as if for every live reload, you had to reopen your editor, and it gets even worse the more dependencies your project has.

To mitigate this, there is an experimental caching feature, disabled by default.

Sections

Enabling caching

In your book.toml, in the [preprocessor.rustdoc-link] table, set cache-dir to the relative path of a directory of your choice (other than your book's build-dir), for example:

[preprocessor.rustdoc-link]
cache-dir = "cache"
# You could also point to an arbitrary directory in target/

Now, when mdbook rebuilds your book during build or serve, the preprocessor reuses the previous resolution and skips rust-analyzer entirely if your edit does not involve changes in the set of Rust items to be linked, that is, no new items unseen in the previous build.

important

If you use a directory under your book root directory, make sure to also have a .gitignore in your book root dir to exclude it from source control, or the cache file could trigger additional reloads. See Specify exclude patterns in the mdBook documentation.

Do not use your book's build-dir as the cache-dir: mdbook clears the output directory on every build, making this setup useless.

How it works

note

The following are implementation details. See rustdoc_link/cache.rs.

The effectiveness of this mechanism is based on the following assumptions:

  • Most of the changes made during authoring don't actually involve item links.
  • Assuming the environment is unchanged, the same set of items should resolve to the same set of links.

The cache keeps the following information in a cache.json:

  • The set of items to be resolved, and their resolved links
  • The environment, as a checksum over the contents of:
    • Your crate's Cargo.toml
    • If you are using a workspace, the workspace's Cargo.toml
    • The entrypoint (lib.rs or main.rs)
    • For each item that is defined within your crate or workspace, its source file
    • (Note that Cargo.lock is currently not considered, nor are dependencies or std)

If a subsequent run has the same set of items (or a subset) and the same checksum (meaning you did not update your code), then the preprocessor simply reuses the previous results.

tip

Items that fail to resolve are not included in the cache.

If you keep such broken links in your Markdown source, the cache will permanently miss, and rust-analyzer will run on every edit.

Help wanted 🙌

The cache feature, as it currently stands, is a workaround at best. If you have insights on how performance could be further improved, please open an issue!

Cache priming and progress tracking

The preprocessor spawns rust-analyzer with cache priming enabled which contributes to the majority of build time.

Furthermore, the preprocessor relies on the LSP Work Done Progress notifications to know when rust-analyzer has finished cache priming, before actually sending out external docs requests. This requires parsing non-structured log messages that rust-analyzer sends out and some debouncing/throttling logic, which is not ideal, see client.rs.

Not waiting for indexing to finish and sending out requests too early causes rust-analyzer to respond with empty results.

Questions:

  • Is it possible to do it without cache priming?
  • Is there a better way to track rust-analyzer's "readiness" without having to arbitrary sleep?

Using ra-multiplex

ra-multiplex "allows multiple LSP clients (editor windows) to share a single rust-analyzer instance per cargo workspace."

In theory, in an IDE setting (e.g. with VS Code), one could setup the IDE and mdbook-rustdoc-link to both connect to the same ra-multiplex server. Then the preprocessor doesn't need to wait for cache priming (the cache is already warm from IDE use). Changes in the workspace could also be reflected in subsequent builds without the preprocessor being aware of them (because the IDE is doing the synchronizing).

In reality, with the current version, connecting the preprocessor to ra-multiplex seems to result in buggy builds. The initial build emits in many warnings despite all items eventually resolving. Subsequent builds hang indefinitely before timing out.

Question:

  • Is it possible to use ra-multiplex here?

Postscript

mdbook encourages a stateless architecture for preprocessors. Preprocessors are expected to work like pure functions over the entire book, even for mdbook serve. Preprocessors are not informed on whether they are invoked as part of mdbook build (prefer fresh starts) or mdbook serve (maintain states between run).

rust-analyzer, meanwhile, has a stateful architecture that also doesn't yet have persistent caching1. It is designed to take in a ground state (your project initially) and then evolve the state (your project edited) entirely in memory.

So rust-analyzer has an extremely incremental architecture, perfect for complex languages like Rust, and mdbook has an explicitly non-incremental architecture, perfect for rendering Markdown. This makes them somewhat challenging to work well together in a live-reload scenario.


  1. It was mentioned that the recently updated, salsa-ified rust-analyzer (version 2025-03-17) will unblock work on persistent caching, among many other things, so hopefully bigger changes are coming!

Standalone usage

You can use mdbook-rustdoc-link as a standalone Markdown processor from the command line.

Simply use the markdown subcommand, send your Markdown through stdin, and receive the result through stdout, for example:

mdbook-rustdoc-link markdown < README.md

The command accepts as arguments all options configurable in book.toml, such as --cache-dir. Run mdbook-rustdoc-link markdown --help to see them.

example using mdbook-rustdoc-link as a command line tool
Use it in any text processing pipeline!

Continuous integration

This page gives information and tips for using mdbook-rustdoc-link in a continuous integration (CI) environment.

The preprocessor behaves differently in terms of logging, error handling, etc., when it detects it is running in CI.

Sections

Detecting CI

To determine whether it is running in CI, the preprocessor honors the CI environment variable. Specifically:

  • If CI is set to "true", then it is considered in CI1;
  • Otherwise, it is considered not in CI.

Most major CI/CD services, such as GitHub Actions and GitLab CI/CD, automatically configure this variable for you.

Installing rust-analyzer

rust-analyzer must be on PATH when running in CI2.

One way is to install it via rustup. For example, in GitHub Actions, you can use:

steps:
  - uses: dtolnay/rust-toolchain@stable
    with:
      components: rust-analyzer

note

Be aware that rust-analyzer from rustup follows Rust's release schedule, which means it may lag behind the version bundled with the VS Code extension.

Logging

By default, the preprocessor shows a progress spinner when it is running.

When running in CI, progress is instead printed as logs (using log and env_logger)3.

You can control logging levels using the RUST_LOG environment variable.

Error handling

By default, when the preprocessor encounters any non-fatal issues, such as when a link fails to resolve, it prints them as warnings but continues to run. This is so that your book continues to build via mdbook serve while you make edits.

When running in CI, all such warnings are promoted to errors. The preprocessor will exit with a non-zero status code when there are warnings, which will fail your build. This prevents outdated or incorrect links from being accidentally deployed.

You can explicitly control this behavior using the fail-on-warnings option.


  1. Specifically, when CI is anything other than "", "0", or "false". The logic is encapsulated in the is_ci function.

  2. Unless you use the rust-analyzer option.

  3. Specifically, when stderr is redirected to something that isn't a terminal, such as a file.

Configuration

This page lists all options for the preprocessor.

For use in book.toml, configure under the [preprocessor.rustdoc-link] table using the keys below, for example:

[preprocessor.rustdoc-link]
rust-analyzer = "path/to/rust-analyzer --option ..."

For use on the command line, use the keys as long arguments, for example:

mdbook-rustdoc-link markdown --rust-analyzer "..."
Option Summary

cache-dir

Directory in which to persist build cache

cargo-features

List of features to activate when running rust-analyzer

fail-on-warnings

Exit with a non-zero status code when some links fail to resolve

manifest-dir

Directory from which to search for a Cargo project

rust-analyzer

Command to use for spawning rust-analyzer

smart-punctuation

Whether to enable punctuations like smart quotes “”

cache-dir

Directory in which to persist build cache.

Setting this will enable caching. Will skip rust-analyzer if cache hits.

Default

None

Type

PathBuf

cargo-features

List of features to activate when running rust-analyzer.

This is just the rust-analyzer.cargo.features config.

In book.toml — to enable all features, use ["all"].

For CLI — to enable multiple features, specify as comma-separated values, or specify multiple times; to enable all features, specify --cargo-features all.

Default

[]

Type

Vec<String>

fail-on-warnings

Exit with a non-zero status code when some links fail to resolve.

Warnings are always printed to the console regardless of this option.

Choice Description
"ci"

Fail if the environment variable CI is set to a value other than 0 or false. Environments like GitHub Actions configure this automatically

"always"

Fail as long as there are warnings, even in local use

Default

"ci"

Type

ErrorHandling

manifest-dir

Directory from which to search for a Cargo project.

By default, the current working directory is used. Use this option to specify a different directory.

The processor requires the Cargo.toml of a package to work. If you are working on a Cargo workspace, set this to the relative path to a member crate.

Default

None

Type

PathBuf

rust-analyzer

Command to use for spawning rust-analyzer.

By default, prebuilt binary from the VS Code extension is tried. If that doesn't exist, it is assumed that rust-analyzer is on PATH. Use this option to override this behavior completely.

The command string will be tokenized by shlex, so you can include arguments in it.

Default

None

Type

String

smart-punctuation

Whether to enable punctuations like smart quotes “”.

This is only meaningful if your links happen to have visible text that has specific punctuation. The processor otherwise passes through the rest of your Markdown source untouched.

In book.toml — this option is not needed because output.html.smart-punctuation is honored.

Default

false

Type

bool

Known issues

Sections

Performance

mdbook-rustdoc-link itself doesn't need much processing power, but it invokes rust-analyzer, which does a full scan of your workspace. The larger your codebase is, the longer mdbook will have to wait for the preprocessor. This is the source of the majority of the run time.

There is an experimental caching feature, which persists query results after runs and reuses them when possible, avoiding spawning rust-analyzer when your edit doesn't involve item links.

In limited circumstances, the preprocessor generates links that are incorrect or inaccessible.

note

The following observations are based on rust-analyzer 2025-03-17.

Macros

Macros exported with #[macro_export] are always exported at crate root, and are documented as such by rustdoc, but rust-analyzer currently generates links to the modules they are defined in. For example:

Attribute macros generate links that use macro.<macro_name>.html, but rustdoc actually generates attr.<macro_name>.html. For example:

Trait items

Rust allows methods to have the same name if they are from different traits, and types can implement the same trait multiple times if the trait is generic. All such methods will appear on the same page for the type.

rustdoc will number the generated URL fragments so that they remain unique within the HTML document. rust-analyzer does not yet have the ability to do so.

For example, these are the same links:

The correct link for the From<Ipv6Addr> implementation is actually https://doc.rust-lang.org/stable/core/net/enum.IpAddr.html#method.from-1

Private items

rustdoc has a private_intra_doc_links lint that warns you when your public documentation tries to link to private items.

The preprocessor does not yet warn you about links to private items: rust-analyzer will generate links for items regardless of their crate-level visibility.

Unresolved items

Associated items on primitive types

note

The following observations are based on rust-analyzer 2025-03-17.

Links to associated methods and items on primitive types are currently not resolved by rust-analyzer. For example:

  • [str::parse]
  • [f64::MIN_POSITIVE]

Sites other than docs.rs

Currently, items from crates other than std always generate links that point to https://docs.rs. mdbook-rustdoc-link does not yet support configuring alternative hosting sites for crates (such as wasm-bindgen which hosts API docs under https://rustwasm.github.io/wasm-bindgen/api/).

Wrong line numbers in diagnostics

When the preprocessor fails to resolve some items, it emits warnings that look like:

warning emitted that has the wrong line numbers

You may notice that the line numbers are sometimes incorrect for your source file. This could happen in files that use the {{#include}} directive, for example.

This is an unfortunate limitation with mdBook's preprocessor architecture. Preprocessors are run sequentially, with the next preprocessor receiving Markdown source rendered by the previous one. If preprocessors running before mdbook-rustdoc-link modify Markdown source in such ways that shift lines around, then the line numbers will look incorrect.

Unless mdBook somehow gains source map support, this problem is unlikely to ever be solved.

mdbook-link-forever

mdBook preprocessor that takes care of linking to files in your Git repository.

mdbook-link-forever rewrites path-based links to version-pinned GitHub permalinks. No more hard-coded GitHub URLs.

Here's a link to the [Cargo workspace manifest](../../../Cargo.toml).

Here's a link to the Cargo workspace manifest.

  • Versions are determined at build time. Supports both tags and commit hashes.
  • Because paths are readily accessible at build time, it also validates them for you.

Getting started

  1. Install this crate:

    cargo install mdbookkit --features link-forever
    
  2. Configure your book.toml:

    [book]
    title = "My Book"
    
    [output.html]
    git-repository-url = "https://github.com/me/my-awesome-crate"
    # will use this for permalinks
    
    [preprocessor.link-forever]
    # mdBook will run `mdbook-link-forever`
    
  3. Link to files using paths, like this:

    See [`book.toml`](../../book.toml#L44-L48) for an example config.
    

    See book.toml for an example config.

License

This project is released under the Apache 2.0 License and the MIT License.

Features

Simply use relative paths to link to any file in your source tree, and the preprocessor will convert them to GitHub permalinks.

This project is dual licensed under the
[Apache License, Version 2.0](../../../LICENSE-APACHE.md) and the
[MIT (Expat) License](../../../LICENSE-MIT.md).

This project is dual licensed under the Apache License, Version 2.0 and the MIT (Expat) License.

Permalinks use the tag name or commit SHA of HEAD at build time, so you get a rendered book with intra-repo links that are always correct for that point in time.

tip

Linking by path is cool! Not only is it well-supported by GitHub, but editors like VS Code also provide smart features like path completions and link validation.

URL fragments are preserved:

This book uses [esbuild] to
[preprocess its style sheet](../../app/build/build.ts#L13-L24).

This book uses esbuild to preprocess its style sheet.

By default, links to files under your book's src/ directory are not converted, since mdBook already copies them to build output, but this is configurable using the always-link option.

Repo auto-discovery

To know what GitHub repository to link to, the preprocessor looks at the following places, in order:

  1. The output.html.git-repository-url option in your book.toml
  2. The URL of a Git remote named origin1

tip

For Git remotes, both HTTP URLs and "scp-like" URIs (git@github.com:org/repo.git) are supported, thanks to the gix_url crate.

If you use Git but not GitHub, you can configure a custom URL pattern using the repo-url-template option. For example:

[preprocessor.link-forever]
repo-url-template = "https://gitlab.haskell.org/ghc/ghc/-/tree/{ref}/{path}"

The preprocessor validates any path-based links and notifies you if they are broken.

warnings emitted for broken links

Formatting of diagnostics powered by miette

note

Link validations are only supported for path-based links. For more comprehensive link checking, look to projects like mdbook-linkcheck.


  1. The remote must be exactly named origin. No other name is recognized.

Working with {{#include}}

mdBook provides an {{#include}} directive for embedding files in book pages. If the embedded content also contains path-based links, then some extra care may be needed:

  • The preprocessor does not resolve links relative to the file being included (because it doesn't have enough information to do so). In this case, relative paths could be valid for the source file (and therefore valid for e.g. GitHub) but invalid for the book.

  • In some situations, you cannot use path-based links and you have to use full URLs. This could be because the included file is also intended for platforms that don't support paths as links.

This page describes some workarounds for linking in included files.

Using absolute paths

To use paths as links in included content, you can use absolute paths that start with a /. Paths that start with a / are resolved relative to the root of your repository:

Front page of this book is actually from
[the crate README](/crates/mdbookkit/README.md).

Front page of this book is actually from the crate README.

tip

This is also the behavior both in VS Code and on GitHub.

You may be in a situation where you have to use full URLs to link to your book rather than relying on paths.

An example (that this project encountered) is including files that are also intended for displaying on crates.io.

In this case, since other platforms like crates.io will not convert path-based .md links to URLs, linking to book pages would require writing down full URLs to the deployed book.

To mitigate this, you can use the book-url option.

In your book.toml, in the [preprocessor.link-forever] table, specify the URL prefix at which you will deploy your book:

[preprocessor.link-forever]
book-url = "https://example.org/"

Then, in Markdown, you may use full URLs, for example:

For a list of the crate's features, see [Feature flags](https://example.org/features).

For a list of the crate's features, see Feature flags.

Specifying book-url enables the preprocessor to check URLs to your book against local paths. If a URL does not have a corresponding .md file under your book's src/ directory, the preprocessor will warn you:

warnings emitted for broken canonical links

note

book-url only enables validation, and is only for links to your book, not to GitHub.

Continuous integration

This page gives information and tips for using mdbook-link-forever in a continuous integration (CI) environment.

The preprocessor behaves differently in terms of logging, error handling, etc., when it detects it is running in CI.

Sections

Detecting CI

To determine whether it is running in CI, the preprocessor honors the CI environment variable. Specifically:

  • If CI is set to "true", then it is considered in CI1;
  • Otherwise, it is considered not in CI.

Most major CI/CD services, such as GitHub Actions and GitLab CI/CD, automatically configure this variable for you.

Linking to Git tags

The preprocessor supports both tags and commit SHAs when generating permalinks. The use of tags is contingent on HEAD being tagged in local Git at build time. You should ensure that tags are present when building in CI, or the preprocessor will fallback to using the full SHA (it would still be a permalink, just that it will be more verbose).

For example, in GitHub Actions, you can use:

steps:
  - uses: actions/checkout@v4
    with:
      fetch-tags: true
      fetch-depth: 0 # https://github.com/actions/checkout/issues/1471#issuecomment-1771231294

Logging

By default, the preprocessor shows a progress spinner when it is running.

When running in CI, progress is instead printed as logs (using log and env_logger)2.

You can control logging levels using the RUST_LOG environment variable.

Error handling

By default, when the preprocessor encounters any non-fatal issues, such as when a link fails to resolve, it prints them as warnings but continues to run. This is so that your book continues to build via mdbook serve while you make edits.

When running in CI, all such warnings are promoted to errors. The preprocessor will exit with a non-zero status code when there are warnings, which will fail your build. This prevents outdated or incorrect links from being accidentally deployed.

You can explicitly control this behavior using the fail-on-warnings option.


  1. Specifically, when CI is anything other than "", "0", or "false". The logic is encapsulated in the is_ci function.

  2. Specifically, when stderr is redirected to something that isn't a terminal, such as a file.

Configuration

This page lists all options for the preprocessor.

For use in book.toml, configure under the [preprocessor.link-forever] table using the keys below, for example:

[preprocessor.link-forever]
always-link = [".rs"]
Option Summary

always-link

Convert some paths to permalinks even if they are under src/.

book-url

Specify the canonical URL at which you deploy your book.

fail-on-warnings

Exit with a non-zero status code when there are warnings

repo-url-template

Use a custom link format for platforms other than GitHub.

Convert some paths to permalinks even if they are under src/.

By default, links to files in your book's src/ directory will not be transformed, since they are already copied to build output as static files. If you want such files to always be rendered as permalinks, specify their file extensions here.

For example, to use permalinks for Rust source files even if they are in the book's src/ directory:

always-link = [".rs"]
Default

[]

Type

Vec<String>

book-url

Specify the canonical URL at which you deploy your book.

Should be a qualified URL. For example:

book-url = "https://me.github.io/my-awesome-crate/"

Enables validation of hard-coded links to book pages. The preprocessor will warn you about links that are no longer valid (file not found) at build time.

This is mainly used with mdBook's {{#include}} feature, where sometimes you have to specify full URLs because path-based links are not supported.

Default

None

Type

UrlPrefix

fail-on-warnings

Exit with a non-zero status code when there are warnings.

Warnings are always printed to the console regardless of this option.

Choice Description
"ci"

Fail if the environment variable CI is set to a value other than 0 or false. Environments like GitHub Actions configure this automatically

"always"

Fail as long as there are warnings, even in local use

Default

"ci"

Type

ErrorHandling

repo-url-template

Use a custom link format for platforms other than GitHub.

Should be a string that contains the following placeholders that will be filled in at build time:

  • {ref} — the Git reference (tag or commit ID) resolved at build time
  • {path} — path to the linked file relative to repo root, without a leading /

For example, the following configures generated links to use GitLab's format:

repo-url-template = "https://gitlab.haskell.org/ghc/ghc/-/tree/{ref}/{path}"

Note that information such as repo owner or name will not be filled in. If URLs to your Git hosting service require such items, you should hard-code them in the pattern.

Default

None

Type

String

Known issues

Working with {{#include}}

Linking by relative paths may not make sense when the links are in files that are embedded using mdBook's {{#include}} directive.

See Working with {{#include}} for some possible workarounds.

Links in HTML (href and src) are currently neither transformed nor checked.

CHANGELOG

The format is based on Keep a Changelog.

This file is autogenerated using release-plz.

1.1.0

Features

CHANGELOG

The format is based on Keep a Changelog.

This file is autogenerated using release-plz

1.0.1 - 2025-04-08

Other

  • fix version number (#10)
  • fix issues with crates.io README (#9)

1.0.0 - 2025-04-08

Initial release.

INTERNAL: Repo README

note

This page is included only for link validation during build.

mdbookkit hero image

mdbookkit

Quality-of-life plugins for your mdBook project.

crates.io documentation MIT/Apache-2.0 licensed

Read the book

You may be looking for:

mdbook-rustdoc-link

mdbook-link-forever