Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# AGENTS.md

Guidance for AI coding agents working in the **docs-kit** repository. This is the
cross-tool convention file (Claude Code, Cursor, Copilot, Aider, …). Claude Code
users also have `.claude/commands/` and `.claude/rules/`; see `CLAUDE.md` for the
full project brief. This file is the fast orientation.

docs-kit is a Rails engine that ships the shared Phlex/daisyUI **chrome** for
documentation sites — the shell, sidebar, code blocks, theme switcher, page kit —
so many docs sites look identical and are maintained in one place. The gem also
**dogfoods itself**: its own docs site lives under `docs/`.

## The two things you'll be asked to do

### A. Change the gem (a component, config knob, generator, the engine)

Read `CLAUDE.md` first — it has the layer map and the critical rules. The
non-negotiables:

- **Compose from `DocsUI::` Phlex components** — never hand-write raw daisyUI markup.
- **Site-specific values come from `DocsKit.configuration`**, never hardcoded in a
component. A new knob lands on `DocsKit::Configuration` with a sensible default
(backwards compatible).
- **The page works with JavaScript off.** The server renders it fully; the one
`docs-nav` Stimulus controller only *enhances* (collapse persistence, auto-TOC).
- **Themes offered must exist in the CSS build** — `config.themes` must match the
`@plugin "daisyui" { themes: … }` block.
- **Required setup wires into BOTH** the install generator
(`lib/generators/docs_kit/install/`) **and** the `docs-kit new` template
(`lib/docs_kit/templates/new_site.rb`) — never the README alone.
- **TDD**: write the failing spec first (`spec/docs_kit`, `spec/docs_ui`,
`spec/generators`), then the minimum code. Assert on semantics, not HTML snapshots.

### B. Write a docs page for docs-kit's own docs site (under `docs/`)

The dogfood site is itself a docs-kit site. Its registry is
`docs/app/models/doc.rb`; its pages are `docs/app/views/docs/pages/`. To document
a feature of the gem:

**1. Scaffold** (from the `docs/` app):

```bash
cd docs && bin/rails g docs_kit:page "Getting Started" --group=Guide
```

That writes `docs/app/views/docs/pages/getting_started.rb` **and** injects the
required `page "Getting Started", group: "Guide"` line into `Doc`. Overrides:
`--slug`, `--view`, `--eyebrow`, `--registry`. The registry line is **required** —
no line, no page.

**2. Write `#content` — Markdown first.** Prose is `md` with a **single-quoted**
heredoc (`<<~'MD'`) so `#{…}` stays literal (Phlex escapes author text — never
`html_safe` or interpolate). `DocsUI::Section` owns page structure and the TOC;
never use a Markdown `##` for structure. The primary arg is positional, modifiers
are keywords: `Section("Title", description:)`, `Code(source, filename:)`. For no
positional arg use the lowercase helpers `md` / `prose` / `example`. Reference
material has dedicated helpers: `DocsUI::PropTable`, `DocsUI::FieldTable`,
`DocsUI::RequestExample`, `DocsUI::Callout(:note | :tip | :warning)`.

The always-current, worked example of the whole contract is
`docs/app/views/docs/pages/authoring.rb` (rendered at `/docs/authoring`). Read it
before writing a page.

## Verify before you finish (every change)

```bash
bundle exec rspec # the suite (SimpleCov enforces 80% minimum)
bundle exec rubocop # lint — no offenses (rubocop -A to autocorrect)
bundle exec rake # both, together
```

For CSS-affecting changes (a new emitted class), rebuild in the `docs/` app:
`bun run build:css`. Never `gem push` by hand — release via `rake release[X.Y.Z]`.
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,32 @@ DocsKit.configure { |c| c.page_markdown_action = false }
to your `get "docs/:doc"` route) to enable the `.md` URLs. Sites that don't
re-run simply have no `.md` route match — HTML rendering is untouched.

## AI-assisted authoring

