fix(website): lead with the AI-first, production-ready positioning - #1460
Merged
Conversation
The hero, the README, the "What is WebJs?" page and the site-wide meta
description all led on transparency ("Nothing is hidden from your agent"),
which describes a property of the framework rather than what someone gets
for adopting it. The promise a reader is weighing is whether the first
thing an agent builds is architecture they can ship, so the surfaces now
say that and keep the transparency claim as the paragraph it supports.
One verbatim string on every surface, "production-ready architecture from
your very first prompt", so a search result, a README and the page it
opens cannot drift into three different promises. The meta description
holds at 154 characters, under the point Google truncates.
A social card is a static image with no theme to follow, and it is read inside someone else's surface: an X timeline, a Slack unfurl, an iMessage bubble. Those are overwhelmingly light, and the near-black card sat in them as a hole rather than as a card. The light translation of the site's own tokens reads as the same brand and survives the thumbnail sizes an unfurl actually renders, where a top accent bar is the only element still legible. The card also carries the new positioning, so a shared link and the page it opens say the same sentence. Two robustness changes come with it. The three faces are inlined as data URIs instead of fetched from Google Fonts, so the render has no network to fail and cannot silently ship a card in a system fallback face. And a fit pass steps the headline down until the card holds it, since a 1200x630 card has nowhere for overflow to go and a headline one word too long is cut off in every unfurl with nothing in the render to signal it.
The card was three kinds of stale at once, and a generated image is where that survives longest, because it is rendered once and then only ever seen inside somebody else's timeline. It claimed the framework is one an agent "can read end to end", and that the agent "reads the whole framework and fits it into context". Both are false, and the repo already knew: the comment above DESCRIPTION in app/layout.ts records that packages/core/src alone is 23,465 lines and core plus server is 50,511. That comment was written when the same sentence was cut from the meta description, and the card kept saying it. The page's own body had already moved on, so the card was also the only surface still making the claim. The true version is about LOCATION rather than volume, which the page has said all along: the agent opens the file it is calling instead of recalling an API from training data. It is the stronger claim too, since it holds however large the framework grows. The same correction goes into the page's purpose comment, which still described the retired argument. It also drew its own logo, a gradient tile beside the word "webjs" set in Inter Tight, which is a redraw of a mark that has since changed. It now carries the real on-light lockup file, so it cannot drift again. And it is light, matching the home card, with the same top accent bar, inlined faces and headline fit pass.
The strip read "AI-FIRST / WEB-COMPONENTS-FIRST / NO BUILD", and two of three claims ending the same way landed as a tic rather than as two separate stances. "-first" was also the wrong word for the middle one. Web components are not a preference this framework ranks highly, they are its component model, the way Next is React-based rather than React-first. As a bare fact it now matches the register of NO BUILD beside it. The hyphens went with the suffix: they only ever bound the compound modifier, and the platform feature is two plain words, which is how the site writes it in prose everywhere else.
The home hero, /what-is-webjs and /why-webjs each rendered their own
hero, and the two inner pages were inverted against the landing page:
a BIGGER h1 (--text-display, 82.4px at 1280 and 46.4px at 390, against
the home's 66 and 29.9) over a SMALLER lede (--text-lede, 23.7px against
30.4px). Landing on one from the home page read as a different site.
The home page was also the one still carrying an inline clamp, which is
the exact thing the comment above --text-display says was wrong ("what
was wrong was repeating the same clamp inline on page after page. Named
once, used everywhere"). That curve is now --text-hero-h1 and all three
heroes read it, so the next tuning pass lands in one place.
It stays a separate step from --text-display rather than replacing it,
because the two have different floors and --text-display's 2.9rem is
reached by its preferred term at a 476px viewport, so every phone got one
identical size. /brand keeps --text-display and is untouched.
Also matches the top padding (pt-12 md:pt-20 lg:pt-28 and mt-2 on the h1,
against md:pt-16 lg:pt-24 and mt-4), so the three heroes start at the
same height. Measured after: h1 66px, lede 30.4px and 112px of section
padding on all three at 1280.
The previous commit changed the lede SIZE without its leading, and leading is coupled to size: leading-[1.6] was a 38px rhythm at 23.7px and became a 48.6px one at 30.4px, which is why the sub-header went slack. The home hero runs leading-[1.3] at exactly this size. The measure and the line breaking were still diverging too, and they are what gives the home subtitle its shape: max-w-[64rem], the same box its H1 uses, with text-balance so the browser evens the lines rather than filling greedily. The inner pages were on a 56ch box with text-pretty, which at this size computed WIDER than the home page's (1074px against 1024px) while breaking differently. All three now measure 30.4px on a 39.5px line height in a 1024px box with balanced lines.
Matching the type scale was not enough here, because the difference from /what-is-webjs was structural. That page states its definition in bold, finishes the sentence in muted at the same size, then drops to a smaller supporting paragraph, so the hero arrives in three legible tiers. This one put all 380 characters in the lede tier as one flat block, which is why it read as a wall at the larger size rather than as a subtitle. Split at the same seams: the definition sentence is bold, one sentence finishes the lede, and the two remaining sentences move down to the text-base tier. Nothing is cut. Every claim the hero made it still makes, one tier lower. The headline was also boxed at max-w-[16ch], which forced three lines of 66px type. It reads from the same max-w-[64rem] the home hero and the lede use, so text-balance evens it into two.
/what-is-webjs had no card of its own and unfurled with the site-wide og.png, whose headline is the product tagline. That page exists to answer one question (its title, h1 and slug all match it, a definition sits in the first 160 characters, and a visible FAQ backs FAQPage JSON-LD), so it is the likeliest of the three to be shared AS an answer, and it was the one arriving under a card that answered something else. Adding it would have made three near-identical generators, which is the count at which a fix stops landing on every card. That is not hypothetical: it is how the /why card kept a retired logo and a claim the site had already corrected. So the shell moved to scripts/lib/og-card.mjs (palette, inlined faces, lockup, top row, panels, footer, render, fit pass) and each card now owns only its content. It is deliberately not a card framework: a card that wants a different structure passes its own CSS. The two fact panels are shared the same way, and the home card adopts them. It was a headline capped at 19ch over a lede at 34ch, so the right half of a 1200px card was empty; the panels give it the composition the /why card already had. The kicker also moves into the shell, so all three carry BUILT FOR THE AI ERA opposite the lockup rather than one of them having it. The /why card is visually unchanged by the refactor.
The panels are two facets of one claim, not step one and step two, so numbering them asserted an order that is not there and made the eyebrow read as a list item rather than as a label. Each now starts on its own title. The .cnum rule goes with them, and the eyebrow's flex row does too, since it existed to sit the numeral beside the label.
The panels sat against the rule because hr carried margin-bottom and no margin-top, so it kept its distance from the footer text and none at all from whatever the middle block ended on. Adding the missing side exposed a worse bug underneath: the fit pass has never done anything. It tested frame.scrollHeight <= frame.clientHeight, and the frame is a fixed-height flex container inside an overflow:hidden body, so scrollHeight is pinned to clientHeight however far the content spills. The loop returned on its first iteration every time. All three cards were rendering their footers OUTSIDE the bottom padding, by 39px on the fullest one, and the render still produced a plausible png. It now tests the footer's bottom against the frame's padding box, which is the thing that actually has to hold, and it warns loudly when even the smallest step overflows, because that means the copy is too long rather than the type too big. With a real test the rhythm had to be real too. It moves into the shell (one .mid with a gap and a top pad) rather than being restated per card, which is how buying clearance above the footer got paid for out of the gap under the lockup and collided the headline with it. Frame padding comes in to 56px to buy the room back. The why card is the fullest and now sets its headline at 49px, chosen by the fit pass rather than by hand.
The two labels above the side-by-side terminals are a matched pair, and only one of them fit. At text-xs with tracking-widest in a 372px column, "Your app code, served to the browser as written" ran 47 characters against its sibling's 39 and wrapped, so the two windows started at different heights. Shortened to "Your app code, served as written", which also makes the pair parallel: a noun phrase, a comma, a participial phrase, on both sides. Nothing is lost by dropping "to the browser", since the terminal directly below it curls localhost and its own caption says the browser fetched it.
vivek7405
added a commit
that referenced
this pull request
Sep 2, 2026
…st (#1461) test/bun/dev-public-before-warm.mjs failed CI on #1460 with a bare [TypeError: fetch failed] whose cause was "bad port", naming nothing in the code under test. The port is derived as base + (process.pid % n), and the range was 10000-10255. 10080 is the last entry on the WHATWG Fetch bad-ports list, so fetch() rejects it before opening a socket and no server is involved in the failure at all. A run whose pid was 80 mod 256 therefore failed every request in the file. That is about one run in 256, and it is deterministic given the pid rather than timing-dependent, which is exactly what makes it read as flake and survive a re-run. dev-morph-verdict.mjs had the same exposure on 9990-10229 and had simply not been unlucky yet. Both are now based past 10080: 10100-10355 and 10400-10639. Scanning 9400-10700 confirms 10080 is the only blocked port in the neighbourhood, so any base above it is permanently safe. The ranges stay disjoint from each other and remain entirely above the 9989 ceiling that the existing comment in dev-public-before-warm reasons about, so its collision argument is preserved. That comment was not wrong, it just predated the blocklist constraint, and it now records both.
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.
Replaces the transparency-led positioning ("Nothing is hidden from your agent") with what a reader actually gets, then follows the consequences through the marketing pages and the social cards.
Copy
One verbatim string on every surface, "production-ready architecture from your very first prompt", so a search result, the README and the page it opens cannot drift into three different promises.
README.md/what-is-webjsThe meta description holds at 154 characters, under the point Google truncates (the old one was 158). It trades the feature keywords for the promise sentence, which was a deliberate call rather than a fit accident;
og-what.pngpicks those keywords back up.Nothing was deleted from the transparency argument. It keeps its paragraph on both the README and
/what-is-webjs, now introduced by the promise it backs up.Marketing page heroes
The three heroes each rendered their own type, and the two inner pages were inverted against the landing page: a bigger h1 (82.4px at 1280 against the home's 66) over a smaller lede (23.7px against 30.4). Landing on one from the home page read as a different site.
The home page was also the one still carrying an inline
clamp(), which is the exact thing the comment above--text-displaysays was wrong ("Named once, used everywhere"). That curve is now--text-hero-h1and all three heroes read it. It stays a separate step from--text-display, which/brandstill uses and which is untouched, because the two want different floors.Measured after, at 1280: h1 66px, lede 30.4px on a 39.5px line height in a 1024px box with balanced lines, and 112px of section padding, identical on all three.
/why-webjsneeded more than type. Its hero put all 380 characters in the lede tier as one flat block, where/what-is-webjsstates its definition in bold, finishes in muted at the same size, then drops to a smaller supporting paragraph. It now splits at the same seams. Nothing is cut; every claim it made it still makes, one tier lower. Its headline also came out of amax-w-[16ch]box that forced three lines of 66px type.Also: the second terminal eyebrow ran 47 characters against its sibling's 39 in a 372px column and wrapped, starting the two windows at different heights.
Social cards
All three are now light instead of near-black. A social card is a static image with no theme to follow, read inside someone else's surface (an X timeline, a Slack unfurl, an iMessage bubble). Those are overwhelmingly light, and the dark cards sat in them as a hole. At thumbnail size the headline is unreadable either way, so the accent bar does the recognition work.
og-what.pngis new./what-is-webjshad no card and unfurled with the tagline card. It is the page built to answer one question (matching title, h1 and slug, a definition in the first 160 characters, a visible FAQ backing FAQPage JSON-LD), so it is the likeliest of the three to be shared AS an answer, and it was arriving under a card that answered something else.The
/why-webjscard was also wrong. It claimed the framework is one an agent "can read end to end", and that the agent "reads the whole framework and fits it into context". Both are false and the repo already knew: the comment aboveDESCRIPTIONinapp/layout.tsrecords thatpackages/core/srcalone is 23,465 lines and core plus server is 50,511. That comment was written when the sentence was cut from the meta description, and the card kept saying it. The page's body had already moved on, so the card was the last surface making the claim. The true version is about location, not volume, which is also the stronger claim since it holds however large the framework grows. The same correction went into the page's purpose comment. The card also drew its own logo, a redraw of a mark that has since changed, and now reads the real lockup file.One shell, three cards. Adding a third generator would have made three near-identical copies, which is the count at which a fix stops landing on every card, and that is not hypothetical: it is how the why card kept a retired logo and a corrected claim.
scripts/lib/og-card.mjsowns the palette, inlined faces, lockup, top row, panels, footer, rhythm and render; each card file is its own content. It is deliberately not a card framework.The fit pass had never done anything. It tested
frame.scrollHeight <= frame.clientHeight, and the frame is a fixed-height flex container inside anoverflow:hiddenbody, soscrollHeightis pinned toclientHeighthowever far content spills. The loop returned on its first iteration every time, and all three cards were rendering their footers outside the bottom padding, by 39px on the fullest one, while still producing a plausible png. It now tests the footer's bottom against the frame's padding box and warns loudly when even the smallest step overflows, since that means the copy is too long rather than the type too big. The why card's headline is now set at 49px by that pass rather than by hand.Other card changes: fonts inlined as data URIs instead of fetched from Google Fonts, so a render has no network to fail and cannot silently ship a card in a system fallback face; the home card gained the two fact panels, because a headline capped at 19ch left half of a 1200px canvas empty;
BUILT FOR THE AI ERAmoved into the shell so all three carry it; and the panel numerals went, since the two panels are facets of one claim rather than step one and step two.Verification
webjs checkpasses inwebsite/.webjs doctor: 11 passed, 3 warnings, 0 failed, all pre-existing. The only new-looking one isSTATIC_ASSET_FRESHNESSon the gitignoredpublic/tailwind.css, which dev rebuilds on request.<meta name="description">andog:imageconfirmed against a running dev server;/why-webjsand/what-is-webjsboth return 200.Not in this PR
Other homes for the retired tagline:
README.mdline 3 ("The web framework for AI agents."), thegallery/app/layout.tsfooter, and the per-packagepackage.jsondescriptions.