
Exdoc Config
- 71 installs
- 74 repo stars
- Updated July 21, 2026
- existential-birds/beagle
Helps with ai & agent building tasks.
About
exdoc-config is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- exdoc-config
- AI & Agent Building
- AI-coding skill
Exdoc Config by the numbers
- 71 all-time installs (skills.sh)
- Ranked #5,651 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
npx skills add https://github.com/existential-birds/beagle --skill exdoc-configAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 71 |
|---|---|
| repo stars | ★ 74 |
| Last updated | July 21, 2026 |
| Repository | existential-birds/beagle ↗ |
What it does
Helps with ai & agent building tasks.
Files
ExDoc Configuration
Quick Reference
| Topic | Reference |
|---|---|
| Markdown, cheatsheets (.cheatmd), livebooks (.livemd) | references/extras-formats.md |
| Custom head/body tags, syntax highlighting, nesting, annotations | references/advanced-config.md |
Gates
Use this sequence before claiming ExDoc is wired correctly or that docs build:
1. Dependencies resolved — Run mix deps.get from the project root. Pass: exit code 0, and mix.exs includes ex_doc as in Dependency Setup (or the project’s equivalent dev-only docs dep). 2. Extra paths real — For every path string in extras/0 (and in groups_for_extras/0 if used), confirm that path exists in the repo or you have just created that file. Pass: no stale or typo paths remain when you run mix docs. 3. Docs build — Run mix docs. Pass: exit code 0, and the HTML entry exists at <output>/index.html (default <project>/doc/index.html; use docs: [output: ...] if you changed output).
For cheatsheets, livebooks, or custom head/body assets, follow the same “path exists before listing” rule; see When to Load References.
Dependency Setup
Add ExDoc to mix.exs deps:
defp deps do
[
{:ex_doc, "~> 0.34", only: :dev, runtime: false}
]
endProject Configuration
Configure your project/0 function in mix.exs:
def project do
[
app: :weather_station,
version: "0.1.0",
elixir: "~> 1.17",
start_permanent: Mix.env() == :prod,
deps: deps(),
# ExDoc
name: "WeatherStation",
source_url: "https://github.com/acme/weather_station",
homepage_url: "https://acme.github.io/weather_station",
docs: docs()
]
endThe docs/0 Function
Define a private docs/0 function to keep project config clean:
defp docs do
[
main: "readme",
logo: "priv/static/images/logo.png",
output: "doc",
formatters: ["html", "epub"],
source_ref: "v#{@version}",
extras: extras(),
groups_for_modules: groups_for_modules(),
groups_for_extras: groups_for_extras()
]
endKey Options
| Option | Default | Description |
|---|---|---|
main | "api-reference" | Landing page module name or extra filename (without extension) |
logo | nil | Path to logo image displayed in sidebar |
output | "doc" | Output directory for generated docs |
formatters | ["html"] | List of output formats ("html", "epub") |
source_ref | "main" | Git ref used for "View Source" links |
assets | nil | Map of source directory to target directory for static assets |
deps | [] | Links to dependency documentation |
Setting the Landing Page
The main option controls what users see first:
# Use the README as the landing page (most common)
docs: [main: "readme"]
# Use a specific module as the landing page
docs: [main: "WeatherStation"]
# Use a custom guide
docs: [main: "getting-started"]The value matches the extra filename without its extension, or a module name.
Extras
Extras are additional pages beyond the API reference. Add them as a list of file paths:
defp extras do
[
"README.md",
"CHANGELOG.md",
"LICENSE.md",
"guides/getting-started.md",
"guides/configuration.md",
"guides/deployment.md",
"cheatsheets/query-syntax.cheatmd",
"notebooks/data-pipeline.livemd"
]
endControlling Extra Titles
By default, ExDoc uses the first h1 heading as the title. Override with a keyword tuple:
defp extras do
[
{"README.md", [title: "Overview"]},
{"CHANGELOG.md", [title: "Changelog"]},
"guides/getting-started.md"
]
endOrdering
Extras appear in the sidebar in the order listed. Put the most important pages first:
defp extras do
[
"README.md",
"guides/getting-started.md",
"guides/architecture.md",
"guides/deployment.md",
"CHANGELOG.md"
]
endGrouping
Grouping Modules
Organize modules into logical sections in the sidebar:
defp groups_for_modules do
[
"Sensors": [
WeatherStation.Sensor,
WeatherStation.Sensor.Temperature,
WeatherStation.Sensor.Humidity,
WeatherStation.Sensor.Pressure
],
"Data Processing": [
WeatherStation.Pipeline,
WeatherStation.Pipeline.Transform,
WeatherStation.Pipeline.Aggregate
],
"Storage": [
WeatherStation.Repo,
WeatherStation.Schema.Reading,
WeatherStation.Schema.Station
]
]
endUse regex to group by pattern:
defp groups_for_modules do
[
"Sensors": [~r/Sensor/],
"Schemas": [~r/Schema/],
"Pipeline": [~r/Pipeline/]
]
endModules not matching any group appear under a default "Modules" heading.
Grouping Functions
Group functions within a module using groups_for_docs:
defp docs do
[
groups_for_docs: [
"Lifecycle": &(&1[:section] == :lifecycle),
"Queries": &(&1[:section] == :queries),
"Mutations": &(&1[:section] == :mutations)
]
]
endTag functions in your module with @doc metadata:
@doc section: :lifecycle
def start_link(opts), do: GenServer.start_link(__MODULE__, opts)
@doc section: :queries
def get_reading(station_id), do: Repo.get(Reading, station_id)Grouping Extras
Organize guides, cheatsheets, and notebooks in the sidebar:
defp groups_for_extras do
[
"Guides": [
"guides/getting-started.md",
"guides/configuration.md",
"guides/deployment.md"
],
"Cheatsheets": [
"cheatsheets/query-syntax.cheatmd",
"cheatsheets/ecto-types.cheatmd"
],
"Tutorials": [
"notebooks/data-pipeline.livemd",
"notebooks/sensor-setup.livemd"
]
]
endUse glob patterns for convenience:
defp groups_for_extras do
[
"Guides": ~r/guides\/.*/,
"Cheatsheets": ~r/cheatsheets\/.*/,
"Tutorials": ~r/notebooks\/.*/
]
endDependency Doc Links
Link to documentation for your dependencies so ExDoc cross-references resolve:
defp docs do
[
deps: [
ecto: "https://hexdocs.pm/ecto",
phoenix: "https://hexdocs.pm/phoenix",
plug: "https://hexdocs.pm/plug"
]
]
endThis enables references like t:Ecto.Schema.t/0 to link directly to the dependency docs.
Generating Docs
# Generate HTML docs
mix docs
# Open in browser
open doc/index.htmlComplete mix.exs Example
defmodule WeatherStation.MixProject do
use Mix.Project
@version "1.3.0"
@source_url "https://github.com/acme/weather_station"
def project do
[
app: :weather_station,
version: @version,
elixir: "~> 1.17",
start_permanent: Mix.env() == :prod,
deps: deps(),
name: "WeatherStation",
source_url: @source_url,
homepage_url: "https://acme.github.io/weather_station",
docs: docs()
]
end
defp docs do
[
main: "readme",
logo: "priv/static/images/logo.png",
source_ref: "v#{@version}",
formatters: ["html"],
extras: extras(),
groups_for_modules: groups_for_modules(),
groups_for_extras: groups_for_extras(),
deps: [
ecto: "https://hexdocs.pm/ecto",
phoenix: "https://hexdocs.pm/phoenix"
]
]
end
defp extras do
[
"README.md",
"CHANGELOG.md",
"guides/getting-started.md",
"guides/configuration.md",
"guides/deployment.md",
"cheatsheets/query-syntax.cheatmd",
"notebooks/data-pipeline.livemd"
]
end
defp groups_for_modules do
[
"Sensors": [~r/Sensor/],
"Data Processing": [~r/Pipeline/],
"Storage": [~r/Schema|Repo/]
]
end
defp groups_for_extras do
[
"Guides": ~r/guides\/.*/,
"Cheatsheets": ~r/cheatsheets\/.*/,
"Tutorials": ~r/notebooks\/.*/
]
end
defp deps do
[
{:phoenix, "~> 1.7"},
{:ecto_sql, "~> 3.12"},
{:ex_doc, "~> 0.34", only: :dev, runtime: false}
]
end
endWhen to Load References
- Setting up cheatsheets or livebooks as extras -> extras-formats.md
- Injecting custom CSS/JS, configuring syntax highlighting, or tuning module nesting -> advanced-config.md
Advanced Configuration
Injecting Custom HTML
before_closing_head_tag
Inject CSS or meta tags into the <head> section. Accepts a function that receives the format (:html or :epub):
defp docs do
[
before_closing_head_tag: &before_closing_head_tag/1
]
end
defp before_closing_head_tag(:html) do
"""
<style>
.content-inner {
max-width: 900px;
}
.deprecated .detail-header {
background-color: #fff3cd;
}
</style>
"""
end
defp before_closing_head_tag(:epub), do: ""before_closing_body_tag
Inject JavaScript before the closing </body> tag. Useful for analytics, custom interactions, or additional syntax highlighting:
defp docs do
[
before_closing_body_tag: &before_closing_body_tag/1
]
end
defp before_closing_body_tag(:html) do
"""
<script>
document.querySelectorAll('pre code').forEach((block) => {
block.addEventListener('click', () => {
navigator.clipboard.writeText(block.innerText);
});
});
</script>
"""
end
defp before_closing_body_tag(:epub), do: ""Format-Specific Injection
Both hooks receive the format atom, allowing different content per output:
defp before_closing_head_tag(:html) do
"""
<link rel="stylesheet" href="assets/custom.css">
"""
end
defp before_closing_head_tag(:epub) do
"""
<style>
/* epub-specific overrides */
.content { font-size: 14pt; }
</style>
"""
endSyntax Highlighting
ExDoc uses the Makeup library for syntax highlighting. Elixir and Erlang are included by default.
Adding Language Support
Add Makeup lexer packages to your deps for additional languages:
defp deps do
[
{:ex_doc, "~> 0.34", only: :dev, runtime: false},
{:makeup_html, ">= 0.0.0", only: :dev, runtime: false},
{:makeup_json, ">= 0.0.0", only: :dev, runtime: false},
{:makeup_diff, ">= 0.0.0", only: :dev, runtime: false},
{:makeup_sql, ">= 0.0.0", only: :dev, runtime: false}
]
endAvailable Makeup lexers:
| Package | Languages |
|---|---|
makeup_elixir | Elixir (included by default) |
makeup_erlang | Erlang (included by default) |
makeup_html | HTML |
makeup_json | JSON |
makeup_diff | Diff/patch |
makeup_sql | SQL |
makeup_eex | EEx templates |
makeup_c | C |
makeup_rust | Rust |
Languages without a Makeup lexer fall back to plain text rendering.
Module Nesting
Automatic Nesting
ExDoc automatically nests modules based on their naming hierarchy. For example:
WeatherStation.Sensorappears as a top-level moduleWeatherStation.Sensor.Temperaturenests underWeatherStation.SensorWeatherStation.Sensor.Temperature.Calibrationnests underWeatherStation.Sensor.Temperature
This creates a collapsible tree in the sidebar.
nest_modules_by_prefix
Control which prefixes trigger nesting. By default, ExDoc nests all modules. Use nest_modules_by_prefix to restrict it:
defp docs do
[
nest_modules_by_prefix: [
WeatherStation.Sensor,
WeatherStation.Pipeline
]
]
endWith this config:
WeatherStation.Sensor.Temperaturenests underWeatherStation.SensorWeatherStation.Pipeline.Transformnests underWeatherStation.PipelineWeatherStation.Schema.Readingstays at the top level (prefix not listed)
Set to an empty list to disable all nesting:
nest_modules_by_prefix: []The api-reference Page
ExDoc generates an api-reference page by default, listing all documented modules. This is the default landing page unless you set main to something else.
The page groups modules according to groups_for_modules and shows a brief description from each module's @moduledoc.
To make it the explicit landing page:
docs: [main: "api-reference"]Suppressing Warnings
skip_undefined_reference_warnings_on
Suppress warnings about undefined references in specific pages. Useful for changelogs and guides that mention modules or functions from other projects:
defp docs do
[
skip_undefined_reference_warnings_on: [
"CHANGELOG.md",
"guides/migration-from-v1.md"
]
]
endskip_code_autolink_to
Prevent ExDoc from auto-linking specific terms that look like module or function references but are not:
defp docs do
[
skip_code_autolink_to: [
"Ecto.Schema",
"Phoenix.Controller",
"mix phx.gen.schema"
]
]
endUse this when you reference external modules that are not in your deps, or when backticked terms like ` Config ` should not link to a module.
Annotations
Add version or status annotations that appear next to module names in the sidebar:
defp docs do
[
annotations_for_docs: fn metadata ->
cond do
metadata[:since] -> "since #{metadata[:since]}"
metadata[:deprecated] -> "deprecated"
true -> nil
end
end
]
endTag functions in your code:
@doc since: "1.2.0"
def stream_readings(station_id, opts \\ []) do
# ...
end
@doc deprecated: "Use stream_readings/2 instead"
def poll_readings(station_id) do
# ...
endStatic Assets
Include images, CSS, or other static files in your generated docs:
defp docs do
[
assets: %{
"guides/images" => "images",
"guides/diagrams" => "diagrams"
}
]
endThis copies files from the source directory (key) to the target directory (value) inside the generated docs. Reference them in your extras:
## System Architecture

