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.

config/initializers/docs_kit.rb
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 page for wiring that up.

OptionTypeDefaultDescription
brandString"Docs"Topbar + sidebar heading. Fallback for title_suffix and nav_storage_key.
taglineString, nilnilOne-line summary; rendered as the llms.txt blockquote. AI-index only — the chrome never shows it.
brand_hrefString"/"Where the topbar brand link points (e.g. "/docs" for a subpath site).
title_suffixString= brandAppended to the page <title> ("Installation · Acme"). Writer only; reader falls back to brand.
themesArray%w[dark light]ThemeSwitcher options; must match the daisyUI @plugin themes: block.
default_themeString= themes.firstThe data-theme applied on first paint. Writer only; reader falls back to themes.first.
version_badgeString or callablenilShort badge string for the sidebar header. A callable is invoked; a plain String is used as-is; nil = no badge.
stylesheetsArray%w[application]Stylesheet logical names linked in <head>, in order.
default_group_iconString"file-text"lucide icon for a nav group with no explicit icon.
icon_libraryString, nil"lucide"The RailsIcons library the chrome renders its own icons from. nil defers to the host default.
nav_storage_keyString= brand slugNamespaces the sidebar localStorage (collapse state) so two sites on one origin don't collide. Writer only.
page_markdown_actionBooleantrueShow 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:panelDefault auto-TOC placement when a page doesn't set its own on_page:.
The version badge accepts a String OR a callablec.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.

config/initializers/docs_kit.rb
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.

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
OptionTypeDefaultDescription
nav_registriesHash{}{ "Heading" => registry_class }; each registry answers .nav_items. The canonical, zero-code nav path.
navcallable-> {}Explicit nav lambda; wins over nav_registries when assigned. For bespoke interleaved nav only.
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 page.

OptionTypeDefaultDescription
code_themeString or Class"Rouge::Themes::Monokai"The base (light) Rouge theme for inline highlight CSS. An unresolvable name degrades to the default.
code_theme_darkString, Class, nilnilOptional second Rouge theme, scoped under each shipped dark theme. nil = single-theme behavior.
dark_themesArray13 built-in dark themesWhich theme names are treated as dark for code_theme_dark scoping. Override for custom dark themes.
code_lexer_aliasesHash{}Friendly-name → Rouge lexer aliases, merged over built-ins ({ dockerfile: "docker" }).
code_lexer_fallbackString"plaintext"The lexer used when a language can't be resolved (no highlighting, never raises).
code_language_labelsHash{}Human labels for Example language tabs, merged over built-ins ({ elixir: "Elixir" }).

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.

config/initializers/docs_kit.rb
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}" }
  )
}
OptionTypeDefaultDescription
api_base_urlString"https://api.example.com"Prefixed onto each RequestExample path so snippets point at a real host.
api_auth_headerString, nilnilExample Authorization header line merged into every snippet. nil = no auth line.
api_clientsHash4 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.

OptionTypeDefaultDescription
mcpBooleantrueWhether the built-in MCP endpoint is active. Actually gated by #mcp_enabled? (toggle AND the mcp gem loadable).