Skip to content

Landing: show the finished app first, then explain it - #136

Merged
adnaan merged 6 commits into
mainfrom
landing-ia
Aug 8, 2026
Merged

Landing: show the finished app first, then explain it#136
adnaan merged 6 commits into
mainfrom
landing-ia

Conversation

@adnaan

@adnaan adnaan commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

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 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.

This touches a shipped example, not just copy

examples/greet-wall gains 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:

  • 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.

Verified in a browser rather than asserted: admin comes back as an inline field error with aria-invalid="true" and the headline unchanged; Ada goes 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

  • The template is verbatim — 9 lines, only the server-heartbeat markup dropped because it is explained further down.
  • The Go is 28 lines against a real file of 249. The old step-1 hero could honestly say "this is the whole app" about a 20-line greet; the wall cannot. The paragraph under it names what is missing — sanitize, the map writes behind saveName, a 20-line cap, a per-session throttle — and links the real file.
  • Pending state is the one section that does not point back. The wall answers instantly, so it has no pending state to render, and the page says so outright. Making it Async to fake one was considered and rejected: apply receives no *Context (reference/api.md:391), so the two ctx.Publish calls could not run inside it, and with no live connection apply never runs at all — the plain-HTTP path would drop greetings silently.
  • No 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: #5A5560 is R90 G85 B96, all but neutral, so names rendered as "dark" rather than as a colour. Names are now plum #6B4A7A and built-in types and literals get 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 also 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 bare input[name="name"] navigate to standalone app URLs. All five embed paths, both nojs-frame iframes and .hero survive the reorder, so the whole TestSpine* suite passes without modification.

Verification

  • Full go test ./e2e against the pinned v0.3.9 image: green, 161s.
  • examples/greet-wall unit 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.
  • A symbol audit over the rendered page: every "you already saw" claim resolves to the hero snippet.

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

adnaan and others added 6 commits August 8, 2026 01:22
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
adnaan merged commit edd471b into main Aug 8, 2026
4 checks passed
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant