Skip to content

Landing: give the last section a subject - #143

Merged
adnaan merged 3 commits into
mainfrom
landing-more-section
Aug 23, 2026
Merged

Landing: give the last section a subject#143
adnaan merged 3 commits into
mainfrom
landing-more-section

Conversation

@adnaan

@adnaan adnaan commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Two rounds of feedback on one section: the heading said nothing, then the eyebrow said nothing, then the lead was too long.

What was wrong

The section held three different taxonomies at once, so no heading could summarise it:

  • eyebrow — "Everything else", a non-label
  • heading — about the documentation
  • first lead — app types: admin panels, CRUD, dashboards
  • eight linkscapabilities: uploads, pubsub, sessions, errors, CLI, client, observability, scaling
slot before after
eyebrow Everything else More capabilities
heading The rest of the docs covers the ordinary screens. Your app has file uploads and a login. Both are here.
lead Admin panels, internal tools, CRUD, dashboards, approvals, uploads, auth, and the occasional shared view. That is what this is for. Admin screens, internal tools, CRUD and dashboards are the point.

The lead was a table of contents for a visible grid

An intermediate draft ran three sentences, each containing a list, with the middle one mixing three single-word nouns against a ten-word clause. All of it enumerated the eight links sitting directly below, where each already carries its own gloss.

It also opened "the wall is one screen, and it needed neither" — false about the grid, since the wall uses sessions and the browser client. Cut to the one thing the grid can't say: who this is for. That claim appears nowhere else on the page.

Two links contradicted the new heading

"Shared views / Subscribe & Publish" is exactly what the Multi-user section demonstrates, and "Forms & errors" is exactly what the Validation section demonstrates. Both references now sit at the end of the section that shows them off, where a reader who wants the API is already standing. Their grid slots go to two genuine gaps:

  • Auth & login/recipes/login/ — the old lead said "auth" and never linked it
  • Navigation/reference/navigate — nothing on the page mentioned it

One false claim, cut before it shipped

A draft of the lead promised "sessions that outlive a restart". Wrong by default — reference/session documents the SessionStore as "in-memory or Redis", and every persistence claim on that page is scoped to surviving a page refresh, not a process restart. Redis-backed behavior presented as the baseline. Same failure mode as the Alpine row in #142, one section later.

Verification

  • voice-check.sh — pass, every ceiling unchanged
  • tinkerdown validate — 98/98, 0 errors
  • go test ./e2e/ — pass, 135s
  • make sweep at desktop + iphone-14 — 196 visits, 2 flagged, both the pre-existing large-table timeout
  • Read at 1440px in a browser
  • All 10 headings still carry a finite verb; no dead in-page anchors

🤖 Generated with Claude Code

https://claude.ai/code/session_0166MK1arBYbVZq6wfm8EsQZ

"Everything else" over "The rest of the docs covers the ordinary screens."
was a heading about the documentation, not about the software. It told a
reader nothing they hadn't already assumed.

The reason it couldn't say anything: the section held three different
taxonomies. The eyebrow was a non-label, the heading described the docs,
the lead listed app types (admin panels, CRUD, dashboards) and the eight
links listed capabilities (uploads, pubsub, sessions, errors, CLI,
client, observability, scaling). No heading summarises three subjects.

  eyebrow  What the wall skipped   -> More capabilities
  heading  The rest of the docs…   -> Your app has file uploads and a
                                      login. Both are here.
  lead     three sentences, three  -> Admin screens, internal tools, CRUD
           lists                      and dashboards are the point.

The lead was a table of contents for a grid that sits directly below it,
where every link already carries its own gloss. Three sentences, each a
list, and the middle one mixed three single-word nouns with a ten-word
clause. It also opened with "the wall needed neither", which was false:
the wall uses sessions and the browser client, both of which are in the
grid. Cut to the one thing the grid cannot say, which is who this is for.

Two of the eight links contradicted the new heading. "Shared views /
Subscribe & Publish" is what the Multi-user section demonstrates, and
"Forms & errors" is what the Validation section demonstrates. Both
references now sit inside the section that shows them off, where a reader
who wants the API is already standing. Their grid slots go to two genuine
gaps: auth, which the old lead named without ever linking, and
navigation, which nothing on the page mentioned.

One draft of the lead promised "sessions that outlive a restart". That is
wrong by default: reference/session documents the SessionStore as
"in-memory or Redis", and every persistence claim on that page is scoped
to surviving a page refresh, not a process restart.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0166MK1arBYbVZq6wfm8EsQZ
@adnaan
adnaan force-pushed the landing-more-section branch from 8506642 to 9d4c12a Compare August 21, 2026 17:40
TestUploadModes_E2E has failed four times in CI with

    upload-modes_test.go:119: proxied flow: context deadline exceeded

