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
11 changes: 11 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,17 @@ jobs:
- name: Validate content
run: tinkerdown validate content/

# The voice ratchet (VOICE.md, scripts/voice-check.sh). Ceilings are the
# counts on the day each was last tightened, so this fails when a tell
# goes UP. It is scoped to docs-native pages — mirrored ones are owned by
# their source repo and would fail the wrong PR.
#
# It cannot tell whether a page reads well, only whether it regressed in
# the ways that are countable. The landing copy was rewritten four times
# before this existed; that is what it is here to stop.
- name: Voice check
run: ./scripts/voice-check.sh

- name: Test sync tool
working-directory: cmd/sync
run: go test ./... -count=1
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,4 @@ cmd/site/site
examples/avatar-upload/uploads/*
!examples/avatar-upload/uploads/.gitkeep
examples/avatar-upload/.uploads/
.prereview/
44 changes: 44 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -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 `<pre>` block. Goldmark ends the HTML block
there and re-parses the rest as markdown: indentation is stripped and `<p>` tags
appear inside the `<code>`. The file carries a note saying so.
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<slug>/` | 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/<slug>/` —
one folder per app, containing the Go package, template, chromedp
Expand Down
123 changes: 123 additions & 0 deletions VOICE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# 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".

## Four things that are not drift

**Emoji in a comparison table.** ✅ / ⚠️ / ❌ down a column are scan markers doing
real work — see the "does this scale?" matrix in `recipes/counter/index.md`. The
checker skips table rows for that reason. In prose they're decoration, and the
checker still counts them there.

**Title Case in an H1.** The H1 carries the page *name*, and it must be either
the front-matter `title:` verbatim or `Title — a sentence-case clause`. It feeds
the nav and breadcrumbs that `docs_ia_test.go` and `breadcrumb_test.go` assert
on, so downcasing it is a rename, not a copy edit — `breadcrumb_test.go` caught
exactly that during this pass. Rename a page deliberately or not at all. H2–H6
are prose and are sentence case.

**`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.
33 changes: 17 additions & 16 deletions content/_meta/source-of-truth.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<app>/README.md` | All mirrored under `/recipes/apps/<app>` | These are user-facing showcases |
| `examples/<app>/README.md` | NOT mirrored | The `/recipes/apps/<app>` 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/<repo>` | Centralizes contributor onboarding |
| Per-repo `README.md` | Mirrored conditionally; some split (see below) | The four repos each have a different role |
Expand Down Expand Up @@ -114,22 +114,23 @@ Order: getting-started -> guides -> reference -> CLI -> client -> recipes -> con
|---|---|---|---|---|
| Recipes (8–10 of them) | this repo | `content/recipes/*.md` | `/recipes/<slug>` | 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/<slug>.md` are
authored here and this repo is their source of truth; the runnable apps live at
`examples/<slug>/` 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/<slug>.md` | `/recipes/apps/<slug>` | no |

### Contributing (mirror, per-repo)

Expand Down Expand Up @@ -187,10 +188,10 @@ These don't currently exist anywhere and must be written from scratch — likely

---

## Literate primitives in mirrored content
## Literate blocks in mirrored content

Mirrored upstream READMEs may use tinkerdown's literate authoring
primitives (since tinkerdown v0.2.0). The sync tool passes them through
blocks (since tinkerdown v0.2.0). The sync tool passes them through
byte-for-byte and mirrors a single adjacency convention so they resolve
correctly post-sync.

Expand Down
4 changes: 1 addition & 3 deletions content/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@ 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 mirrors that repository's `CHANGELOG.md` at release time. 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.
Expand Down
4 changes: 2 additions & 2 deletions content/getting-started/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: "Add LiveTemplate to a Go project — one go get and you have what

# Install

LiveTemplate is a Go framework. The minimum to get a reactive page running is one `go get`.
One `go get` is the whole install. Everything below is optional.

## Add the framework

Expand All @@ -17,7 +17,7 @@ That's it for the server side. The TypeScript client wires up the browser side a

## Loading the browser client

Add these to your template. The `lvtClientScriptURL` / `lvtClientStyleURL` functions are provided by the framework on every template:
Add these to your template. The framework hands every template two functions, `lvtClientScriptURL` and `lvtClientStyleURL`:

```html
<link rel="stylesheet" href="{{lvtClientStyleURL}}">
Expand Down
29 changes: 15 additions & 14 deletions content/getting-started/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<button name="increment">`
*is* the action — you don't annotate it to make it reactive. You reach for an
Expand All @@ -20,16 +21,16 @@ keyboard shortcut, a reactive class toggle), never as boilerplate.

## When LiveTemplate fits

It's a good fit when you're building app screens in Go and want live behavior —
forms with inline validation, multi-tab sync, dashboards that update
themselves, cross-user views — without standing up a separate frontend. Because
the same program works as a plain form POST first, it
[degrades gracefully](/recipes/progressive-enhancement/) to environments with no
JavaScript and upgrades to WebSocket-driven real time where you want it.
Good fit: app screens in Go that need live behavior. Forms with inline
validation, multi-tab sync, dashboards that update themselves, views shared
across users. All of it without standing up a separate frontend.

The same program works as a plain form POST first, so it
[keeps working](/recipes/progressive-enhancement/) where JavaScript is off, and
picks up WebSockets where you want them.

It's a weaker fit for highly bespoke client-side interaction (canvas editors,
offline-first apps, animation-heavy UIs) where the logic genuinely belongs in
the browser.
Weaker fit: canvas editors, offline-first apps, animation-heavy UIs. The logic
really does belong in the browser there, and this would be fighting you.

## How it compares

Expand Down
Loading
Loading