spec: two docs sites, one Eleventy pipeline, and a gate that holds the CLI reference to main.go
CARLOS has no docs. This designs the two that fix it — carloku.com/docs for customers doing ordinary work, carlosframework.com/docs for the platform itself — and the CI gate that stops them drifting. Structure follows rastrillo.org: a markdown corpus with a nav.json, a Go package that parses it and fails a PR when it stops describing the code, and Eleventy rendering it into a sidebar site at authoring time. Its prose is explicitly not the model; the corpora are written against the humane-docs skill instead. The platform corpus lives here so the gate can hold it to main.go's usage. The Carloku corpus lives in the carloku repo, where it is product copy with no Go to be held against, and gets the JavaScript checks only. Both sites gain a build step, which means neither may ship its repo tree again. That is the one part of this that can take the docs offline if a runbook is worked from memory, and the spec says so.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 file changed,
+241
−0
docs/superpowers/specs/2026-08-24-docs-sites-design.md+241 −0
diff --git a/docs/superpowers/specs/2026-08-24-docs-sites-design.md b/docs/superpowers/specs/2026-08-24-docs-sites-design.md| new file mode 100644 |
| index 000000000..4b26b263b |
| --- /dev/null |
| +++ b/docs/superpowers/specs/2026-08-24-docs-sites-design.md |
| @@ -0,0 +1,241 @@ |
| +# Two docs sites: carlosframework.com/docs and carloku.com/docs |
| + |
| +Date: 2026-08-24 |
| +Status: approved design, not implemented |
| + |
| +CARLOS has no documentation. There is a CLI with thirty-odd commands, a |
| +console, a bucket format, a channel ladder and a self-hosting story, and the |
| +only way to learn any of it is to read `main.go`'s usage text, ask an agent, or |
| +find the one paragraph in a spec that mentions it. This builds the two docs |
| +sites that fix that, and the CI gate that stops them going stale. |
| + |
| +Two sites, because there are two audiences and they want different things. A |
| +Carloku customer wants to know how to attach a domain. Someone evaluating |
| +CARLOS wants to know what a bucket-as-control-plane actually means before they |
| +trust it with anything. |
| + |
| +## The split |
| + |
| +`carloku.com/docs` teaches the jobs a customer does — deploy, domains, config, |
| +storage, logs, the console, their account. It is complete enough that a paying |
| +customer never has to leave the site they bought from to do ordinary work. |
| + |
| +`carlosframework.com/docs` is the platform itself — how it works, how to run |
| +your own, and the full CLI reference. Carloku links out to it for reference and |
| +internals, and for nothing else. |
| + |
| +The overlap is real and the boundary is the reader's intent, not the subject. |
| +"Attach a custom domain to your app" is a Carloku page. "How the edge resolves |
| +a hostname to a route" is a carlosframework.com page. Both mention domains. |
| + |
| +## Where things live |
| + |
| +``` |
| +carlosframework/platform |
| + docs/site/**.md, docs/site/nav.json the OSS corpus |
| + internal/docsite/ parser + the gates, Go tests |
| + |
| +carlosframework/website carlosframework.com |
| + src/docs/**.md vendored from platform, never edited here |
| + src/_data/docsnav.json vendored |
| + src/docs.njk, docs-index.njk, docsmd.njk, _includes/docs.njk |
| + hack/sync-docs.mjs, check-docs.mjs, check-anchors.mjs |
| + _site/ gitignored, shipped |
| + |
| +carlosframework/carloku carloku.com |
| + src/docs/**.md, src/_data/docsnav.json the Carloku corpus, authored here |
| + (same layouts, same hack/ scripts minus sync-docs) |
| + _site/ gitignored, shipped |
| +``` |
| + |
| +The OSS corpus lives in the platform repo so that CI can hold it to the code: |
| +a PR that adds `carlos foo` fails until `foo` appears in the CLI reference. |
| +That gate is the single most valuable thing in this design, and it only works |
| +if the markdown and the code change in the same pull request. |
| + |
| +The Carloku corpus lives in the carloku repo. It is product copy about a hosted |
| +service — sign-in, the console, limits, support — and it has no Go code to be |
| +held against. It gets the JavaScript half of the gates instead. |
| + |
| +## The pipeline |
| + |
| +Eleventy with `markdown-it` and `markdown-it-anchor`, copied from |
| +`carlosframework/rastrillo-website`, which has run this exact pipeline since |
| +rastrillo.org's docs landed. Three page templates and one layout, all of which |
| +already exist there and need skinning rather than writing: |
| + |
| +`docs.njk` paginates the corpus, one output page per markdown file, at |
| +`/docs/<slug>/`. `docs-index.njk` renders `/docs/` — the index page's own prose |
| +followed by a card per nav entry, showing its label and blurb. `docsmd.njk` |
| +publishes each page's raw markdown at `/docs/<slug>.md`, which costs nothing |
| +and is the version an agent should fetch. |
| + |
| +`_includes/docs.njk` is the shell: breadcrumb, sidebar of sections and pages, |
| +and a sub-nav of the current page's `##` headings. The sidebar is the whole |
| +navigation model — there is no search. Amadan ships client-side search for its |
| +docs; that costs served JavaScript, which both of these sites refuse, and |
| +fourteen pages with a visible sidebar do not need it. |
| + |
| +Each site gets its own `docs.css` alongside its existing `site.css`. |
| +carlosframework.com is Neo-Grotesque, platform blue, hairline rules, `--prose: |
| +40rem`. Carloku is Geist, its own accent, its own header. Same HTML, two skins; |
| +they should look like siblings and not like the same page. |
| + |
| +`hack/sync-docs.mjs` vendors the platform corpus into the website repo and |
| +records the sha it came from, so the site build never needs a Go checkout |
| +present and never fails because someone's platform clone is missing. Run it |
| +with `--check` to find out whether the vendored copy is behind. |
| + |
| +### Deploying |
| + |
| +Both sites gain a build step, so neither may ship its repo tree again. The |
| +sequence becomes the one rastrillo-website already documents: export a clean |
| +`git archive`, `npm ci && npm run check`, then `carlos ship -kind static` the |
| +`_site/` directory and promote it. `_site/` is gitignored. Both repos' AGENTS.md |
| +get the runbook, and the old "ship the tree" wording goes. |
| + |
| +This is the one genuinely disruptive part of the design. A person who has |
| +shipped carlosframework.com by muscle memory will ship a tree without a `/docs` |
| +directory in it and take the docs off the internet. The runbook change is not |
| +optional, and the ship command should be copied from AGENTS.md rather than |
| +recalled. |
| + |
| +## The gates |
| + |
| +Go, in `internal/docsite`, over the platform corpus: |
| + |
| +- **Nav and files agree both ways.** A nav entry with no file, or a file no nav |
| + entry reaches, fails. |
| +- **Internal links resolve.** Every `/docs/...` link in the corpus names a page |
| + that exists, and every `#fragment` names a heading that exists on it. |
| +- **Anchors are unique** within a page, and the Go slug rule matches |
| + `markdown-it-anchor`'s configured `slugify`. The fixture is a JSON list of |
| + heading-to-anchor pairs committed in the platform repo next to the corpus and |
| + vendored with it, so both sides assert against the same file rather than |
| + against each other's implementation. Both are lowercase alphanumerics with |
| + everything else a separator, because the library's default URI-encodes |
| + punctuation and would anchor a heading naming `carlos ship -app` differently |
| + on each side. |
| +- **Fence languages are declared.** No bare triple-backticks. |
| +- **Every top-level command is documented.** The list comes from `main.go`'s |
| + usage text; each command must appear in the CLI reference page. |
| +- **Every `carlos` invocation in a fence names a real command.** This is the |
| + gate rastrillo does not have and CARLOS needs: a renamed verb currently |
| + breaks nothing until a reader types it. |
| + |
| +JavaScript, in both site repos, run by `npm run check`: Eleventy builds, then |
| +`check-anchors.mjs` compares the slug rule to the fixture and `check-docs.mjs` |
| +re-runs the link and nav checks against the corpus present in that repo. For |
| +carloku this is the only gate, so it has to carry the link and nav checks on |
| +its own. |
| + |
| +Neither site repo has CI today and Actions minutes have been tight, so |
| +`npm run check` is a local step in the ship runbook. Adding a five-line |
| +workflow to each is cheap and I would rather have it, but it is a separate |
| +decision and the runbook does not depend on it. |
| + |
| +## carlosframework.com/docs |
| + |
| +**Start here** — what CARLOS is: one binary, a bucket and DNS, and what |
| +"bucket as control plane" buys you · installing the CLI: brew, apt, the direct |
| +download, and the Windows zip · your first deploy, end to end, against |
| +whichever CARLOS deployment you have access to. |
| + |
| +**How it works** — the bucket as control plane: what is under `apps/`, |
| +`control/` and `releases/`, and how a serial bump reaches a box in seconds · |
| +releases and channels: immutable artifacts, the `canary → edge → beta → stable` |
| +ladder, what promoting does, what rollback does, and where the semver tag gets |
| +cut · routes and instances: what each one is, which fields the instance record |
| +owns once a route is `Provisioned`, and why `carlos route -channel` refuses |
| +them · sleeping and waking: hibernation, wake on demand, and what a visitor |
| +sees during one · the app contract: what a release must do to be servable, |
| +what `carlos vet` checks, and how static, exec and unit kinds differ. |
| + |
| +**Run your own** — self-hosting: bootstrap a box, the bucket, DNS, certificates |
| +and the console · fleets: account-owned boxes that dial a console, and the |
| +token rules that come with them · signing keys: the root key and scoped keys, |
| +and why the key never touches a terminal · operations: litestream, |
| +restore-verify, `carlos status -live`, and upgrading the platform binary. |
| + |
| +**Reference** — the CLI: every command grouped by role, generated against |
| +`main.go`'s usage and held there by the gate · bucket layout: the on-disk |
| +shape, for anyone writing a tool against it. |
| + |
| +Fourteen pages plus the index. Two I considered and cut: a contributing guide, which the repo |
| +does not want yet, and an architecture-decisions page, which is what |
| +`docs/superpowers/specs/` already is. |
| + |
| +## carloku.com/docs |
| + |
| +**Start here** — the index, pointing at the existing `/get-started` rather than |
| +restating it. `/get-started` stays where it is; it does marketing work as much |
| +as documentation work, and it is current. |
| + |
| +**Running your app** — deploying: `ship`, `promote`, `deploy`, and what "live" |
| +means · channels: how a release reaches production and how to put it back · |
| +custom domains: attaching a hostname, the DNS you add, and how the certificate |
| +happens · env and secrets: the two stores, and which one a value belongs in · |
| +object storage: creating a store, granting access, rotating · logs: the merged |
| +timeline, and following it live · restarting and sleeping: cycling an app, and |
| +what hibernation means for a customer. |
| + |
| +**Your account** — the console: what each page shows · members and service |
| +credentials: who can do what, and credentials a server holds rather than a |
| +person · limits and getting help: the ship rate limit, and where to ask. |
| + |
| +Ten pages plus the index. No pricing language anywhere — billing does not |
| +exist yet, the site says nothing about it, and #226 is unmerged. |
| + |
| +## Voice |
| + |
| +Both corpora are written against the `humane-docs` skill, and the revision pass |
| +it describes is a required step, not a nicety. |
| + |
| +Worth saying plainly, because the obvious move is to imitate it: rastrillo.org |
| +is the structural model and not the prose model. Its nav blurbs are a catalogue |
| +of what that skill tells you to cut — "its two callbacks", "the three |
| +middlewares", "the six refusals", "the one seam every owned query goes |
| +through", "— and why there is no Down". Every one of those is a sentence shape |
| +repeated until it stops carrying meaning. Take the sidebar and the card index; |
| +write the sentences fresh. |
| + |
| +Each page opens with what the thing is and why you would care, in plain |
| +sentences, then the common case as one runnable example. Configuration and edge |
| +cases come after, or not at all where `-h` already covers them. |
| + |
| +## What this does not do |
| + |
| +No search. No versioned docs — the platform ships from `main` and the docs |
| +describe what is live. No API reference for the console HTTP API; it is not a |
| +public interface yet and documenting it would imply a stability promise nobody |
| +has made. No generated CLI pages: the reference page is written by hand and |
| +gated against `main.go`, because generated usage dumps are the "exhaustive |
| +option dump" failure mode and read like one. |
| + |
| +## Risks |
| + |
| +**The Carloku corpus has a weaker gate than the platform corpus.** It gets the |
| +JavaScript link and nav checks and nothing that holds it to the console's |
| +behaviour. A console change that renames a page will not fail anything. Accepted: |
| +the alternative is a Go toolchain in a static site repo. |
| + |
| +**Docs describing unmerged work.** Several things a customer would look for are |
| +built but not rolled — `carlos instances set-channel` needs a console roll, |
| +`carlos email test` is broken as shipped (#265). Every page states what is live |
| +on the day it is written, and anything unrolled is left out rather than |
| +described in the future tense. |
| + |
| +**The ship runbook change can take the docs offline** if someone ships a repo |
| +tree out of habit. Covered above; the mitigation is the runbook and nothing |
| +cleverer. |
| + |
| +## Rollout |
| + |
| +1. `internal/docsite` and its gates, against an empty-but-valid corpus. |
| +2. The platform corpus: fifteen pages, written and gated. |
| +3. The website pipeline: layouts, skin, sync script, checks; then the runbook. |
| +4. The Carloku corpus and pipeline, same shape. |
| +5. Ship both, verify by content against the live URLs. |
| + |
| +Each of the three repos lands as its own pull request. The platform PR goes |
| +first, because the website's sync script vendors from it. |