The empty landing page kills crates
You publish a crate called super-fast-json. A developer searches crates.io, finds your crate, and clicks "Documentation". The page loads. It shows a list of functions and structs. There is no description. No examples. No context. The developer assumes the crate is abandoned or poorly maintained. They close the tab and pick a competitor. Your crate gets zero downloads.
Documentation is the storefront. If the storefront is dark, nobody walks in. In Rust, the storefront is the crate-level documentation. It lives in your code, renders automatically, and stays in sync with your API. Writing it correctly is the first step to building a crate people actually use.
Crate docs live in the code
Rust does not use separate documentation files. The documentation lives in the source code. The compiler tool rustdoc extracts comments and generates HTML. There are two comment styles. /// documents the item below it. //! documents the module containing it.
When you place //! at the top of src/lib.rs, you are writing the documentation for the crate itself. This content becomes the landing page. Users see this before they see any functions. It sets the context, shows how to use the crate, and explains the design goals.
Think of /// as the label on a jar in the pantry. It tells you what is inside that specific jar. //! is the sign on the pantry door. It tells you what kind of kitchen this is and how to navigate the shelves.
Minimal example
Create a file src/lib.rs. Add //! comments at the very top. The content is Markdown. You can use headers, lists, and code blocks.
// src/lib.rs
//! # math-utils
//!
//! A simple library for basic arithmetic operations.
//!
//! ## Features
//!
//! - Safe integer addition with overflow checking.
//! - Fast multiplication using lookup tables.
//!
//! ## Example
//!
//! ```
//! use math_utils::add;
//!
//! let result = add(2, 3);
//! assert_eq!(result, 5);
//! ```
/// Adds two integers safely.
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
Run cargo doc --open to view the rendered documentation in your browser. The --open flag launches your default browser. The page shows the Markdown from the //! comments at the top, followed by the public items like add.
How rustdoc processes your comments
When you run cargo doc, the tool rustdoc scans your source files. It parses the Abstract Syntax Tree and looks for /// and //! comments. It strips the comment syntax and treats the content as Markdown.
For //! comments at the top of lib.rs, rustdoc collects them and places the result at the very top of the generated documentation page. This is the crate root module documentation. It renders to the index.html of your docs.
Code blocks inside the comments are treated as doc tests. rustdoc extracts the code, compiles it, and runs it. If a doc test fails, the documentation build fails. This mechanism ensures your examples stay accurate. If you change the API and forget to update the example, the build breaks. You catch the error before you publish.
Doc tests are real tests
Code blocks in documentation are not just text. They are executable tests. This is a core feature of Rust. When you write an example in //!, rustdoc creates a temporary test file, injects the code, and runs it with cargo test.
Doc tests have access to your crate's dependencies. They also have access to dev-dependencies. You can use testing utilities in your examples without polluting the production dependency list.
You can run doc tests separately from unit tests. Use cargo test --doc to run only the documentation tests. This is useful in CI pipelines where you want to verify examples quickly.
Controlling test behavior with attributes
Sometimes you need to control how rustdoc handles a code block. You can add attributes after the opening triple backticks.
//! # server-lib
//!
//! A library for starting HTTP servers.
//!
//! ## Example
//!
//! This example starts a server. It blocks forever, so we use `no_run`.
//!
//! ```no_run
//! use server_lib::Server;
//!
//! let server = Server::new("0.0.0.0:8080");
//! server.start();
//! ```
//!
//! This example demonstrates error handling. It panics intentionally.
//!
//! ```should_panic
//! use server_lib::parse_config;
//!
//! let config = parse_config("invalid.toml");
//! ```
The no_run attribute tells rustdoc to compile the code but not execute it. Use this for examples that require external setup, like starting a server or reading a file that doesn't exist in the test environment. The code still compiles, so you catch type errors.
The should_panic attribute tells rustdoc to expect the code to panic. Use this to demonstrate error cases. If the code does not panic, the test fails.
Avoid the ignore attribute. ignore tells rustdoc to skip compilation and execution entirely. This hides bugs. If the example is broken, nobody notices. Use ignore only when the example is impossible to test automatically, such as when it requires hardware interaction. The community treats ignore as a technical debt marker. Prefer no_run whenever possible.
Realistic crate documentation
A production crate needs more than a greeting. It needs structure. A good crate doc includes an overview, a quick start example, a list of features, and links to important modules.
// src/lib.rs
//! # config-reader
//!
//! Reads configuration from TOML files with validation and hot reloading.
//!
//! ## Quick Start
//!
//! Add `config-reader = "0.1.0"` to your `Cargo.toml`.
//!
//! ```
//! use config_reader::Config;
//!
//! let config = Config::load("config.toml").unwrap();
//! println!("Port: {}", config.port);
//! ```
//!
//! ## Features
//!
//! - **Validation:** Ensures configuration values meet constraints.
//! - **Hot reloading:** Watches files for changes and updates config automatically.
//! - **Merging:** Supports multiple config sources with precedence rules.
//!
//! ## Design Goals
//!
//! - **Zero allocations:** The parser returns references into the input string.
//! - **Predictable speed:** Parsing time is linear relative to input size.
//! - **No std:** This crate works in `no_std` environments.
//!
//! ## Modules
//!
//! - [`parser`]: The core parsing logic.
//! - [`error`]: Error types and handling.
//! - [`watcher`]: File watching for hot reloading.
pub mod parser;
pub mod error;
pub mod watcher;
/// The main configuration struct.
pub struct Config {
pub port: u16,
}
impl Config {
/// Loads configuration from a file.
pub fn load(path: &str) -> Result<Self, error::ConfigError> {
Ok(Config { port: 80 })
}
}
The example includes a code block that compiles. It shows the import and usage. The features list highlights what makes the crate unique. The design goals set expectations for performance and compatibility. The modules section links to submodules, making navigation easier.
Convention: Include the README
A common convention in the Rust ecosystem is to include the README directly in the crate docs. This keeps the documentation in sync with the code. You can do this with include_str!.
//! # my-crate
//!
//! [](https://crates.io/crates/my-crate)
//!
//! ```
//! #[doc = include_str!("../README.md")]
//! ```
This attribute includes the README content at the crate level. It replaces manual //! comments for the main description. You still need //! for doc tests and examples that are not in the README. This approach is standard for large crates. It reduces duplication and ensures the README and docs never drift apart.
Pitfalls and errors
Confusion between //! and ///
Developers often confuse //! and ///. If you use /// at the top of lib.rs, the comment attaches to the first item in the file, not the crate. Your crate docs will be empty. The compiler does not warn you. rustdoc just renders what it finds. You'll publish a crate with a blank landing page. Use //! for crate-level docs. Use /// for items.
Broken intra-doc links
If you link to a module or item that doesn't exist, rustdoc might warn or error depending on settings. Enable #![deny(rustdoc::broken_intra_doc_links)] at the crate level to catch broken links. This turns warnings into errors. It forces you to fix links before publishing.
#![deny(rustdoc::broken_intra_doc_links)]
//! # my-crate
//!
//! See the [`parser`] module for details.
If parser is missing, the build fails with a broken link error. This keeps your documentation navigable.
Doc test failures
Doc tests run as real code. If your example references a function that doesn't exist, the docs fail to build. You'll see error[E0432]: unresolved import if you forget to import the crate. Doc tests run in a separate context. You must import the crate explicitly.
//! ```
//! use my_crate::add;
//! let result = add(2, 3);
//! ```
If you omit use my_crate::add, you get error[E0425]: cannot find value add. If you use the wrong type, you get error[E0308]: mismatched types. Fix the code in the comment. The docs are part of the contract. If the example doesn't compile, the crate is not ready.
Decision matrix
Use //! comments at the top of src/lib.rs when you want to write the crate-level documentation that appears on the landing page. Use /// comments above items like functions, structs, and modules when you want to document specific parts of your API. Use #![doc = include_str!("README.md")] when your README contains the primary documentation and you want to avoid duplicating content. Use #![doc(hidden)] when you want to exclude a module or item from the public documentation. Use no_run in doc tests when the example compiles but cannot run in the test environment. Use should_panic when the example demonstrates a panic condition. Avoid ignore unless the example is impossible to test automatically.