Getting started

# 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.

```ruby
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
end
```

## Brand & 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](https://docs-kit.zoolutions.llc/docs/styling) 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:. |

> **Note:** The version badge accepts a **String OR a callable** — `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.

## The sidebar nav

Registries are the canonical path; a nav lambda is the escape hatch.

The common case is `nav_registries` — an ordered `{ "Heading" => registry_class }` map. Each registry answers `.nav_items` (a `DocsKit::Registry` method returning `{ group => [NavItem] }`), and the whole sidebar derives from it with zero site nav code. A heading whose registry has no authored pages is dropped, so no empty group renders. The registry's groups are the top level of the menu; the heading itself only renders — as a static label — when several headings are registered, so a one-registry site gets no redundant top fold.

```ruby
c.nav_registries = { "Docs" => Doc, "API" => ApiDoc }
```

Reach for the explicit `nav` lambda only for bespoke nav — interleaving multiple registries, or hand-built subgroups. It must return an ordered `{ "Heading" => { "Subgroup" => [items] } }` Hash where each item responds to `#href`, `#label`, and optional `#icon`.

```ruby
c.nav = lambda do
  grouped = Doc.all.group_by(&:group).transform_values do |docs|
    docs.map { |d| DocsKit::NavItem.new(href: "/docs/#{d.slug}", label: d.title) }
  end
  {
    "Getting started" => { "Basics" => grouped.fetch("basics", []) },
    "Reference"       => { "API" => grouped.fetch("api", []) }
  }
end
```

| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `nav_registries` | Hash | {} | { "Heading" => registry_class }; each registry answers .nav_items. The canonical, zero-code nav path. |
| `nav` | callable | -> {} | Explicit nav lambda; wins over nav_registries when assigned. For bespoke interleaved nav only. |

> **Warning:** Assigning `nav` to **any** value — even `-> { {} }` — marks it explicit and stops derivation from `nav_registries`. If you set an empty nav lambda while relying on registries, you get an empty sidebar. Leave `nav` unset unless you truly need the escape hatch.

## 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](https://docs-kit.zoolutions.llc/docs/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](https://docs-kit.zoolutions.llc/docs/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](https://docs-kit.zoolutions.llc/docs/api) page shows the kit rendered live.

```ruby
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](https://docs-kit.zoolutions.llc/docs/ai) 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). |