Configuration
One initializer drives the shared chrome. Everything that differs between two sites — brand, themes, nav, search, API examples — is config; the Shell, Sidebar, and ThemeSwitcher are identical everywhere.
DocsKit.configure#
Set it once; the shared chrome reads it everywhere.
Call DocsKit.configure with a block and set c.<knob> on the yielded DocsKit::Configuration singleton. Read any value back with DocsKit.configuration. Every knob has a sensible default, so a brand-new site works with an empty block.
Configure inside config.to_prepare so the block re-runs on every code reload — that keeps the derived nav pointing at the current registry in development.
Rails.application.config.to_prepare do
DocsKit.configure do |c|
c.brand = "Acme Docs"
c.tagline = "Everything the Acme API can do."
c.themes = %w[dark light dracula night]
c.default_theme = "dark"
c.version_badge = -> { "v#{Acme::VERSION}" }
c.nav_registries = { "Docs" => Doc, "API" => ApiDoc }
end
endBrand & themes#
Identity, the topbar, the <title>, and the theme switcher.
The one required-ish knob is brand — it names the topbar and sidebar header, and is the fallback for both title_suffix and nav_storage_key, so setting it alone gets you sensible page titles and per-site localStorage namespacing.
The theme list is the contract with your CSS: the values in themes MUST match the daisyUI @plugin "daisyui" { themes: ... } block in your Tailwind entry. A theme offered here that the build never generated is a dead switcher entry. See the Styling & CSS page for wiring that up.
| Option | Type | Default | Description |
|---|---|---|---|
brand | String | "Docs" | Topbar + sidebar heading. Fallback for title_suffix and nav_storage_key. |
tagline | String, nil | nil | One-line summary; rendered as the llms.txt blockquote. AI-index only — the chrome never shows it. |
brand_href | String | "/" | Where the topbar brand link points (e.g. "/docs" for a subpath site). |
title_suffix | String | = brand | Appended to the page <title> ("Installation · Acme"). Writer only; reader falls back to brand. |
themes | Array | %w[dark light] | ThemeSwitcher options; must match the daisyUI @plugin themes: block. |
default_theme | String | = themes.first | The data-theme applied on first paint. Writer only; reader falls back to themes.first. |
version_badge | String or callable | nil | Short badge string for the sidebar header. A callable is invoked; a plain String is used as-is; nil = no badge. |
stylesheets | Array | %w[application] | Stylesheet logical names linked in <head>, in order. |
default_group_icon | String | "file-text" | lucide icon for a nav group with no explicit icon. |
icon_library | String, nil | "lucide" | The RailsIcons library the chrome renders its own icons from. nil defers to the host default. |
nav_storage_key | String | = brand slug | Namespaces the sidebar localStorage (collapse state) so two sites on one origin don't collide. Writer only. |
page_markdown_action | Boolean | true | Show the "Markdown" masthead action (a link to the .md twin). false hides it; the .md route still works. |
on_page_default | :panel | :toggle | :sidebar | false | :panel | Default auto-TOC placement when a page doesn't set its own on_page:. |
c.version_badge = "v1.2" and c.version_badge = -> { "v#{Acme::VERSION}" } both render. A lambda is handy when the version lives in a constant that loads after the initializer.Code highlighting#
The Rouge themes and the lexer/label maps DocsUI::Code and Example read.
Syntax highlighting is inline Rouge CSS. code_theme is the base (light) theme, emitted un-scoped so it applies everywhere. Set code_theme_dark and docs-kit additionally emits that theme's CSS scoped under each shipped dark theme — CSS-only, no JS, no flash. Which themes count as dark comes from dark_themes (defaults to the 13 built-in daisyUI dark themes), intersected with your themes.
The lexer and label maps are merged over the built-ins, so you only add or override. Any of Rouge's ~200 languages already works by its own name — see the Code languages page.
| Option | Type | Default | Description |
|---|---|---|---|
code_theme | String or Class | "Rouge::Themes::Monokai" | The base (light) Rouge theme for inline highlight CSS. An unresolvable name degrades to the default. |
code_theme_dark | String, Class, nil | nil | Optional second Rouge theme, scoped under each shipped dark theme. nil = single-theme behavior. |
dark_themes | Array | 13 built-in dark themes | Which theme names are treated as dark for code_theme_dark scoping. Override for custom dark themes. |
code_lexer_aliases | Hash | {} | Friendly-name → Rouge lexer aliases, merged over built-ins ({ dockerfile: "docker" }). |
code_lexer_fallback | String | "plaintext" | The lexer used when a language can't be resolved (no highlighting, never raises). |
code_language_labels | Hash | {} | Human labels for Example language tabs, merged over built-ins ({ elixir: "Elixir" }). |
Search#
The topbar search form and the ⌘K command palette.
Search is on by default. The Shell renders the affordance when search is true and search_path is non-blank (the gate is #search_enabled?). Blank the path to disable the form without touching the toggle. The keyboard shortcuts that open the palette are configurable — mod is the platform modifier (⌘ on mac, Ctrl elsewhere), so one entry works on every OS.
See the Search page for how the index is built and served.
| Option | Type | Default | Description |
|---|---|---|---|
search | Boolean | true | Whether the topbar renders the search form + palette markup. |
search_path | String | "/docs/search" | Where the form submits (GET ?q=) and the palette fetches .json. Blank to disable. |
search_shortcuts | Array | %w[/ mod+k] | Keyboard shortcuts that open the palette. Writer only; read the parsed form via #search_shortcuts. |
API examples#
The base URL, auth line, and client tabs DocsUI::RequestExample renders.
The API-docs kit turns one request declaration into a tab per client. api_base_url is prefixed onto each snippet's path; api_auth_header is an optional example auth line merged into every snippet. api_clients overrides or extends the four shipped defaults (curl, javascript, ruby, python) — reusing a token replaces that client, a new token appends a tab.
The API reference page shows the kit rendered live.
c.api_base_url = "https://api.acme.com"
c.api_auth_header = "Authorization: Bearer sk_live_..."
c.api_clients = {
cli: DocsKit::ApiClient.new(
label: "CLI", lexer: :shell,
template: ->(req) { "acme #{req.http_method.downcase} #{req.path}" }
)
}| Option | Type | Default | Description |
|---|---|---|---|
api_base_url | String | "https://api.example.com" | Prefixed onto each RequestExample path so snippets point at a real host. |
api_auth_header | String, nil | nil | Example Authorization header line merged into every snippet. nil = no auth line. |
api_clients | Hash | 4 shipped defaults | { token => DocsKit::ApiClient } merged over curl/javascript/ruby/python. Writer only; read the merged map via #api_clients. |
AI & tooling#
The built-in MCP endpoint.
docs-kit ships an optional read-only MCP endpoint (POST /mcp, JSON-RPC exposing list_pages / get_page / search_docs over the docs registry). mcp is true by default, but the endpoint only turns on when the optional mcp gem is also loadable and the host draws the route — gate on #mcp_enabled?, not the raw toggle.
See the AI & agents page for llms.txt, the .md twins, and the MCP server.
| Option | Type | Default | Description |
|---|---|---|---|
mcp | Boolean | true | Whether the built-in MCP endpoint is active. Actually gated by #mcp_enabled? (toggle AND the mcp gem loadable). |