feat(website): phone-width hero poster, re-recorded desktop poster, redesigned OG card - #1002
Merged
Conversation
The old card put the product name in an 18px eyebrow, the runtimes and the capability list inside a three-line 26px paragraph, and three 15px mono pills along the bottom. At the ~500px a timeline actually renders a share card, all of that is grey texture: the only legible element was the headline, and the brand name was the smallest type on the card. Rebuilt around what survives a 0.42x downscale. Four elements, none below 30px source: the wordmark at 100px, the category line (HERO_H1, now on one line) at 52px, HERO_SUBHEAD at 36px for what you get, and a LangGraph + AG-UI pill at 34px for what it plugs into. Centred rather than left-aligned so a square thumbnail crop keeps the product name. Moved onto the production dark-surface tokens from ui.css ([data-ui="section"][data-surface="dark"]) resolved to literals, since Satori cannot read CSS variables, plus the radial accent glow from landing.css .proof-strip::before and --color-angular-red for the seam. Text contrast is 16.8:1 (primary), 11:1 (secondary) and 8.6:1 (accent). Copy stays single-sourced: the wordmark is derived from PRIMARY_TAGLINE rather than retyped, and the runtime claim is POSITIONING_PROOF_POINTS[0]. `alt` now describes what the card says instead of naming the page. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Next's file-convention metadata (the alt and size exports in app/opengraph-image.tsx) is overridden the moment openGraph.images is set explicitly. Both layout.tsx and createPageMetadata passed a bare URL string, so production emitted og:image alone — no width or height for a platform to lay the card out before fetching it, and no alt text at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A parallel to record-hero-poster.record.ts that drives /hero at 390x650 and writes hero-walkthrough-poster-mobile.webp. Same beat as the desktop script — the first streamed reply — because 650 CSS px is the shortest frame that holds the whole answer from its first line, and because matching beats mean crossing the breakpoint swaps the source without changing the story. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The hero poster is the server-rendered LCP and the only thing a phone sees until it taps Play. It was a 1200x720 desktop capture; at 390px it landed in a 348px stage, so the last change narrowed the stage to 4/5 and pushed object-position to 40% so cover would crop rather than shrink. That bought legibility and paid for it in credibility: every line of prose lost its right edge, and the frame read as a broken screenshot. Replaced with a second render of the same moment at phone dimensions (585x975, 33,576 bytes against the desktop poster's 37,718), chosen by a <picture> media source on the same 768px boundary the stage ratio and MIN_AUTOPLAY_WIDTH already use. The stage below the breakpoint now holds 3/5 — the capture's own ratio — so object-fit: cover crops nothing and the object-position nudge is gone. currentSrc resolves to the phone poster at 390 and to the desktop one at 768 and 1440; the stage box measures the same with the image aborted as with it loaded at all three widths, so the swap costs no layout shift. Two follow-on fixes the new asset exposed: the phone frame ends on the demo's own "Take control" button, which read straight through the play control's 97% fill and sat half-uncovered beneath it. The fill is now opaque and the control drops to 12px on phones so it lands squarely on the dead one. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The desktop poster was stale. It showed "Take control" floating over the composer — the absolutely-positioned layout replaced in #998 — and a beat where the walkthrough's second prompt was already typed into the composer. It is the LCP element for every desktop visitor, so the largest image on the homepage was showing a UI that no longer exists. Both recorders now hold 2500ms after the interrupt detaches instead of 1500ms. That is about the scripted cursor, not the text: HOLD_AFTER_ANSWER_MS (2000) plus CURSOR_MOVE_MS (650) means at 1500ms the arrow is still parked where it pressed Accept, which at phone width dropped it onto the word `retain` in step 3 — an artifact rather than a hint that the demo is live. At 2500ms it has reached the composer, which is the beat the desktop script's docstring has always claimed. The beat intent is otherwise unchanged: the first streamed reply, with the user turn, the tool call and the whole answer on screen. Both scripts now assert what they captured — `.hero__take` visible in normal flow, composer empty — so a drifted wait or another layout change fails the recording instead of silently shipping a poster of a UI we no longer have. Desktop 37,718 -> 38,114 bytes; phone 33,576 -> 33,624. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
blove
enabled auto-merge (squash)
September 4, 2026 22:10
Contributor
Contributor
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.
The two images the homepage ships: the hero poster (the LCP element) and the Open Graph card.
A real phone poster, not a crop of a desktop one
The mobile hero was a hard crop of a 1200×720 desktop capture, and the CSS comment said so. Every line of prose died mid-word at the right edge and the prompt bubble ran off entirely.
It is now captured at phone size, 390×650 at 2× and shipped at 585×975. That height was chosen by looking at a matrix of heights and beats: shorter frames open mid-sentence, which reads as a scroll position rather than a composed frame. 390×650 is exactly 3:5, which lets the stage ratio match the asset so nothing is cropped.
The approval-interrupt beat looks like the stronger frame and was rejected for two reasons: at phone height the panel slices the user's prompt bubble, and a still of a live Accept dialog invites taps that do nothing.
A phone downloads this one instead of the desktop one, so mobile LCP bytes go down (33.6KB against 37.7KB) while the text gets about three times bigger.
The desktop poster was showing a UI that no longer exists
Found while verifying the phone one. It still showed "Take control" floating over the composer in the absolutely-positioned layout we replaced, and the composer already carrying the walkthrough's second prompt.
The cause was a wait, not a layout: the recorder held 1500ms after the interrupt detached, but the script's own constants put the cursor at the composer at ~2650ms. At 1500ms the cursor is still parked where it pressed Accept — which at phone width landed the arrowhead squarely on the
retaincode chip. Both posters are re-recorded at 2500ms, the only window where the cursor is heading to the composer with the composer still empty, and the artifact is gone from both.Both recorders now assert what they captured before encoding:
.hero__takevisible, and the composer empty. That second assertion is exactly the check that would have caught the poster this replaces, so a drifted wait now fails loudly instead of shipping.Source selection and LCP
A
<picture>with one media source rather thansrcset, because the two posters are different renders at different aspect ratios — art direction, keyed off viewport, not pixel density. Verified against a production build: the phone poster resolves at 390, the desktop one at 768 and 1440, with eager loading and high fetch priority intact at every width.Zero layout shift, proven by aborting both image requests: the stage measures identically with no image at all, because its own aspect ratio sizes the box.
The damage-control CSS is gone. A mobile ratio remains, but derived from the asset (3/5) rather than chosen to crop a desktop one, and the
object-positionhack is removed — deliberately, since the stage also sizes the iframe, and without a ratio a phone tapping Play would get a 348×209 chat.The OG card was illegible in a feed
Rendered and viewed at feed size first. Everything except the headline was under 11px: the brand name was the smallest type on the card, and LangGraph and AG-UI sat inside a low-contrast grey paragraph that reads as a smear. Three pills duplicated words above them at 6px. A 120px dead band sat in the middle.
Four elements now, nothing below 30px source, sized against a 0.42× rendering, on a dark substrate so the card has an edge against a white timeline. Centred rather than left-aligned — the left-aligned version was built first, then centre-cropped the way compact surfaces do, and "Threadplane" became "readplane".
Copy stays single-sourced, and the wordmark is derived from the tagline rather than retyped so the card and the page title cannot disagree. Contrast is 16.8:1 on primary text.
The card's alt and dimensions never shipped
Next's file-convention metadata is overridden the moment
openGraph.imagesis set explicitly, and both the layout and the page-metadata helper passed a bare URL string. Production emittedog:imagealone: no width or height for a platform to lay the card out before fetching it, and no alt text at all. Now an object carrying all four, with a spec pinning them.Verification
Website and scripts suites green, 0 lint errors, production build succeeds with the OG route prerendered. Posters read at full size and in the rendered hero at 390, 768 and 1440.
Known and left alone: the OG body font is fetched from Google at build time and failed once mid-session, degrading to all-serif — documented as best-effort in
og-font.ts.🤖 Generated with Claude Code