always at 60.0s, always the proxied flow, always on PRs that changed only
markdown, and always green on re-run.

It is a race, not a flake in the sense of "unknowable". Every upload here
is driven by a change event on an input[lvt-upload], and that input is
server-rendered — visible in the initial HTML, well before the deferred
client bundle loads and binds its listener. chromedp waits for DOM
visibility, so SetUploadFiles can fire into the gap. The event is dropped
with no error, no upload starts, #proxied-result never appears, and the
WaitVisible burns the whole 60s context deadline set in newChromiumCtx.

The CI timestamps show the gap directly: the session group is created at
:32.85 and the client logs "WebSocket connected" at :33.07, while the
navigate, WaitVisible, SetValue and SetUploadFiles all complete in the
milliseconds after :32.85.

Reproduced under 300ms of emulated latency, which widens the same gap:

    without the readiness wait   context deadline exceeded, 60.04s
    with the readiness wait      pass, 2.12s

Every other browser suite in this repo already waits on the client before
interacting; this one never did.

isReady() rather than the weaker `typeof window.liveTemplateClient !==
'undefined'` that three other suites use: measured against this app with
the socket killed, the object exists after 1.8ms while isReady() only goes
true at 637ms, so the weak check clears 635ms too early. isReady() also
resolves on the HTTP-fallback path, so all four tests can wait on it —
the three WS-disabled ones reproduce the same hang under latency, they
just have a narrower window because DeadSocket fails immediately.

Verified with the bundle CI pins (@livetemplate/client@0.20.0, fetched the
way build.yml fetches it): all four pass unthrottled, and the proxied flow
passes under the 300ms latency that reproduced the failure.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0166MK1arBYbVZq6wfm8EsQZ
Comment thread content/index.md Outdated
<p class="lead">Both halves are in the app above. The input carries <code>required</code>, and <code>ctx.ValidateForm()</code> re-runs exactly that rule on the server — a client that skipped it, scripting off or a direct POST, gets the same answer. Then <code>strings.EqualFold(name, "admin")</code> adds the rule HTML has no way to state.</p>

<p class="lead">The template side is the other two lines you read: <code>{{.lvt.AriaInvalid "name"}}</code> marks the field, <code>{{.lvt.ErrorTag "name"}}</code> is where the message lands. Returning an error from <code>Greet</code> is the whole mechanism — there's nothing to route. Scroll up and type <em>admin</em>, or try the smaller app here.</p>
<p class="lead">The template side is the other two lines you read: <code>{{.lvt.AriaInvalid "name"}}</code> marks the field, <code>{{.lvt.ErrorTag "name"}}</code> is where the message lands. Returning an error from <code>Greet</code> is the whole mechanism — there's nothing to route, and the <a href="/reference/error-handling">error handling reference</a> has the rest. Scroll up and type <em>admin</em>, or try the smaller app here.</p>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The template side is the other two lines you read: We can simply say: On the template side:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 35b134aOn the template side: verbatim.

Also dropped the trailing and the error handling reference has the rest from the same sentence, which was the same padding as the one you flagged on line 313. It was there because the error-handling link got relocated out of the capabilities grid and a trailing clause was the lazy place to put it. It now hangs off the word "error" instead, so the paragraph goes back to ending on "Scroll up and type admin".

Comment thread content/index.md Outdated
<div class="snip-label">app.go · the server can start the same cycle</div>
<pre class="language-go"><code class="language-go">sess.TriggerAction("ServerRefresh", nil)</code></pre>
<p class="note">You already read the <code>WithTopicACL</code> in <code>main</code> that admits <code>"wall"</code> — developer topics are deny-all until you name one. This is the same publish path with no user action behind it: the "the server said hi at …" line in the cards above, pushed on a timer.</p>
<p class="note">You already read the <code>WithTopicACL</code> in <code>main</code> that admits <code>"wall"</code> — developer topics are deny-all until you name one. This is the same publish path with no user action behind it: the "the server said hi at …" line in the cards above, pushed on a timer. The <a href="/reference/pubsub">pubsub reference</a> has the topic rules and the ACL in full.</p>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pubsub reference has the topic rules and the ACL in full: sentence is nonsensical and doesnt mean anything

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, it was padding — "has the topic rules and the ACL in full" bolted onto a paragraph that had already finished, and the reader had just read the ACL in main anyway.

