Landing: show the finished app first, then explain it - #136
Merged
Conversation
The six-step spine asked the reader to travel five steps before seeing what the thing is. This leads with the finished app — a shared greeting wall, live on the page — and its code, then annotates the parts of that code worth a second look. Each section below the fold points back at lines already read rather than adding a new one: the button name is the action, the same form works with scripting off, the HTML rule re-runs in Go, slow work has a pending state, and two Publish calls are the whole difference between "my tabs" and "everyone". Every claim keeps its live app, now as evidence rather than as a step to climb. The hero's Go is 28 lines against a real file of 249, so the page says so in the paragraph under it and links the file. Overclaiming "this is the whole app" is what the old step-1 hero could honestly say about a 20-line greet; the wall cannot. The template, by contrast, is verbatim: 9 lines, with only the server-heartbeat markup dropped because it is explained further down. The hero's two snippets stack full width instead of sitting side by side. Read in sequence rather than compared, and at 1440px a .pair column is ~420px against signatures past 60 characters — every interesting line was clipping. e2e is untouched and passes unchanged. Every test that loads "/" uses scoped embed selectors, .hero, or the iframe sandbox string; the ones with bare input[name=name] selectors navigate to standalone app URLs. All five embed paths, both nojs iframes and .hero survive the reorder. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166MK1arBYbVZq6wfm8EsQZ
"including anyone else reading this page right now" is unverifiable for the usual visitor, who is alone on the page. The lead now links a second tab of the same page, so the claim is something you can test in one click rather than take on trust.
…true
Two reports, both fair.
"Still no highlight": the fourth hue was #5A5560, which is R90 G85 B96 —
all but neutral. Its luminance distance from plain text was fine, the
same as the keyword blue's; what it lacked was chroma, so it read as
"dark" rather than as a colour. Names are now plum #6B4A7A and built-in
types and literals get their own amber #7D5226, which also stops Go's
`string`/`error` reading as string literals. Six hues, each mapping to a
distinct token class, all still darker than --lt-meta so the <=13px
contrast floor holds. `builtin` had to come out of the string rule too —
it was listed in both, and the later one was winning.
"The final app doesn't have the snippets the later breakdowns call out":
correct, and it was worse than the one instance. Five referenced symbols
were absent from the hero — lvtClientScriptURL, ValidateForm, EqualFold,
lvt.Pending, lvt.Async — while the section intro promised that everything
below pointed back at lines already read. That promise is the entire
premise of showing the app first, so:
- the hero template gains its <script> line, which is genuinely in
wall.tmpl and is what the no-JavaScript section points at;
- the validation section now leads with the NewFieldError the wall
really does enforce, and introduces ValidateForm as the second half,
attributed to greet-validate rather than implied to be in the hero;
- the pending section says outright that the wall answers instantly and
these are two different apps;
- the intro no longer claims everything points backwards, and names the
two sections that don't.
An audit over the rendered symbols now shows every already-seen claim
resolving to the hero snippet.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0166MK1arBYbVZq6wfm8EsQZ
The hero is supposed to be the app the rest of the page annotates, so a
breakdown pointing at code that isn't there breaks the premise. Five
symbols were only in the prose. The fix belongs in the app, not the
snippet: if the code shows ValidateForm, the demo has to do it.
greet-wall gains what it was missing and now genuinely has it:
- the input carries `required`, and Greet calls ctx.ValidateForm() to
re-run exactly that rule server-side, so a client that skipped it —
scripting off, or a direct POST — gets the same answer;
- a reserved-name rule HTML cannot state ("admin"), which also stops a
visitor posing as the server on a public wall;
- the button carries the two lvt-el:*:on:pending/done attributes, so
pending is real behaviour rather than a claim. No server state and no
spinner element: on a fast action it is a brief dip, which is honest.
Verified in a browser: "admin" comes back as an inline field error with
aria-invalid set and the headline unchanged, "Ada" goes through.
The hero snippet now mirrors that, and grows a main() carrying the
WithTopicACL the multi-user section had been restating separately. Twelve
referenced symbols, all resolving to the hero. The sections stop
disclaiming — validation and pending point back at lines above instead of
introducing other apps, and the intro no longer needs its caveat.
Full e2e green, greet-wall unit tests green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0166MK1arBYbVZq6wfm8EsQZ
The hero was carrying two lvt-el:*:on:pending/done attributes directly above a section titled "No attributes" that calls lvt-* an escape hatch. Leading with the escape hatch argues against the page's own point. The server-owned form is the one to show, but it cannot go in the wall. Async's apply callback receives no *Context (api.md:391), so the two ctx.Publish calls could not run inside it — cross-user fan-out would have to capture the session and TriggerAction from the closure. And with no live connection apply never runs at all, so the plain-HTTP path would drop greetings silently. Rewriting the flagship app that way to display an idiom is a bad trade. So the wall's button goes back to plain HTML, and the pending section owns the topic outright: it now says plainly that this is the one thing the app above cannot demonstrate, because the wall answers instantly and has no pending state to render. Variant A leads as the one to reach for — the flag is a template variable, so the spinner is ordinary Go and ordinary HTML — and B is named as the escape hatch, for when the Go should not change, with A's live-session requirement stated against it. The hero is now free of lvt-* attributes entirely, which is what the section below it claims. Eleven referenced symbols still resolve to it. Full e2e green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166MK1arBYbVZq6wfm8EsQZ
A reader asked what `a.record(...)` and the state assignment beside it were for, which is the snippet failing at the one job it has. `record` was a name invented for the page that conveys nothing. It stood for three writes — the throttle stamp, the per-group name Refresh re-reads, and the append to the shared ring buffer. It is now two calls named for their effects, `saveName` and `appendWall`, the latter a real method on the controller. The state assignment is the line worth explaining and had no comment at all. It looks redundant next to the two Publish calls below it, and is not: a publish skips the connection that called it, so without this the person who clicked would be the only one who did not see their own greeting. That reason is now on the page.
adnaan
added a commit
that referenced
this pull request
Aug 10, 2026
* docs: write down the voice, and fix the pages that lost it worst 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) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166MK1arBYbVZq6wfm8EsQZ * ci: run the voice ratchet on every PR Without this the checker is a script nobody runs, which ratchets nothing. It goes in the validate job next to `tinkerdown validate` — both are content gates and neither needs the browser stack. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166MK1arBYbVZq6wfm8EsQZ * docs(recipes/apps): bring the ten ported pages into the same voice These were ported once from examples/<slug>/README.md and then cut loose, so they still read like READMEs while the rest of the site moved on. Against the other 53 docs-native pages they carried 9 exclamation marks to 2, 14 emoji to 5, and 36 Title Case headings to 7. Counts across all docs-native pages: emoji 19 -> 0, Title Case headings 43 -> 0, exclamations 11 -> 3, passives 114 -> 83. The three exclamations left are quoted UI copy ("Changes saved!"), which is what a flash message actually says. Two more false claims went with the register, both in chat.md: - "Just modify state - broadcasting happens automatically!" Mutating state does nothing on its own; Mount subscribes and each action publishes. - The comparison table's "Auto-broadcasting" framing, same problem. Two carve-outs, both in VOICE.md and the checker: - Emoji in a comparison table are scan markers doing real work (the "does this scale?" matrix in recipes/counter/index.md). The checker skips table rows. - H1 carries the page NAME and must equal the front-matter title. Downcasing it renames the page — breadcrumb_test.go caught me doing exactly that on App Recipes. Added an h1-vs-title check so it can't happen again, and verified the check fires by breaking a title on purpose. Not fixed here, flagged instead: chat.md's tutorial documents an API that does not exist (a Change(ctx *ActionContext) switch on ctx.Action, and an undefined `controller` in main). The real examples/chat/main.go uses the controller pattern. That is a correctness fix, not a voice one, and it needs its own PR. Verified: voice-check green at the new ceilings, tinkerdown validate 98/98, e2e green uncached on :8084, sweep 194/196 (the 2 are the pre-existing 4.9 MB large-table timeouts), and all ten pages read back clean from a real browser. Anchor links checked against main: 15 broken on both, 0 new. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166MK1arBYbVZq6wfm8EsQZ * docs: finish the register pass on the remaining native pages Phase 2b, across the 57 docs-native pages outside recipes/apps/. Every countable tell is now at its floor: claudisms 14 -> 0 product-as-subject 3 -> 0 royal we 4 -> 0 meta-commentary 3 -> 0 hype lexicon 1 -> 0 triadic negation 5 -> 1 passive voice 114 -> 34 The passive number is where the work was. What is left is mostly not passive voice at all — "the title is required" describes an HTML attribute, "action methods are exported" is Go, "Email is required" is a quoted flash message, and "is untouched" is an adjective. The regex cannot tell those apart, so the ceiling stays at 34 rather than pretending 0 is reachable. The one triadic negation left is the landing's "no hx-post, no onClick, no route to register". Each item names a real thing you would otherwise write, so it is an argument rather than a drumbeat. Two judgement calls worth naming: - "literate primitives" in source-of-truth.md was not upstream terminology — nothing else in the repo or in tinkerdown uses it. Renamed to "literate blocks", which is what the section actually documents. - "reach for" stays. At 24 files it is house idiom, not drift, and it is deliberately absent from the checker. Also fixed two checker false positives found by running it: "the rest of the page" is usually literal ("without blocking the rest of the page"), so meta-commentary now requires a following verb; and the h1-vs-title comparison strips backticks, which a front-matter title cannot carry. chat.md's remaining "automatic syncing" claims went too — leaving them would have contradicted the correction made to the same file in the previous commit. Verified: voice-check green at the new ceilings, tinkerdown validate 98/98, e2e green uncached on :8084, sweep 194/196 (the 2 are the pre-existing 4.9 MB large-table timeouts), anchors 15 broken on both branch and main so 0 new, and 14 rewrites read back out of a real browser. No mirrored page touched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166MK1arBYbVZq6wfm8EsQZ --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
adnaan
added a commit
that referenced
this pull request
Aug 16, 2026
…concede (#142) Four headings were verbless noun phrases punctuated as sentences: "A shared greeting wall, running on this page.", "The parts of that worth a second look.", "Other things in here.", "Getting started." No finite verb, so nothing in them is doing anything. That reads as passive even though it isn't one. The split is the interesting part. Headings that make a technical claim were already sentences — "The button's name is the action", "The HTML rule runs again in Go", "Two calls sync your tabs". The four fragments were all headings that orient the reader rather than argue. The register broke where the page navigated. The comparison table had a worse problem. Every row sat under a neighbour's name and described LiveTemplate instead, so the Alpine.js row read as a description of Alpine. Five rows, five wins, no losses, from a library whose own eyebrow says alpha. Each row now describes the neighbour and says where it still wins: htmx works against any backend, templ type-checks markup at compile time, Alpine keeps real state in the browser, LiveView is years ahead. The React row reuses the line from getting-started/introduction.md so the two pages agree. The first draft of the Alpine row claimed it "answers a click without a round trip, which nothing here can do." That is false — reference/ client-attributes.md documents native DOM event triggers that "execute client-side with no server round-trip", which is the dropdown case exactly. An overstated concession is the same credibility problem in reverse, and it contradicted the escape-hatch note two screens up. Also fixed: - #diff in the rail pointed at no section — a fossil from #136, where the diff step folded into Multi-user. Nothing resolves in-page anchors, so validate, the sweep and the e2e suite all passed over it. - "The first is the way to reach for" — mangled idiom. - "It targets what Go teams actually ship" — product as subject, via a pronoun that dodges the checker's ^LiveTemplate regex. - A relative clause in the section intro that didn't attach, plus "and which says so", which is the document narrating itself. - The hero's triadic negation, which the front-matter description repeated verbatim — two copies, one edit. - The CTA promised the counter tutorial that neither button linked to. No checker rule for the heading defect: "lacks a finite verb" needs a parser, and a fragile approximation gets switched off within a week. It goes in VOICE.md with the reading test, same call as rhetorical reversals. Every ratchet ceiling is unchanged. Claude-Session: https://claude.ai/code/session_0166MK1arBYbVZq6wfm8EsQZ Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #135. That one changed how the site looks; this changes what the landing says and in what order.
Why
The six-step spine asked the reader to travel five steps before seeing what the thing is. This leads with the finished app — a shared greeting wall, live on the page — and its code, then annotates the parts of that code worth a second look.
Each section below the fold points back at lines you have already read rather than adding a new one: the button name is the action, the same form works with scripting off, the HTML rule re-runs in Go, and two
Publishcalls are the whole difference between "my tabs" and "everyone". Every claim keeps its live app, now as evidence rather than as a step to climb.This touches a shipped example, not just copy
examples/greet-wallgains real behavior, because the alternative was worse. The hero is meant to be the app the rest of the page annotates — so a breakdown pointing at code the demo does not run is a lie, not a shortcut. Rather than adding the missing lines to the snippet, the features went into the app:required, andGreetcallsctx.ValidateForm()to re-run exactly that rule server-side, so a client that skipped it — scripting off, or a direct POST — gets the same answer;"admin"), which also stops a visitor posing as the server on a public wall.Verified in a browser rather than asserted:
admincomes back as an inline field error witharia-invalid="true"and the headline unchanged;Adagoes through.The wall's own presentation also moved into
wall.tmpl(list markers, timestamp colour), so it travels with the app to the recipe pages that embed it.What is honest about the hero, and what isn't
greet; the wall cannot. The paragraph under it names what is missing —sanitize, the map writes behindsaveName, a 20-line cap, a per-session throttle — and links the real file.Asyncto fake one was considered and rejected:applyreceives no*Context(reference/api.md:391), so the twoctx.Publishcalls could not run inside it, and with no live connectionapplynever runs at all — the plain-HTTP path would drop greetings silently.lvt-*attributes in the hero. It briefly carried two, directly above a section titled "No attributes" calling them an escape hatch. That argued against the page's own point.Highlighting
The four-hue palette from #135 was too timid to read:
#5A5560is R90 G85 B96, all but neutral, so names rendered as "dark" rather than as a colour. Names are now plum#6B4A7Aand built-in types and literals get amber#7D5226, which also stops Go'sstring/errorreading as string literals. Six hues, each mapping to a distinct token class, all still darker than--lt-metaso the ≤13px contrast floor holds.builtinalso had to come out of the string rule — it was listed in both, and the later one was winning.e2e is untouched
Every test that loads
/uses scoped embed selectors,.hero, or the iframe sandbox string; the ones with bareinput[name="name"]navigate to standalone app URLs. All five embed paths, bothnojs-frameiframes and.herosurvive the reorder, so the wholeTestSpine*suite passes without modification.Verification
go test ./e2eagainst the pinned v0.3.9 image: green, 161s.examples/greet-wallunit tests: green.make sweep: 196 page-viewport visits, 0 overflow. Two flags remain on/recipes/ui-patterns/lists/large-table, a pre-existing 4.9 MB page that renders in 9.6s on production and ~7s here.That last check is currently a script, not a test. The failure mode is silent and returns every time the hero code is edited, so it is worth promoting to
calm_theme_test.go— happy to do that here or in a follow-up.🤖 Generated with Claude Code
https://claude.ai/code/session_0166MK1arBYbVZq6wfm8EsQZ