The install generator scaffolds the authoring contract in a **machine-readable**
form, so "document this endpoint" works out of the box — an agent doesn't have to
reverse-engineer the kit's idioms. Two files, both brand-substituted and
maintained in one place (the gem's templates):

- **`AGENTS.md`** (site root) — the cross-tool convention file (Claude Code,
Cursor, Copilot, Aider, …). A terse, example-first authoring contract: the
one-command page flow (`rails g docs_kit:page`), the `md <<~'MD'` prose idiom,
that `DocsUI::Section` owns structure and the TOC, the reference-material
helpers, and the invariants an agent must not break (the registry line is
required, JS-off must work, themes ↔ CSS build). It links the live
[Authoring pages](#the-authoring-convention) doc for depth.
- **`.claude/skills/write-docs-page/SKILL.md`** — a Claude Code skill: the recipe
for the task (gather the subject → `rails g docs_kit:page` → write sections
md-first → self-review against a checklist → run the gates). Its frontmatter
targets "write / add / update a documentation page," so Claude Code reaches for
it automatically.

If a site already has an `AGENTS.md`, the generator injects the docs-kit block
between `<!-- BEGIN docs-kit -->` / `<!-- END docs-kit -->` markers — your own
content is preserved, and a re-run only rewrites what's between the markers. An
existing skill file is never clobbered. `docs-kit new` inherits both files
automatically (it runs the install generator).

## Search

Every site gets search from the gem — no external service, no build step, no
Expand Down
69 changes: 69 additions & 0 deletions lib/generators/docs_kit/install/install_generator.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# frozen_string_literal: true

require "erb"
require "rails/generators/base"

module DocsKit
Expand All @@ -22,6 +23,12 @@ class InstallGenerator < ::Rails::Generators::Base
# NO controllers register. Eager-loading a few docs controllers is fine.
REGISTER_LINE = 'eagerLoadControllersFrom("docs_kit/controllers", application)'

# The delimiters bounding the gem-owned block inside AGENTS.md. Everything
# outside them is the user's; a re-run only rewrites what's between them.
AGENTS_BEGIN = "<!-- BEGIN docs-kit -->"
AGENTS_END = "<!-- END docs-kit -->"
AGENTS_BLOCK_RE = /#{Regexp.escape(AGENTS_BEGIN)}.*#{Regexp.escape(AGENTS_END)}/m

def create_phlex_initializer
# Phlex autoload namespaces (Views:: / Components::). Skip if the app
# already configures phlex-rails so we don't clobber a bespoke setup.
Expand Down Expand Up @@ -116,6 +123,17 @@ def wire_assets_and_package_json
add_package_json_scripts
end

# AI-authoring scaffold: an AGENTS.md (the cross-tool authoring contract)
# and a Claude Code skill (.claude/skills/write-docs-page/SKILL.md). Both
# encode how to write a docs-kit page so "document this" works out of the
# box. Idempotent: a fresh AGENTS.md is created whole; an existing one gets
# only its delimited docs-kit block replaced (the user's own content is
# never touched); the skill file is skipped if it already exists.
def create_agent_docs
write_agents_md
write_write_docs_page_skill
end

def register_stimulus_controller
index = stimulus_index_path
return say_status(:skip, "no controllers/index.js — add: #{REGISTER_LINE}", :yellow) unless index
Expand Down Expand Up @@ -145,6 +163,57 @@ def show_post_install

private

# Create AGENTS.md whole when absent; otherwise replace only the delimited
# docs-kit block (or append it if the file predates docs-kit), leaving the
# user's own content intact.
def write_agents_md
path = File.join(destination_root, "AGENTS.md")
rendered = render_template("agents_md.erb")

return create_file("AGENTS.md", rendered) unless File.exist?(path)

existing = File.read(path)
block = extract_agents_block(rendered)
updated = merge_agents_block(existing, block)
return say_status(:identical, "AGENTS.md", :blue) if updated == existing

File.write(path, updated)
say_status(:update, "AGENTS.md (docs-kit block)", :green)
end

# The BEGIN…END docs-kit block (inclusive) sliced out of the rendered
# template — the unit injected into a pre-existing AGENTS.md.
def extract_agents_block(rendered)
rendered[AGENTS_BLOCK_RE]
end

# Swap the existing delimited block for the fresh one, or append it when the
# file has none yet. Idempotent: same block in → same file out.
def merge_agents_block(existing, block)
if existing.include?(AGENTS_BEGIN) && existing.include?(AGENTS_END)
existing.sub(AGENTS_BLOCK_RE, block)
else
"#{existing.rstrip}\n\n#{block}\n"
end
end

# Write the write-docs-page Claude Code skill, unless the site already has
# one (a hand-customized skill is never clobbered).
def write_write_docs_page_skill
skill = ".claude/skills/write-docs-page/SKILL.md"
return say_status(:skip, skill, :blue) if File.exist?(File.join(destination_root, skill))

create_file skill, render_template("skill.md.erb")
end

# Render an ERB template from source_root against the generator binding, so
# helpers like app_brand resolve — used where we need the rendered string in
# memory (block extraction/merge) rather than Thor's file-to-file `template`.
def render_template(name)
source = File.read(File.join(self.class.source_root, name))
ERB.new(source, trim_mode: "-").result(binding)
end

def add_package_json_scripts
pkg = File.join(destination_root, "package.json")
return create_file("package.json", package_json_stub) unless File.exist?(pkg)
Expand Down
100 changes: 100 additions & 0 deletions lib/generators/docs_kit/install/templates/agents_md.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# AGENTS.md

Guidance for AI coding agents working in this repository. `AGENTS.md` is the
cross-tool convention (Claude Code, Cursor, Copilot, Aider, …); Claude Code also
reads it through the bundled `write-docs-page` skill. Edit freely — a
`docs_kit:install` re-run only touches the delimited block below.

<!-- BEGIN docs-kit -->
## Writing docs pages (docs-kit)

<%= app_brand %> is a [docs-kit](https://github.com/mhenrixon/docs-kit) site: a
Phlex/daisyUI chrome where **every page is a `DocsUI::Page` subclass** and the
sidebar, TOC, search, and Markdown twin come free. To document something, you
scaffold a page, then write its `#content`. Never hand-write HTML or daisyUI
markup — compose the kit's `DocsUI::` helpers.

### 1. Scaffold the page (one command)

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

That writes `app/views/docs/pages/getting_started.rb` **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`. Overrides:
`--slug=auth`, `--view=OauthGuide`, `--eyebrow="Advanced"`, `--registry=Guide`.
Re-running is idempotent.

> The registry line is **required** — a page with no `page "…"` line in
> `app/models/doc.rb` is not routed and not in the nav. The generator adds it;
> if you hand-write a page, add the line yourself.

### 2. Write `#content` — Markdown first

Prose is `md` with a **single-quoted** heredoc (`<<~'MD'`) so `#{…}` stays
literal (Phlex escapes author text — never `html_safe` or interpolate):

```ruby
class Views::Docs::Pages::Guide < DocsUI::Page
title "Guide"
eyebrow "Getting started"

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

def content
DocsUI::Section("First steps", description: "What this covers.") do
md <<~'MD'
Prose as **Markdown** — lists, `inline code`, links, GFM tables, and
fenced ```ruby``` blocks all render styled. Use Markdown `###` only for
sub-headings *inside* a Section.
MD

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

### The authoring contract

- **`DocsUI::Section` owns page structure and the "On this page" TOC.** One
Section per part of the page; each heading becomes a TOC entry. **Never** use a
Markdown `##` for page structure — only for sub-headings inside a Section.
- **The primary argument is positional; modifiers are keywords.**
`Section("Title", description:)`, `Code(source, filename:)`,
`Header("Title", eyebrow:)`.
- **Wrappers that take no positional arg use lowercase page helpers** so a block
needs no parens: `md <<~'MD' … MD`, `prose { … }`, `example { |ex| … }`. (A bare
`DocsUI::Prose do` is a Ruby SyntaxError; the helpers sidestep it.)
- **Reference material has dedicated helpers** — reach for these before prose:
`DocsUI::PropTable`, `DocsUI::FieldTable`, `DocsUI::RequestExample`,
`DocsUI::Callout(:note | :tip | :warning)`.

### Invariants — do not break

- **The registry line is required** (see above) — no line, no page.
- **The page must work with JavaScript off.** The server renders it fully;
the one `docs-nav` controller only *enhances*. Never require JS to read a page.
- **Themes offered must exist in the CSS build** — `c.themes` in
`config/initializers/docs_kit.rb` must match the `@plugin "daisyui" { themes: … }`
block in `app/assets/stylesheets/application.tailwind.css`. Don't add one
without the other.
- **No inline `rubocop:disable`** to force layout — write idiomatic Ruby the
site's cops accept.

### 3. Verify before you finish

```bash
bundle exec rspec && bundle exec rubocop # tests + lint must pass
bun run build:css # if you added classes the CSS scans
```

Then render the page locally (`bin/dev`, open `/docs/<slug>`) and confirm it
reads correctly — with JavaScript off, too.

**Depth:** the live [Authoring pages](/docs/authoring) doc is the full,
always-current version of this contract. When in doubt, read it.
<!-- END docs-kit -->
81 changes: 81 additions & 0 deletions lib/generators/docs_kit/install/templates/skill.md.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
---
name: write-docs-page
description: "Write, add, or update a documentation page in this docs-kit site. Use when asked to document a feature, endpoint, class, or workflow, or to add or edit a page under app/views/docs/pages/. Scaffolds with `rails g docs_kit:page`, writes Markdown-first content, and runs the verification gates."
---

# Write a docs page

This is a [docs-kit](https://github.com/mhenrixon/docs-kit) site (<%= app_brand %>).
Every page is a `DocsUI::Page` subclass; the shell, sidebar, "On this page" TOC,
search, and the `.md` twin all come free. Your job is to scaffold a page and
write its `#content` — never hand-write HTML or daisyUI markup.

The full authoring contract is in the repo's `AGENTS.md`. This is the recipe.

## 1. Gather the subject

Identify exactly what to document (the code, endpoint, or workflow) and where it
belongs in the sidebar (the `--group`). Read the relevant source first — do not
invent behavior. If the subject is an HTTP endpoint, prefer `DocsUI::RequestExample`
/ `DocsUI::FieldTable`; if it's a Ruby API, prefer `DocsUI::PropTable`.

## 2. Scaffold (one command)

```bash
rails g docs_kit:page "Page Title" --group=Guide
```

This writes `app/views/docs/pages/<slug>.rb` **and** injects the required
`page "…"` registry line into `app/models/doc.rb`. Overrides: `--slug`, `--view`,
`--eyebrow`, `--registry`. If it reports a legacy `entries [...]` registry, add
the printed line by hand.

## 3. Write `#content` — Markdown first

- Set `title`, `eyebrow`, and a one-sentence `lead`.
- One `DocsUI::Section("…")` per part of the page — **Sections own structure and
the TOC.** Never use a Markdown `##` for page structure.
- Prose is `md <<~'MD' … MD` — a **single-quoted** heredoc (no escaping, no
interpolation; Phlex escapes author text). Markdown `###` is only for
sub-headings inside a Section.
- Reference material: `DocsUI::PropTable`, `DocsUI::FieldTable`,
`DocsUI::RequestExample`, `DocsUI::Code(source, filename:)`,
`DocsUI::Callout(:note | :tip | :warning)`.

```ruby
class Views::Docs::Pages::PageTitle < DocsUI::Page
title "Page Title"
eyebrow "Guide"

def lead = "One sentence describing the page."

def content
DocsUI::Section("Overview", description: "What this covers.") do
md <<~'MD'
Prose as **Markdown**. Fenced ```ruby``` blocks highlight; `inline code`,
lists, links, and GFM tables all render styled.
MD
end
end
end
```

## 4. Self-review against the checklist

- [ ] The `page "…"` registry line exists (the generator adds it).
- [ ] Structure is `DocsUI::Section`s, not Markdown `##` headings.
- [ ] Prose uses `md <<~'MD'` (single-quoted); no `html_safe`, no `raw`.
- [ ] No hand-written HTML/daisyUI markup, no per-feature Stimulus controller.
- [ ] Reads correctly with JavaScript off (the server renders it fully).
- [ ] Any new theme is in both `c.themes` and the Tailwind `@plugin` block.
- [ ] No inline `rubocop:disable` to force layout.

## 5. Run the gates

```bash
bundle exec rspec && bundle exec rubocop
bun run build:css # only if you added classes the CSS must scan
```

Then render locally (`bin/dev`, open `/docs/<slug>`) and confirm it reads well.
For depth on any idiom, read the live [Authoring pages](/docs/authoring) doc.
Loading
Loading