Fixed in 35b134a by deleting the sentence and linking WithTopicACL itself to the pubsub reference. The link survives, no sentence needed.

Both this and the error-handling one existed because I pulled those two references out of the capabilities grid below (they duplicated what the Multi-user and Validation sections already demonstrate) and needed to re-home the links. A trailing sentence was the wrong answer twice.

@adnaan

adnaan commented Aug 22, 2026

Copy link
Copy Markdown
Contributor Author

Second commit: the upload-modes-e2e flake, diagnosed

The PR body above covers the landing section. This is the other commit — 331887a, unrelated to the copy, which is why it's called out separately.

upload-modes-e2e failed on this PR, as it has four times now:

upload-modes_test.go:119: proxied flow: context deadline exceeded

Always at 60.0s, always the proxied flow, always on PRs that touch only markdown, always green on re-run. I'd re-run it three times before actually reading it.

It's a race

Every upload in that suite is driven by a change event on an input[lvt-upload]. That input is server-rendered — present and visible in the initial HTML, well before the deferred client bundle loads and binds its listener. chromedp.WaitVisible waits only for DOM visibility, so SetUploadFiles can fire into the gap. The event is dropped with no error, no upload starts, #proxied-result never appears, and the WaitVisible on it burns the whole 60s deadline from newChromiumCtx.

The CI timestamps show the gap directly — session group created at :32.85, client logs WebSocket connected at :33.07, and navigate → WaitVisible → SetValue → SetUploadFiles all complete in the milliseconds after :32.85.

Reproduced under controlled conditions

It passes 5/5 in ~1.6s locally, so I widened the window with 300ms of emulated network latency and ran the identical action sequence both ways:

result
without readiness wait context deadline exceeded @ 60.04s — CI's exact signature
with readiness wait pass @ 2.12s

Same code, same conditions, one line different.

Two things the measurement changed

All four tests were affected, not just the failing one. The three WS-disabled variants reproduce the same hang under latency — they have a narrower window only because DeadSocket fails immediately instead of doing a real handshake. Fixing one would have left three latent.

The readiness condition three other suites in this repo use is too weak. Measured against this app with the socket killed:

signal true after
typeof window.liveTemplateClient !== 'undefined' 1.8ms
window.liveTemplateClient.isReady() 637ms

The weak check clears 635ms early. isReady() also resolves on the HTTP-fallback path, so all four tests wait on the same signal.

Verification

Against the bundle CI pins (@livetemplate/client@0.20.0, fetched the way build.yml fetches it):

  • all four tests pass unthrottled — 1.61s / 0.75s / 0.81s / 0.80s
  • the proxied flow passes under the 300ms latency that reproduced the failure
  • upload-modes-e2e in CI here: pass in 31s, down from 49s passing / 1m37s failing

Both diagnostic files deleted; the commit touches one test file.

// check used elsewhere in the repo: measured against this app with the socket
// killed, the object appears after ~2ms and isReady() only goes true at ~640ms.
// It resolves on the HTTP-fallback path too, so the WS-disabled tests can wait
// on the same condition.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up worth filing, deliberately not done here.

isReady() is the strong signal. Three other suites in this repo wait on the weaker typeof window.liveTemplateClient !== 'undefined':

  • examples/chat/chat_e2e_test.go:89
  • examples/live-preview/live_preview_test.go:273
  • examples/seat-picker/seat_picker_test.go:144

Measured against the upload-modes app with the socket killed, that condition goes true at 1.8ms while isReady() goes true at 637ms. So those three clear the gate ~635ms early and carry the same latent race this commit fixes — they just haven't lost it in CI yet.

Left alone because they're passing and it's outside this PR's scope. Happy to do it as its own change.

"The template side is the other two lines you read:" carried three clauses
of setup before reaching the point. "On the template side:" says the same
thing.

"The pubsub reference has the topic rules and the ACL in full" said
nothing — a padded pointer bolted onto a paragraph that had already
finished. Same species as "and the error handling reference has the rest"
one section up, which was in there for the same reason: both links were
relocated out of the capabilities grid and needed somewhere to land, and
a trailing sentence was the lazy place to put them.

Both now hang off the API symbol the reader is already looking at —
WithTopicACL links to the pubsub reference, "error" links to the error
handling reference. No added sentence, and the paragraph keeps ending on
its imperative.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0166MK1arBYbVZq6wfm8EsQZ
@adnaan
adnaan merged commit a99b260 into main Aug 23, 2026
8 checks passed
@adnaan
adnaan deleted the landing-more-section branch August 23, 2026 11:46
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