Authoring

Authoring pages

One command scaffolds a page — the class and its registry line. Then write content; the shell, masthead, and TOC come free.

One command#

rails g docs_kit:page writes the class AND registers it — both derived from the title.

rails g docs_kit:page "Getting Started" --group=Guide

That writes app/views/docs/pages/getting_started.rb (slug getting-started, class GettingStarted) and injects page "Getting Started", group: "Guide" into the Doc registry, so the page is routed and in the sidebar the moment you fill in #content. Every derivation is overridable:

  • --slug=auth — the URL slug,
  • --view=OauthGuide — the class basename,
  • --eyebrow="Advanced" — the eyebrow (defaults to the group),
  • --registry=Guide — a differently-named registry class.

Re-running is idempotent, and a legacy hash-entries registry is left untouched (the generator prints the entry to add by hand).

The rest of this page is what the generator produces — the shape to reach for when you hand-write or edit a page.

A page is a Phlex class#

Subclass DocsUI::Page, declare its metadata, fill in #content.

app/views/docs/pages/guide.rb
# frozen_string_literal: true

# Compact class reference — Zeitwerk resolves it through the
# directory-implied namespaces, so no nested-module ceremony.
class Views::Docs::Pages::Guide < DocsUI::Page
  title "Guide"
  eyebrow "Getting started"

  def lead = "One sentence that sits under the page title."

  def content
    DocsUI::Section("First steps", description: "What this section covers.") do
      md <<~'MD'
        Prose written as Markdown, styled with the reading rhythm.
      MD

      DocsUI::Code(<<~SOURCE, filename: "config/routes.rb")
        Rails.application.routes.draw do
          mount DocsKit::Engine, at: "/docs"
        end
      SOURCE
    end
  end
end

title names the page, eyebrow groups it above the title, and lead is the summary sentence under it. Everything you render lives in content.

The shell (topbar, sidebar, theme switcher), the page masthead, and the On this page TOC are added automatically — you only write the body.

Register the page#

One line in the Doc registry — slug and view derive from the title.

A page shows up once it has a page line in the Doc registry. slug and view derive from the title (both overridable per line), and group: sets its sidebar heading. The generator injects this line for you.

app/models/doc.rb
class Doc
  extend DocsKit::Registry
  path_prefix    "/docs"
  view_namespace "Views::Docs::Pages"

  page "Overview", group: "Getting started"
  page "Guide",    group: "Getting started"
  # overrides win: page "OAuth", group: "Guide", slug: "auth", view: "OauthGuide"
end

The sidebar derives from the registry — set c.nav_registries = { "Docs" => Doc } in the initializer and never hand-write a nav lambda again.

The sidebar only links a page whose class exists, so a page line without its class yet is a no-op — no dead links.

The building blocks#

The DocsUI kit you compose inside #content — and where each one is documented in full.

Inside #content you reach for a small kit. The everyday four — a DocsUI::Section wrapper, md for Markdown prose, DocsUI::Code for a highlighted block, and DocsUI::Callout for an aside — carry most pages. The rest are specialised; each has its own reference page rather than being re-explained here.

BlockUse forFull reference
DocsUI::Section(title)an anchored subsection with a heading (+ optional description:)Components
md(source)a block of GFM Markdown — the everyday prose helperMarkdown authoring
prose { … }hand-authored prose (p/ul/code) in a reading-rhythm wrapperComponents
DocsUI::Code(source)a Rouge-highlighted code blockCode languages
example { |ex| … }multi-language tabbed codeCode languages
DocsUI::Callout(level)note / tip / warning boxesComponents
DocsUI::Table / PropTablereference tables — headers + rows, or the args presetComponents
DocsUI::Endpoint / RequestExamplethe API-reference kit — a method+path line, client tabs, a fields tableAPI reference

The primary argument is always positional — Section("Title"), Code(source, filename:) (the filename picks the language), Header("Title") — with modifiers as keywords (description:, eyebrow:).

For the wrappers that take no argument, use the lowercase page helpers prose / example (and md for Markdown). A lowercase method takes a block without parens, so prose do … end just works. The kit forms DocsUI::Prose() / DocsUI::Example() stay valid — they only need the empty () because a bare DocsUI::Prose do parses as a constant reference (a SyntaxError).

Prose is a Markdown island: md <<~'MD' parses GFM with commonmarker and emits native Phlex nodes — tables, fenced code (routed through DocsUI::Code), links, all Phlex-escaped. Use a single-quoted heredoc so #{...} stays literal author text. See Markdown authoring for the full vocabulary.

The "On this page" TOC#

Built for you from your section headings.

Every DocsUI::Section heading becomes an entry in the automatic On this page table of contents — you never list them by hand.

Override its placement per page with on_page.

class Views::Docs::Pages::Guide < DocsUI::Page
  on_page :toggle   # :toggle | :panel | :sidebar | false
end

See the On this page reference for every mode and the site-wide default.