From 4b2070ca10a4a10fe6f7dc8e4bb3875ed76dff42 Mon Sep 17 00:00:00 2001 From: Adnaan Badr Date: Sun, 9 Aug 2026 20:59:17 +0000 Subject: [PATCH 1/4] docs: write down the voice, and fix the pages that lost it worst MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The landing copy has been rewritten four times (#65, #83, #135, #136) and drifted back to the same flat register every time, because nothing recorded what the target was. VOICE.md is that record. An audit of the 67 docs-native pages found two hand-written voices, not one. The ten pages under content/recipes/apps/ were ported from example READMEs and then cut loose: 9 exclamation marks, 14 emoji and 36 Title Case headings, against 2, 5 and 7 across the other 53 files. Everything else was already restrained — zero occurrences of powerful, effortless, robust, out of the box. The marketing register was also producing wrong claims. chat.md said "Zero manual broadcasting code required!" while examples/chat/main.go has an explicit Subscribe and three explicit Publish calls. A reader who believed it would wonder why their own app doesn't sync. Also in here: - scripts/voice-check.sh, a ratchet. Ceilings are today's counts, so the build fails when a number goes up. Bring one down, lower the ceiling in the same commit. It will not catch a page that is merely flat; most of VOICE.md is not checkable, which is why it is written down. - source-of-truth.md claimed recipes/apps/* was mirrored from livetemplate/examples. It hasn't been since the consolidation, and source-of-truth.yaml said so. That stale row is why those ten pages went unmaintained. Verified: tinkerdown validate 98/98, e2e green on :8084, sweep 194/196 (the two are the pre-existing 4.9 MB large-table timeouts), and the rewritten copy read back out of a real browser on all ten pages. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_0166MK1arBYbVZq6wfm8EsQZ --- CLAUDE.md | 44 +++++++++ README.md | 10 +- VOICE.md | 111 ++++++++++++++++++++++ content/_meta/source-of-truth.md | 29 +++--- content/changelog.md | 5 +- content/getting-started/introduction.md | 11 ++- content/getting-started/mental-model.md | 6 +- content/getting-started/your-first-app.md | 11 ++- content/index.md | 2 +- content/recipes/apps/chat.md | 65 ++++++------- content/recipes/apps/counter.md | 2 +- content/recipes/index.md | 3 +- scripts/voice-check.sh | 96 +++++++++++++++++++ 13 files changed, 326 insertions(+), 69 deletions(-) create mode 100644 CLAUDE.md create mode 100644 VOICE.md create mode 100755 scripts/voice-check.sh diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..e4436d9 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,44 @@ +# Working in this repo + +## Before writing any prose, read `VOICE.md` + +Every rewrite of the landing page so far has drifted back to the same flat +register (#65, #83, #135, #136). `VOICE.md` is the spec that exists to stop the +fifth. It covers the register, the banned vocabulary, and the two constructions +the repo owner has called out by name: **claudisms** and **rhetorical +reversals**. It applies to PR descriptions too. + +`scripts/voice-check.sh` catches the countable tells. It will not catch a page +that is merely flat — that part is on you. + +## Never hand-edit a mirrored page + +A page with an upstream `source_repo` in its front matter is overwritten +wholesale by `cmd/sync` on the next release (`cmd/sync/sync.go`, `os.WriteFile`). +Editing it here is work that gets thrown away. Fix it in the source repo. + +- Mirrored: `content/reference/*`, `content/changelog/*`, `content/guides/*` + (except `index.md`), `content/cli/*`, `content/client/*`, `content/contributing/*` +- Docs-native: `content/index.md`, `content/getting-started/*`, + `content/recipes/**` (including `recipes/apps/*`), the section `index.md` files + +`content/_meta/source-of-truth.yaml` is the machine-readable mapping and the +authority when it and `source-of-truth.md` disagree. + +## Builds and tests + +```bash +GOWORK=off make test # everything +make test-e2e # chromedp, needs the site serving +make sweep SWEEP_URL=... # sitemap crawl for overflow +``` + +Verify UI in a real browser, never `curl`. `/recipes/ui-patterns/lists/large-table` +is a 4.9 MB page that times out the sweep on production too — that flag is +pre-existing. + +## Editing `content/index.md` + +Never put a blank line inside a `
` block. Goldmark ends the HTML block
+there and re-parses the rest as markdown: indentation is stripped and `

` tags +appear inside the ``. The file carries a note saying so. diff --git a/README.md b/README.md index e0feb9a..1f3a124 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,15 @@ Most reference content is mirrored from canonical files in the source repos: | Recipes (markdown) | this repo, under `content/recipes/` | Authored directly here | | Runnable apps + their tests | this repo, under `examples//` | Authored directly here | -The source-of-truth matrix lives at `content/_meta/source-of-truth.md`. +The source-of-truth matrix lives at `content/_meta/source-of-truth.md`, and +`content/_meta/source-of-truth.yaml` is the machine-readable version the sync +reads. Where the two disagree, the YAML wins. + +## Writing + +`VOICE.md` is how the prose here is written — the register, the vocabulary to +avoid, and why. Read it before editing anything under `content/`. +`scripts/voice-check.sh` checks the parts of it that can be checked. Every runnable demo cited by a recipe lives at `examples//` — one folder per app, containing the Go package, template, chromedp diff --git a/VOICE.md b/VOICE.md new file mode 100644 index 0000000..8ac2e8e --- /dev/null +++ b/VOICE.md @@ -0,0 +1,111 @@ +# Voice + +How the prose on this site is written. This file exists because the landing copy +has been rewritten four times (#65, #83, #135, #136) and drifted back to the same +flat register every time. Nothing recorded the target, so every rewrite guessed. + +Applies to `content/**` and to the writing around it — PR descriptions, this +file, `README.md`. The register leaks into the meta-writing first. + +## Do + +**Argue by concession.** Claim, then the objection, then concede it, then +resolve. Four short sentences beat one balanced one: + +> A process enforced by a human performs a little better than documents. But a +> human is a human. They get sick, go on a vacation or quit. + +**Vary sentence length hard.** Four words, then twenty. Fragments are fine. +Start sentences with But, So and And. + +**Ask, then answer in one word.** + +> But doesn't the import fetch the module over the network? Yes. That could be a +> problem. + +**Understate.** "quite powerful", "fairly simple", "a little better", "for the +most part". Never a superlative. + +**Name the catch.** "The catch here is to use `T.Errorf`…" + +**Rank what matters.** "This snippet is the most interesting part." + +**Admit limits without defending them.** "the abstraction is not complete." + +**Position honestly against neighbours, conceding where they win.** htmx, templ, +Alpine and LiveView are all reasonable choices. Say where they're better. + +**Stop.** No call to action, no recap, no "happy coding". When the page is done, +end it. + +**Bare imperative for instructions.** "Use it in a test." "Run it." The reader is +`you`. Nothing is `we`. + +## Don't + +**Product as the subject.** Not "LiveTemplate builds reactive web UIs" — write +what the reader does, or what the mechanism does. + +**Triadic negation as cadence.** "no client-side framework, no second state +model, no build step" is a drumbeat, not an argument. One negation, doing work. + +**Meta-commentary about the document.** "This is the idea the rest of the page +elaborates" tells the reader nothing about the software. + +**Exclamation marks, emoji, Title Case headings.** Headings are sentence case. +"The magic:" and "That's it!" are banned outright. + +**Claudisms.** The vocabulary that signals a language model wrote it. It comes +back on every rewrite because it reads as thoughtful: + +> load-bearing · spine · crisp · surfaces (as a verb) · delve · testament to · +> underscore · nuanced · at its core · fundamentally · it's worth noting · that +> said · crucially · importantly · genuinely · meaningfully · north star · +> unpack · double-click on · orthogonal · non-trivial · heavy lifting · footgun · +> batteries included · sane defaults · first-class · opinionated · ergonomic · +> primitives · the shape of · earns its keep · seam · tapestry · isn't just X, +> it's Y + +Say the plain thing. "load-bearing" is "this line is what makes it work". +"surfaces an error" is "shows the error". "primitives" is usually "functions". + +**The rhetorical reversal.** A contrast whose second half exists only to make the +sentence land: + +> so you can check the claim rather than take it + +The test: **does the contrast tell the reader something?** "sends a frame instead +of a form POST" does — both halves are concrete and you learn what the +alternative was. "check the claim rather than take it" doesn't. This is not a ban +on `rather than`; most uses on this site are the good kind. It's a ban on the +ornamental kind. + +## No first person + +The docs stay impersonal. That's a deliberate choice, and it means rhythm, +concession and understatement have to carry the voice on their own — there's no +`I` to lean on. Where a blog post would say "too costly for me", a page here says +"that cost is the thing this avoids". + +## Two things that are not drift + +**`reach for`** appears on 24 pages and is now house idiom, not a slip. Thin it +where the choosing isn't the point. It's deliberately not in the checker. + +**The `ui-patterns/` heading skeleton** — `Template` / `Handler & state` / `When +to use` — is a catalog template. Fix the prose under it, not the shape. + +## What the checker does + +`scripts/voice-check.sh` counts the tells that can be counted: emoji, +exclamation marks, Title Case headings, product-as-subject openers, royal we, +claudisms, passive constructions. It runs on docs-native paths only. + +It will not find a page that is merely flat. Most of this file is not +checkable — that's why it's written down. + +## Pages this file can't fix + +Anything with an upstream `source_repo` in its front matter is mirrored, and +`cmd/sync` overwrites the whole file on the next release. Fix those at the +source repo. See `content/_meta/source-of-truth.yaml` for the mapping. diff --git a/content/_meta/source-of-truth.md b/content/_meta/source-of-truth.md index acc010a..e298b81 100644 --- a/content/_meta/source-of-truth.md +++ b/content/_meta/source-of-truth.md @@ -32,7 +32,7 @@ It is consumed by humans deciding where to edit content, and (Phase 3 onwards) b | `*/CLAUDE.md` files | NEVER mirrored | They're agent instructions, not user docs | | `docs/proposals/`, `docs/plans/`, `docs/archive/`, `design/`, `WORKFLOWS.md`, `ROADMAP.md`, `AGENT_*.md` | NEVER mirrored | Internal RFCs / planning / agent setup | | `docs/performance/` | NOT mirrored in v1 | May graduate to public if stabilized | -| `examples//README.md` | All mirrored under `/recipes/apps/` | These are user-facing showcases | +| `examples//README.md` | NOT mirrored | The `/recipes/apps/` pages are docs-native (see App Recipes below); the READMEs serve the example directory | | `examples/patterns/<...>` | NOT mirrored | Served live via reverse proxy at `/recipes/ui-patterns/*` (Phase 1 PR-D) | | `*/CONTRIBUTING.md` | All mirrored under `/contributing/` | Centralizes contributor onboarding | | Per-repo `README.md` | Mirrored conditionally; some split (see below) | The four repos each have a different role | @@ -114,22 +114,23 @@ Order: getting-started -> guides -> reference -> CLI -> client -> recipes -> con |---|---|---|---|---| | Recipes (8–10 of them) | this repo | `content/recipes/*.md` | `/recipes/` | recipe (Phase 5 authors directly) | -### App Recipes (mirrored from examples) +### App Recipes (docs-native) + +App recipes used to be mirrored from `livetemplate/examples`. They aren't any +more. After the consolidation, the pages at `content/recipes/apps/.md` are +authored here and this repo is their source of truth; the runnable apps live at +`examples//` and are mounted by `cmd/site`. + +The rows below said `Mirror? yes` until 2026-08-09. They were wrong, and the +mistake was expensive: those ten pages were ported once from the example READMEs +and then cut loose, so they still carry the README register — Title Case +headings, emoji, exclamation marks — while the rest of the site moved on. See +`VOICE.md`. | Concept | Source repo | Source path | Site URL | Mirror? | |---|---|---|---|---| -| App recipes index | `examples` | `README.md` | `/recipes/apps/` | yes | -| counter | `examples` | `counter/README.md` | `/recipes/apps/counter` | yes | -| todos | `examples` | `todos/README.md` | `/recipes/apps/todos` | yes | -| chat | `examples` | `chat/README.md` | `/recipes/apps/chat` | yes | -| avatar-upload | `examples` | `avatar-upload/README.md` | `/recipes/apps/avatar-upload` | yes | -| flash-messages | `examples` | `flash-messages/README.md` | `/recipes/apps/flash-messages` | yes | -| login | `examples` | `login/README.md` (when present) | `/recipes/apps/login` | yes | -| dialog-patterns | `examples` | `dialog-patterns/README.md` (when present) | `/recipes/apps/dialog-patterns` | yes | -| live-preview | `examples` | `live-preview/README.md` (when present) | `/recipes/apps/live-preview` | yes | -| progressive-enhancement | `examples` | `progressive-enhancement/README.md` | `/recipes/apps/progressive-enhancement` | yes | -| shared-notepad | `examples` | `shared-notepad/README.md` (when present) | `/recipes/apps/shared-notepad` | yes | -| ws-disabled | `examples` | `ws-disabled/README.md` | `/recipes/apps/ws-disabled` | yes | +| App recipes index | this repo | `content/recipes/apps/index.md` | `/recipes/apps/` | no | +| counter, todos, chat, avatar-upload, flash-messages, login, progressive-enhancement, shared-notepad, ws-disabled, file-tree, seat-picker, upload-modes | this repo | `content/recipes/apps/.md` | `/recipes/apps/` | no | ### Contributing (mirror, per-repo) diff --git a/content/changelog.md b/content/changelog.md index 3a0d41b..8dca9b9 100644 --- a/content/changelog.md +++ b/content/changelog.md @@ -8,9 +8,8 @@ source_path: content/changelog.md # Changelog LiveTemplate ships as three released pieces, each with its own history. Every -page below is mirrored straight from that repository's `CHANGELOG.md` on -release, so it stays current by construction rather than by anyone remembering -to copy it across. +page below is mirrored straight from that repository's `CHANGELOG.md` when it +releases. Nobody has to remember to copy it across. - [**Core library**](/changelog/livetemplate) — `github.com/livetemplate/livetemplate`. The Go server: the reactive tree, actions, sessions, uploads, pub/sub. diff --git a/content/getting-started/introduction.md b/content/getting-started/introduction.md index 44ba39d..0ead869 100644 --- a/content/getting-started/introduction.md +++ b/content/getting-started/introduction.md @@ -7,11 +7,12 @@ source_path: content/getting-started/introduction.md # Introduction -LiveTemplate builds reactive web UIs in **standard HTML and Go**. You write an -ordinary `html/template` and a small Go controller; the browser sends ordinary -form data; the server re-renders, diffs, and patches the page. There is no -client-side framework to learn, no second state model to keep in sync, and no -build step for the common app screens. +Write an `html/template` and a small Go controller. The browser posts a form, the +server re-renders, and only the changed parts of the page get patched. + +A client framework does this too, and for a canvas editor it should. But for a +settings screen it means keeping two copies of the same data in sync. That cost +is the one thing this is trying to avoid. The defining idea is that you never leave HTML. A `