Admonitions and Formatting
Admonition Blocks
Admonitions are callout boxes rendered by ExDoc. They use blockquote syntax with a special heading:
> #### Watch out for atom exhaustion {: .warning}
>
> Calling `String.to_atom/1` on user input can exhaust the atom table.
> Use `String.to_existing_atom/1` instead.
Structure
- Start with
> ####followed by the admonition title - Add
{: .class}at the end of the title line - Follow with
>blank line and>content lines
Available Classes
| Class | Use for | Rendered appearance |
|---|---|---|
.warning |
Potential pitfalls, breaking changes | Yellow/amber box |
.error |
Dangerous operations, common mistakes | Red box |
.info |
Additional context, background | Blue box |
.tip |
Best practices, performance hints | Green box |
.neutral |
General callouts without urgency | Grey box |
Examples
@moduledoc """
Manages database connections for the application.
> #### Requires database access {: .info}
>
> This module expects a running PostgreSQL instance. See the
> [setup guide](setup.md) for local development configuration.
> #### Connection pooling {: .tip}
>
> For high-throughput workloads, increase the pool size in
> `config/runtime.exs`:
>
> config :my_app, MyApp.Repo,
> pool_size: 20
> #### Do not call at compile time {: .error}
>
> Functions in this module require the application to be started.
> Calling them in module attributes or at compile time will raise.
"""
Multi-Paragraph Admonitions
Continue with > on each line:
@doc """
> #### Migration required {: .warning}
>
> After upgrading to v2.0, run the following migration:
>
> mix ecto.migrate
>
> This adds the `archived_at` column used by the new soft-delete
> feature. Existing rows will have `NULL` in this column, which
> the query functions treat as "not archived."
"""
Heading Levels
In @moduledoc and @doc, use second-level headings (##) as the highest level. First-level headings (#) are reserved for the module or function name in ExDoc output.
# GOOD
@moduledoc """
Handles webhook delivery and retry logic.
## Retry Strategy
Failed deliveries are retried with exponential backoff.
## Configuration
Set the maximum retry count in your config.
"""
# BAD - # will clash with ExDoc's page title
@moduledoc """
# Webhook Delivery
Handles webhook delivery and retry logic.
"""
Within reference documentation and extra pages, # is acceptable as a page title.
Tabbed Content
ExDoc supports tabbed content blocks using HTML comments and third-level headings:
@moduledoc """
## Installation
<!-- tabs-open -->
### Mix
Add to your `mix.exs` dependencies:
{:my_library, "~> 1.0"}
### Rebar3
Add to your `rebar.config`:
{deps, [{my_library, "1.0.0"}]}.
### Erlang.mk
Add to your `Makefile`:
dep_my_library = hex 1.0.0
<!-- tabs-close -->
"""
Rules:
- Open with
<!-- tabs-open --> - Each tab is a
###heading - Close with
<!-- tabs-close --> - Content between
###headings becomes that tab's body - Tabs work in
@moduledoc,@doc, and extra pages
Realistic Example
@doc """
Serializes a struct to a transport format.
## Examples
<!-- tabs-open -->
### JSON
iex> MyApp.Serializer.encode(%User{name: "Alice"}, :json)
{:ok, ~s({"name":"Alice"})}
### MessagePack
iex> MyApp.Serializer.encode(%User{name: "Alice"}, :msgpack)
{:ok, <<129, 164, 110, 97, 109, 101, 165, 65, 108, 105, 99, 101>>}
<!-- tabs-close -->
"""
Code Blocks
Use fenced code blocks with a language tag for syntax highlighting:
@moduledoc """
## Usage
```elixir
{:ok, conn} = MyApp.Connection.open("localhost", 5432)
MyApp.Connection.query(conn, "SELECT 1")
```
Configuration in `config/runtime.exs`:
```elixir
config :my_app, MyApp.Connection,
hostname: System.get_env("DB_HOST", "localhost"),
port: String.to_integer(System.get_env("DB_PORT", "5432"))
```
"""
For shell commands:
@moduledoc """
## Getting Started
```bash
mix deps.get
mix ecto.setup
mix phx.server
```
"""
Indented Code Blocks in Doctests
Within ## Examples sections, use four-space indentation (not fenced blocks) so that ExDoc can detect and run doctests:
@doc """
## Examples
iex> MyApp.Math.add(2, 3)
5
"""
Lists
Unordered Lists
@doc """
Supported formats:
* `:json` - JSON encoding via Jason
* `:msgpack` - MessagePack via Msgpax
* `:csv` - CSV encoding via NimbleCSV
"""
Ordered Lists
@doc """
Processing pipeline:
1. Validate input against the schema
2. Transform to internal representation
3. Persist to the database
4. Broadcast change event
"""
Nested Lists
@doc """
Options:
* `:format` - Output format
* `:json` - Default
* `:csv` - Comma-separated
* `:compress` - Whether to gzip the output
* `true` - Enable compression
* `false` - Default, no compression
"""
Tables
@moduledoc """
## HTTP Status Mapping
| Status | Atom | Description |
|--------|------|-------------|
| 200 | `:ok` | Successful request |
| 201 | `:created` | Resource created |
| 400 | `:bad_request` | Invalid input |
| 404 | `:not_found` | Resource missing |
| 422 | `:unprocessable_entity` | Validation failed |
"""
Tables must have a header row and a separator row. Alignment colons (:---, :---:, ---:) are supported.
Inline Formatting
| Syntax | Renders as | Use for |
|---|---|---|
`code` |
code |
Module names, functions, atoms, options |
**bold** |
bold | Emphasis on key terms |
*italic* |
italic | Titles, introducing terms |
[text](url) |
link | External URLs |
[`text`](`Module`) |
code link | Cross-references (see cross-references.md) |
Combining Formatting Techniques
A well-formatted @moduledoc uses several of these elements together:
defmodule MyApp.RateLimiter do
@moduledoc """
Token-bucket rate limiter backed by ETS.
Limits are configured per endpoint and enforced in the
`MyApp.Plugs.RateLimit` plug.
> #### Production configuration {: .tip}
>
> Set limits based on your capacity planning. Start conservative
> and adjust based on metrics from `MyApp.Telemetry`.
## Examples
iex> {:ok, limiter} = MyApp.RateLimiter.start_link(name: :api)
iex> MyApp.RateLimiter.check(limiter, "user:42", :search)
:allow
## Configuration
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `:window_ms` | `pos_integer()` | `60_000` | Window duration |
| `:max_requests` | `pos_integer()` | `100` | Requests per window |
| `:ban_duration_ms` | `pos_integer()` | `300_000` | Ban duration after exceeding limit |
## Architecture
<!-- tabs-open -->
### Single Node
Uses a local ETS table. Suitable for development and single-instance
deployments.
### Distributed
Wraps the ETS table with `:pg`-based synchronization. Each node
maintains its own counter and periodically reconciles with peers.
<!-- tabs-close -->
"""
end