## Sensor Placement
Asset Path Rules
- Source paths are relative to the project root
- Target paths are relative to the doc output directory
- Files are copied as-is (no processing)
- Use consistent directory names between source and your markdown references
Complete Advanced Example
defp docs do
[
main: "readme",
logo: "priv/static/images/logo.png",
source_ref: "v#{@version}",
extras: extras(),
groups_for_modules: groups_for_modules(),
groups_for_extras: groups_for_extras(),
nest_modules_by_prefix: [
WeatherStation.Sensor,
WeatherStation.Pipeline,
WeatherStation.Schema
],
skip_undefined_reference_warnings_on: ["CHANGELOG.md"],
skip_code_autolink_to: ["Config"],
assets: %{"guides/images" => "images"},
before_closing_head_tag: &before_closing_head_tag/1,
before_closing_body_tag: &before_closing_body_tag/1,
deps: [
ecto: "https://hexdocs.pm/ecto",
phoenix: "https://hexdocs.pm/phoenix"
]
]
endExtras Formats
ExDoc supports three formats for extra pages: Markdown, Cheatsheets, and Livebooks.
Markdown (.md)
Standard Markdown files for long-form documentation. Use for conceptual guides, architecture overviews, getting started guides, and changelogs.
Structure
# Getting Started with WeatherStation
## Prerequisites
- Elixir 1.17+
- PostgreSQL 15+
## Installation
Add `weather_station` to your dependencies:
{:weather_station, "~> 1.3"}
## Configuration
Configure your sensor endpoints in `config/config.exs`:
config :weather_station,
sensors: ["temp_01", "humidity_01"],
poll_interval: :timer.seconds(30)Tips
- Use the first
h1heading as the page title (ExDoc picks it up automatically) - Fenced code blocks with language tags get syntax highlighting
- Relative links between extras work:
[Configuration](configuration.md) - Link to modules with backticks: `
WeatherStation.Sensor` - Link to functions: `
WeatherStation.Sensor.read/1`
Cheatsheets (.cheatmd)
Quick-reference cards rendered in a visual card layout. Use for syntax summaries, common patterns, and lookup tables.
Basic Structure
A cheatsheet uses specific heading levels to create the card layout:
h1(#) -- Page titleh2(##) -- Section heading (rendered as a card group)h3(###) -- Individual card within a section- Content under each
h3becomes the card body
Example Cheatsheet
# Ecto Query Syntax
## Basic Queries
### Select all records
Repo.all(User)
### Filter with where
from u in User, where: u.active == true, select: u
### Limit and offset
from u in User, limit: 10, offset: 20
## Associations
### Preload associations
Repo.all(User) |> Repo.preload(:posts)
Or in the query
from u in User, preload: [:posts]
### Join and select
from u in User, join: p in assoc(u, :posts), where: p.published == true, select: {u.name, p.title}
Card Layout Rules
- Each
h2section becomes a visually distinct group with a header - Each
h3under anh2becomes a card in that group - Code blocks inside cards are rendered as styled examples
- Keep card content concise -- cheatsheets are for quick scanning
- Plain text under
h2(before anyh3) appears as an intro for that section - Content before the first
h2appears as a page introduction - Cheatsheets support only a limited subset of Markdown -- headings, plain text, fenced code blocks, and inline attributes
Layout Attributes
ExDoc provides inline attributes on h2 and h3 headers to control card layout:
Column layouts (on `h2` sections):
{: .col-2}-- Two equal columns{: .col-3}-- Three equal columns{: .col-2-left}-- Two columns, left column wider
List layouts (on `h3` cards):
{: .list-4}-- Four-column list{: .list-6}-- Six-column list
Width control (on `h2` sections):
{: .width-50}-- Half-width section
## API
{: .col-2}
### Functions
{: .list-6}
* `foo/1`
* `bar/2`
* `baz/3`When to Use Cheatsheets
- API quick reference (common function calls)
- Syntax summaries (Ecto queries, Phoenix routes)
- Configuration option lookup tables
- Migration from another library (side-by-side comparisons)
Livebooks (.livemd)
Interactive Livebook notebooks that render as rich documentation pages. Use for tutorials, data exploration walkthroughs, and interactive examples.
How They Render in ExDoc
ExDoc renders .livemd files as static documentation pages with:
- Code cells displayed as syntax-highlighted code blocks
- Markdown cells rendered normally
- A "Run in Livebook" badge linking to the raw
.livemdfile so readers can open it interactively - Mermaid diagrams rendered if present
- Output cells are not included -- only source and markdown cells render
Example Livebook Structure
# Data Pipeline Tutorial
## Setup
Mix.install([ {:weather_station, "~> 1.3"}, {:kino, "~> 0.14"} ])
## Connecting to Sensors
First, start the sensor supervisor:
{:ok, pid} = WeatherStation.Sensor.Supervisor.start_link( sensors: ["temp_01", "humidity_01"] )
## Reading Data
Poll the sensors and inspect the results:
readings = WeatherStation.Sensor.read_all() Kino.DataTable.new(readings)
Best Practices for Livebooks in ExDoc
- Include
Mix.install/1in the first code cell so readers can run the notebook standalone - Write Livebooks that make sense both as static docs and interactive sessions
- Use Markdown cells to explain what each code cell does
- Keep notebooks focused on a single workflow or concept
- Place livebooks in a dedicated directory (e.g.,
notebooks/orlivebooks/)
Organizing Extras
Directory Conventions
project/
README.md
CHANGELOG.md
guides/
getting-started.md
configuration.md
deployment.md
architecture.md
cheatsheets/
query-syntax.cheatmd
router-helpers.cheatmd
notebooks/
data-pipeline.livemd
sensor-setup.livemdOrdering in Sidebar
Extras appear in the order they are listed in the extras option. Group related pages together:
defp extras do
[
"README.md",
"guides/getting-started.md",
"guides/configuration.md",
"guides/architecture.md",
"guides/deployment.md",
"cheatsheets/query-syntax.cheatmd",
"cheatsheets/router-helpers.cheatmd",
"notebooks/data-pipeline.livemd",
"CHANGELOG.md"
]
endNaming Conventions
- Use kebab-case for filenames:
getting-started.md, notGetting Started.md - The first
h1heading becomes the sidebar title (override with the keyword tuple form) - Keep filenames short -- they become part of the URL in hosted docs
- Prefix with numbers if you need explicit ordering without
groups_for_extras:01-getting-started.md
Grouping with groups_for_extras
Combine ordering with grouping for the best sidebar organization:
defp groups_for_extras do
[
"Introduction": [
"README.md"
],
"Guides": [
"guides/getting-started.md",
"guides/configuration.md",
"guides/architecture.md",
"guides/deployment.md"
],
"Cheatsheets": [
"cheatsheets/query-syntax.cheatmd",
"cheatsheets/router-helpers.cheatmd"
],
"Tutorials": [
"notebooks/data-pipeline.livemd",
"notebooks/sensor-setup.livemd"
]
]
endExtras not matching any group appear in a default section at the bottom of the sidebar.