From e3317f327fca0502763633ef29aafe19d28251c3 Mon Sep 17 00:00:00 2001 From: Vivek Date: Fri, 31 Jul 2026 14:47:51 +0530 Subject: [PATCH 1/5] feat(website): sell plain-language prompts and cross-model quality The /why-webjs page argued model-agnosticism only from the model side: any model can read the source, so no training data is required. That leaves out the half a reader actually cares about. Someone with no technical background can write an ordinary request and still get correct routing, a real schema, server code on the right side of the boundary, and a coherent design system, because the conventions decide the shape before the prompt is written. Adds a section making that argument with a prompt-to-files pairing, and extends the any-model section to say the output quality holds across model sizes rather than only that a small model can participate. --- website/app/what-is-webjs/page.ts | 2 +- website/app/why-webjs/page.ts | 57 +++++++++++++++++++++++++- website/test/ssr/why-webjs-ssr.test.ts | 11 +++++ 3 files changed, 68 insertions(+), 2 deletions(-) diff --git a/website/app/what-is-webjs/page.ts b/website/app/what-is-webjs/page.ts index c784d9164..27180a2ab 100644 --- a/website/app/what-is-webjs/page.ts +++ b/website/app/what-is-webjs/page.ts @@ -161,7 +161,7 @@ const PROSE = 'text-fg-muted text-base leading-[1.7] m-0'; const CAPABILITIES = [ { title: 'AI-first, readable end to end', - body: 'Predictable file conventions, one server function per file, and an explicit .server.ts boundary keep the context a change needs small, so an agent can edit one route without reading the whole app. Every app ships an AGENTS.md contract that Claude Code, Cursor, Copilot, Gemini, and opencode read from one source, and the framework itself is plain JavaScript with JSDoc in node_modules rather than a compiled bundle.', + body: 'Predictable file conventions, one server function per file, and an explicit .server.ts boundary keep the context a change needs small, so an agent can edit one route without reading the whole app. Because those conventions decide the shape, a non-technical request in ordinary words still produces a well-structured app, and the result holds up across models of very different sizes. Every app ships an AGENTS.md contract that Claude Code, Cursor, Copilot, Gemini, and opencode read from one source, and the framework itself is plain JavaScript with JSDoc in node_modules rather than a compiled bundle.', }, { title: 'Web components, server-rendered', diff --git a/website/app/why-webjs/page.ts b/website/app/why-webjs/page.ts index 2b4f4e8c6..e0f6f1a7e 100644 --- a/website/app/why-webjs/page.ts +++ b/website/app/why-webjs/page.ts @@ -24,7 +24,7 @@ export function generateMetadata(ctx: { url: string }) { const image = `${origin}/public/og-why.png`; const title = 'Why WebJs - The Framework Your AI Agent Already Understands'; const description = - 'WebJs is a full-stack JavaScript framework that serves the framework source and your app code exactly as written, so any AI model can read the whole stack from node_modules, reason about it, and debug it. No training data required, no single blessed model. Built on the web components, HTML, and JavaScript every model already knows.'; + 'WebJs is a full-stack JavaScript framework that serves the framework source and your app code exactly as written, so any AI model can read the whole stack from node_modules, reason about it, and debug it. No training data required, no single blessed model. Describe an app in plain language and the file conventions land it in the right shape, whatever model you use. Built on the web components, HTML, and JavaScript every model already knows.'; return { title, description, @@ -166,6 +166,52 @@ Counter.register('counter'); +
+
+
+

Describe what you want in plain language

+

+ You do not have to know what a route handler or a server action is + to ask for one. Say what the app should do, the way you would say it + out loud, and the agent still lands on the right structure, because + the structure was decided before the prompt was written. Where a + page lives, where a form submission is handled, and where database + code is allowed to run are all answered by file and folder + convention, so the model fills in a shape instead of inventing an + architecture from a sentence. +

+
+
+
+

What you ask for

+
+
${DOTS}prompt
+
> Build a page where customers can book a
+  table, save the bookings, and let staff
+  see today's list.
+
+# no framework terms, no file names, no
+# architecture. just the thing you want.
+
+
+
+

Where the conventions put it

+
+
${DOTS}terminal
+
app/book/page.ts
+  the page, server-rendered to real HTML
+app/staff/bookings/page.ts
+  the list, behind the session check
+modules/bookings/actions/create.server.ts
+  the write, server-side by extension
+db/schema.server.ts
+  the table it all reads and writes
+
+
+
+
+
+
@@ -177,6 +223,15 @@ Counter.register('counter'); context and gets to work. Switch models between tasks and the output stays reliable, because they are all reading the same readable code.

+

+ That shows up in the quality of what comes back, not just in whether + a model can work at all. Large or small, every model is reading the + same source and following the same conventions, so the architecture + it reaches for, the code it writes, and the design system it applies + come out looking like one project rather than three. You still read + what an agent hands you. What you stop doing is re-deciding the + shape of the app every time you change model. +

Human developers get the same deal. There is no hidden compiler output to reverse engineer when something breaks. You open the file, diff --git a/website/test/ssr/why-webjs-ssr.test.ts b/website/test/ssr/why-webjs-ssr.test.ts index e6a9e886e..aa3c07775 100644 --- a/website/test/ssr/why-webjs-ssr.test.ts +++ b/website/test/ssr/why-webjs-ssr.test.ts @@ -24,6 +24,17 @@ test('the pitch page SSRs with its headline, terminals, reason cards, and a main assert.ok(out.includes('

{ + // The page used to argue model-agnosticism only from the model side (any + // model CAN read the source). These two claims are the reader-side half: a + // non-technical prompt still lands on the right structure, and the quality + // of the output does not swing with the size of the model. + const out = await renderToString(Why()); + assert.ok(out.includes('Describe what you want in plain language'), 'includes the plain-language section heading'); + assert.ok(out.includes('architecture from a sentence'), 'says the conventions decide the shape, not the prompt'); + assert.ok(out.includes('quality of what comes back'), 'ties model-agnosticism to output quality, not just to whether a model works'); +}); + test('why metadata is self-consistent and points at the dedicated /why-webjs social card', () => { const m = generateMetadata({ url: 'https://webjs.dev/why-webjs' }); assert.equal(m.openGraph.title, m.title, 'og:title matches the '); From b57b139fb80191b29344ccd72a6f912afd3d45b5 Mon Sep 17 00:00:00 2001 From: Vivek <vivek7405@gmail.com> Date: Fri, 31 Jul 2026 14:57:58 +0530 Subject: [PATCH 2/5] fix(website): keep the cross-model claim to what the framework backs Review caught the design-system half of the any-model paragraph as an overclaim. AGENTS.md says the opposite outright: there is no design gate, taste is the agent's job, and an app sets its own palette in the root layout. The paragraph now credits the real mechanism, design tokens set in one place, and says plainly that taste is the reader's to direct. Also from review: the meta description had grown to 444 chars with the new clause sitting past every truncation point it feeds, so the claim now leads it instead; the what-is-webjs card is back to roughly its pre-change length, since h-full made the longest cell pin its row and buy dead space in both neighbours; the two demo panels now answer each other (the prompt no longer says page while the result shows two); the static file listing is labelled files rather than terminal; and the four scrollable pre blocks on the page carry role=region, so the aria-label each already had is one ARIA permits. --- website/app/what-is-webjs/page.ts | 2 +- website/app/why-webjs/page.ts | 29 ++++++++++++++--------------- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/website/app/what-is-webjs/page.ts b/website/app/what-is-webjs/page.ts index 27180a2ab..475709e4a 100644 --- a/website/app/what-is-webjs/page.ts +++ b/website/app/what-is-webjs/page.ts @@ -161,7 +161,7 @@ const PROSE = 'text-fg-muted text-base leading-[1.7] m-0'; const CAPABILITIES = [ { title: 'AI-first, readable end to end', - body: 'Predictable file conventions, one server function per file, and an explicit .server.ts boundary keep the context a change needs small, so an agent can edit one route without reading the whole app. Because those conventions decide the shape, a non-technical request in ordinary words still produces a well-structured app, and the result holds up across models of very different sizes. Every app ships an AGENTS.md contract that Claude Code, Cursor, Copilot, Gemini, and opencode read from one source, and the framework itself is plain JavaScript with JSDoc in node_modules rather than a compiled bundle.', + body: 'Predictable file conventions, one server function per file, and an explicit .server.ts boundary keep the context small, so an agent edits one route without reading the whole app, and a request written in ordinary words still lands in the right shape. Every app ships one AGENTS.md contract that Claude Code, Cursor, Copilot, Gemini, and opencode all read, and the framework is plain JavaScript rather than a compiled bundle.', }, { title: 'Web components, server-rendered', diff --git a/website/app/why-webjs/page.ts b/website/app/why-webjs/page.ts index e0f6f1a7e..0a48b54f8 100644 --- a/website/app/why-webjs/page.ts +++ b/website/app/why-webjs/page.ts @@ -24,7 +24,7 @@ export function generateMetadata(ctx: { url: string }) { const image = `${origin}/public/og-why.png`; const title = 'Why WebJs - The Framework Your AI Agent Already Understands'; const description = - 'WebJs is a full-stack JavaScript framework that serves the framework source and your app code exactly as written, so any AI model can read the whole stack from node_modules, reason about it, and debug it. No training data required, no single blessed model. Describe an app in plain language and the file conventions land it in the right shape, whatever model you use. Built on the web components, HTML, and JavaScript every model already knows.'; + 'WebJs is a full-stack JavaScript framework you can describe in plain language, because the file conventions land an app in the right shape whatever model you use. Nothing is hidden behind a build step, so any AI model reads the framework source from node_modules, reasons about the whole stack, and debugs it. No training data required, no single blessed model, on the web components and HTML every model already knows.'; return { title, description, @@ -118,7 +118,7 @@ export default function Why() { <p class="font-mono font-semibold text-xs leading-[1.4] tracking-widest uppercase text-fg-subtle mb-2.5 ml-1">The framework, readable in node_modules</p> <figure class=${WIN}> <figcaption class=${WINBAR}>${DOTS}<span class=${WINNAME}>terminal</span></figcaption> - <pre class="scroll-thin m-0 p-4 overflow-x-auto font-mono text-sm leading-[1.7] [tab-size:2] flex-1" tabindex="0" aria-label="Listing and grepping the framework source in node_modules"><code><span class="text-accent">$</span> ls node_modules/@webjsdev/core/src + <pre class="scroll-thin m-0 p-4 overflow-x-auto font-mono text-sm leading-[1.7] [tab-size:2] flex-1" role="region" tabindex="0" aria-label="Listing and grepping the framework source in node_modules"><code><span class="text-accent">$</span> ls node_modules/@webjsdev/core/src component.js html.js render-client.js css.js directives.js render-server.js serialize.js router-client.js @@ -134,7 +134,7 @@ server/src/ssr.js: const html = await renderToString(tree) <p class="font-mono font-semibold text-xs leading-[1.4] tracking-widest uppercase text-fg-subtle mb-2.5 ml-1">Your app code, served to the browser as written</p> <figure class=${WIN}> <figcaption class=${WINBAR}>${DOTS}<span class=${WINNAME}>terminal</span></figcaption> - <pre class="scroll-thin m-0 p-4 overflow-x-auto font-mono text-sm leading-[1.7] [tab-size:2] flex-1" tabindex="0" aria-label="Fetching an app module served unbundled"><code><span class="text-accent">$</span> curl localhost:5001/components/counter.ts + <pre class="scroll-thin m-0 p-4 overflow-x-auto font-mono text-sm leading-[1.7] [tab-size:2] flex-1" role="region" tabindex="0" aria-label="Fetching an app module served unbundled"><code><span class="text-accent">$</span> curl localhost:5001/components/counter.ts import { WebComponent } from '@webjsdev/core'; class Counter extends WebComponent({ count: Number }) { @@ -186,9 +186,8 @@ Counter.register('counter'); <p class="font-mono font-semibold text-xs leading-[1.4] tracking-widest uppercase text-fg-subtle mb-2.5 ml-1">What you ask for</p> <figure class=${WIN}> <figcaption class=${WINBAR}>${DOTS}<span class=${WINNAME}>prompt</span></figcaption> - <pre class="scroll-thin m-0 p-4 overflow-x-auto font-mono text-sm leading-[1.7] [tab-size:2] flex-1" tabindex="0" aria-label="A plain-language prompt with no technical terms in it"><code><span class="text-accent">></span> Build a page where customers can book a - table, save the bookings, and let staff - see today's list. + <pre class="scroll-thin m-0 p-4 overflow-x-auto font-mono text-sm leading-[1.7] [tab-size:2] flex-1" role="region" tabindex="0" aria-label="A plain-language prompt with no technical terms in it"><code><span class="text-accent">></span> Let customers book a table, save the + bookings, and give staff today's list. <span class="text-fg-subtle"># no framework terms, no file names, no</span> <span class="text-fg-subtle"># architecture. just the thing you want.</span></code></pre> @@ -197,8 +196,8 @@ Counter.register('counter'); <div class="flex flex-col min-w-0"> <p class="font-mono font-semibold text-xs leading-[1.4] tracking-widest uppercase text-fg-subtle mb-2.5 ml-1">Where the conventions put it</p> <figure class=${WIN}> - <figcaption class=${WINBAR}>${DOTS}<span class=${WINNAME}>terminal</span></figcaption> - <pre class="scroll-thin m-0 p-4 overflow-x-auto font-mono text-sm leading-[1.7] [tab-size:2] flex-1" tabindex="0" aria-label="The files a WebJs app grows for that request"><code>app/book/page.ts + <figcaption class=${WINBAR}>${DOTS}<span class=${WINNAME}>files</span></figcaption> + <pre class="scroll-thin m-0 p-4 overflow-x-auto font-mono text-sm leading-[1.7] [tab-size:2] flex-1" role="region" tabindex="0" aria-label="The files a WebJs app grows for that request"><code>app/book/page.ts <span class="text-fg-subtle"> the page, server-rendered to real HTML</span> app/staff/bookings/page.ts <span class="text-fg-subtle"> the list, behind the session check</span> @@ -224,13 +223,13 @@ db/schema.server.ts stays reliable, because they are all reading the same readable code. </p> <p class="text-fg-muted text-base leading-[1.6] m-0 mb-4"> - That shows up in the quality of what comes back, not just in whether - a model can work at all. Large or small, every model is reading the - same source and following the same conventions, so the architecture - it reaches for, the code it writes, and the design system it applies - come out looking like one project rather than three. You still read - what an agent hands you. What you stop doing is re-deciding the - shape of the app every time you change model. + That shows up in the quality of what comes back, not only in whether + a model can participate. Routing, the server boundary, and the file + layout are settled by convention, and the palette lives in design + tokens the root layout sets once, so a smaller model is filling + those in rather than inventing them. Taste is still yours to + direct, and you still read what an agent hands you. What you stop + doing is re-deciding the shape of the app every time you switch. </p> <p class="text-fg-muted text-base leading-[1.6] m-0"> Human developers get the same deal. There is no hidden compiler From 00487b5afd2d656978bb673d9d950d7472297744 Mon Sep 17 00:00:00 2001 From: Vivek <vivek7405@gmail.com> Date: Fri, 31 Jul 2026 15:09:11 +0530 Subject: [PATCH 3/5] fix(website): restore the claims the trim broke and cover the demo grid The previous commit traded accuracy for grid height and lost. Trimming the what-is-webjs card cut the two phrases that were carrying it: without 'with JSDoc in node_modules' the framework claim is flatly false, since core resolves its default entry to dist/webjs-core.js, and without 'read from one source' the AGENTS.md sentence claims five tools read a file three of them do not. Both phrases are back, and the length is won by dropping a convention detail instead. The demo grid was also entirely unasserted, so it could have been deleted with every test in the file still green. It now pins both panels, their correspondence, and the accessible name each block carries, plus a snippet-window assertion mirroring the one on the sibling page. The role=region fix goes site-wide rather than stopping at one page: the prohibited-name construct was on ten more blocks. The one block that is not focusable loses its name instead of gaining a role, since nothing can focus it to hear one. --- website/app/page.ts | 10 ++++----- website/app/what-is-webjs/page.ts | 4 ++-- website/app/why-webjs/page.ts | 4 ++-- website/test/ssr/why-webjs-ssr.test.ts | 29 ++++++++++++++++++++++++++ 4 files changed, 38 insertions(+), 9 deletions(-) diff --git a/website/app/page.ts b/website/app/page.ts index 4e79712f1..603046c0b 100644 --- a/website/app/page.ts +++ b/website/app/page.ts @@ -103,7 +103,7 @@ function codeWindow(title: string, sample: string) { return html` <figure class=${WIN}> <figcaption class=${WINBAR}>${DOTS}<span class=${WINNAME}>${title}</span></figcaption> - <pre class="scroll-thin m-0 p-4 overflow-x-auto font-mono text-sm leading-[1.7] [tab-size:2] flex-1" tabindex="0" aria-label=${title + ' code sample'}><code>${highlight(sample)}</code></pre> + <pre class="scroll-thin m-0 p-4 overflow-x-auto font-mono text-sm leading-[1.7] [tab-size:2] flex-1" role="region" tabindex="0" aria-label=${title + ' code sample'}><code>${highlight(sample)}</code></pre> </figure> `; } @@ -216,7 +216,7 @@ export default function LandingPage() { <div class="hero-stage max-w-5xl mx-auto grid grid-cols-1 wide:grid-cols-2 rounded-2xl overflow-hidden border border-border-strong bg-bg-sunken shadow-[var(--shadow)]"> <div class="min-w-0 border-b wide:border-b-0 wide:border-r border-border bg-bg-subtle"> <div class=${WINBAR}>${DOTS}<span class=${WINNAME}>components/like-button.ts</span></div> - <pre class="scroll-thin m-0 p-5 overflow-x-auto font-mono text-xs leading-[1.72] [tab-size:2] text-left" tabindex="0" aria-label="like-button component source"><code>${highlight(HERO_SAMPLE)}</code></pre> + <pre class="scroll-thin m-0 p-5 overflow-x-auto font-mono text-xs leading-[1.72] [tab-size:2] text-left" role="region" tabindex="0" aria-label="like-button component source"><code>${highlight(HERO_SAMPLE)}</code></pre> </div> <div class="group/stage flex flex-col min-w-0 bg-bg"> <input type="checkbox" id="stage-usage" class="sr-only peer" /> @@ -232,7 +232,7 @@ export default function LandingPage() { <div class="flex-1 grid place-items-center px-6 py-10 group-has-[:checked]/stage:hidden"> <like-button count="3"></like-button> </div> - <pre class="hidden group-has-[:checked]/stage:block flex-1 m-0 p-5 overflow-x-auto font-mono text-xs leading-[1.72] text-left" aria-label="like-button usage"><code>${highlight(USAGE_SAMPLE)}</code></pre> + <pre class="hidden group-has-[:checked]/stage:block flex-1 m-0 p-5 overflow-x-auto font-mono text-xs leading-[1.72] text-left"><code>${highlight(USAGE_SAMPLE)}</code></pre> <div class="px-4 py-3 border-t border-border text-center font-mono text-xs leading-[1.5] text-fg-subtle"> Server-rendered first, then upgraded. Click it. </div> @@ -384,7 +384,7 @@ export default function LandingPage() { <span class="text-xs font-medium leading-none text-accent">Full-stack</span> <h3 class="font-display font-bold text-lg leading-[1.25] m-0">Pages + API + components</h3> <p class="m-0 text-sm leading-[1.6] text-fg-muted">SSR pages, web components, server actions, Drizzle, streaming, and a browsable feature gallery. Auth (login, sessions, a protected route) ships as a gallery card. The default.</p> - <pre class="scroll-thin m-0 px-3.5 py-3 overflow-x-auto rounded-lg border border-border bg-bg-subtle font-mono text-xs leading-[1.6] text-fg-muted" tabindex="0" aria-label="Example files">app/page.ts + <pre class="scroll-thin m-0 px-3.5 py-3 overflow-x-auto rounded-lg border border-border bg-bg-subtle font-mono text-xs leading-[1.6] text-fg-muted" role="region" tabindex="0" aria-label="Example files">app/page.ts components/counter.ts actions/posts.server.ts</pre> <div class="cmd-foot pt-2 mt-auto font-mono text-xs leading-[1.6] text-fg-muted max-w-full min-w-0"><copy-cmd>npm create webjs@latest my-app</copy-cmd></div> @@ -393,7 +393,7 @@ actions/posts.server.ts</pre> <span class="text-xs font-medium leading-none text-accent">Backend (API)</span> <h3 class="font-display font-bold text-lg leading-[1.25] m-0">Route handlers + Database</h3> <p class="m-0 text-sm leading-[1.6] text-fg-muted">A backend-only app, no UI or SSR. File-based route handlers, modules, middleware, rate limiting, WebSockets, a database, and a backend-features gallery.</p> - <pre class="scroll-thin m-0 px-3.5 py-3 overflow-x-auto rounded-lg border border-border bg-bg-subtle font-mono text-xs leading-[1.6] text-fg-muted" tabindex="0" aria-label="Example files">app/api/users/route.ts + <pre class="scroll-thin m-0 px-3.5 py-3 overflow-x-auto rounded-lg border border-border bg-bg-subtle font-mono text-xs leading-[1.6] text-fg-muted" role="region" tabindex="0" aria-label="Example files">app/api/users/route.ts app/api/chat/route.ts middleware.ts</pre> <div class="cmd-foot pt-2 mt-auto font-mono text-xs leading-[1.6] text-fg-muted max-w-full min-w-0"><copy-cmd>npm create webjs@latest my-api -- --template api</copy-cmd></div> diff --git a/website/app/what-is-webjs/page.ts b/website/app/what-is-webjs/page.ts index 475709e4a..5022bb4f2 100644 --- a/website/app/what-is-webjs/page.ts +++ b/website/app/what-is-webjs/page.ts @@ -161,7 +161,7 @@ const PROSE = 'text-fg-muted text-base leading-[1.7] m-0'; const CAPABILITIES = [ { title: 'AI-first, readable end to end', - body: 'Predictable file conventions, one server function per file, and an explicit .server.ts boundary keep the context small, so an agent edits one route without reading the whole app, and a request written in ordinary words still lands in the right shape. Every app ships one AGENTS.md contract that Claude Code, Cursor, Copilot, Gemini, and opencode all read, and the framework is plain JavaScript rather than a compiled bundle.', + body: 'Predictable file conventions and an explicit .server.ts boundary decide the shape of an app, so an agent edits one route without reading the whole app, and ordinary words are enough to ask for a new one. Every app ships an AGENTS.md contract that Claude Code, Cursor, Copilot, Gemini, and opencode read from one source, and the framework itself is plain JavaScript with JSDoc in node_modules rather than a compiled bundle.', }, { title: 'Web components, server-rendered', @@ -207,7 +207,7 @@ function codeWindow(title: string, sample: string, label: string) { return html` <figure class=${WIN}> <figcaption class=${WINBAR}>${DOTS}<span class=${WINNAME}>${title}</span></figcaption> - <pre class="scroll-thin m-0 p-4 overflow-x-auto font-mono text-sm leading-[1.7] [tab-size:2] flex-1" tabindex="0" aria-label=${label}><code>${highlight(sample)}</code></pre> + <pre class="scroll-thin m-0 p-4 overflow-x-auto font-mono text-sm leading-[1.7] [tab-size:2] flex-1" role="region" tabindex="0" aria-label=${label}><code>${highlight(sample)}</code></pre> </figure> `; } diff --git a/website/app/why-webjs/page.ts b/website/app/why-webjs/page.ts index 0a48b54f8..5a6b80fd8 100644 --- a/website/app/why-webjs/page.ts +++ b/website/app/why-webjs/page.ts @@ -24,7 +24,7 @@ export function generateMetadata(ctx: { url: string }) { const image = `${origin}/public/og-why.png`; const title = 'Why WebJs - The Framework Your AI Agent Already Understands'; const description = - 'WebJs is a full-stack JavaScript framework you can describe in plain language, because the file conventions land an app in the right shape whatever model you use. Nothing is hidden behind a build step, so any AI model reads the framework source from node_modules, reasons about the whole stack, and debugs it. No training data required, no single blessed model, on the web components and HTML every model already knows.'; + 'WebJs is a full-stack JavaScript framework you describe in plain language, because the file conventions land an app in the right shape whatever model you use. Nothing is hidden behind a build step, so any AI model reads the framework source from node_modules, reasons about the whole stack, and debugs it. No training data required, no single blessed model, on the web components and HTML every model already knows.'; return { title, description, @@ -200,7 +200,7 @@ Counter.register('counter'); <pre class="scroll-thin m-0 p-4 overflow-x-auto font-mono text-sm leading-[1.7] [tab-size:2] flex-1" role="region" tabindex="0" aria-label="The files a WebJs app grows for that request"><code>app/book/page.ts <span class="text-fg-subtle"> the page, server-rendered to real HTML</span> app/staff/bookings/page.ts -<span class="text-fg-subtle"> the list, behind the session check</span> +<span class="text-fg-subtle"> the staff view, its own route</span> modules/bookings/actions/create.server.ts <span class="text-fg-subtle"> the write, server-side by extension</span> db/schema.server.ts diff --git a/website/test/ssr/why-webjs-ssr.test.ts b/website/test/ssr/why-webjs-ssr.test.ts index aa3c07775..531a7ed23 100644 --- a/website/test/ssr/why-webjs-ssr.test.ts +++ b/website/test/ssr/why-webjs-ssr.test.ts @@ -35,6 +35,35 @@ test('the pitch page makes the plain-language prompt and cross-model quality arg assert.ok(out.includes('quality of what comes back'), 'ties model-agnosticism to output quality, not just to whether a model works'); }); +test('the plain-language section pairs a prompt against the files the conventions produce', async () => { + // The prose above the demo carries the argument, but the two windows are what + // SHOW it, and they were previously unasserted: the whole grid could be + // deleted with every test in this file still green. These pin both panels, + // their correspondence, and the accessible name each scrollable block needs. + const out = await renderToString(Why()); + assert.ok(out.includes('Let customers book a table'), 'the prompt panel carries a request written in ordinary words'); + assert.ok(!/> Build a page/.test(out), 'the prompt does not name a page, which the file panel would then have to match one for one'); + for (const file of ['app/book/page.ts', 'app/staff/bookings/page.ts', 'modules/bookings/actions/create.server.ts', 'db/schema.server.ts']) { + assert.ok(out.includes(file), `the file panel answers the prompt with ${file}`); + } + assert.ok(out.includes('<span class="ml-2 font-mono font-medium text-xs leading-none text-fg-subtle">files</span>'), 'the file listing is labelled files, not terminal'); + // <pre> maps to role generic, where ARIA prohibits an author-supplied name, + // so every named scrollable block on this page carries an explicit role. + const named = out.match(/<pre[^>]*aria-label=/g) ?? []; + const region = out.match(/<pre[^>]*role="region"[^>]*aria-label=/g) ?? []; + assert.equal(named.length, 4, 'the page renders its four named code blocks'); + assert.equal(region.length, named.length, 'every named pre carries role=region, so the name is one ARIA permits'); +}); + +test('the /why-webjs description answers in the snippet window a SERP actually shows', async () => { + // Mirrors the assertion on /what-is-webjs. The claim this page leads with is + // only worth adding if it survives truncation, and nothing pinned that here. + const { description } = generateMetadata({ url: 'https://webjs.dev/why-webjs' }); + const firstSentence = description.slice(0, description.indexOf('. ') + 1); + assert.ok(firstSentence.length <= 160, `first sentence is ${firstSentence.length} chars, over the 160-char snippet window`); + assert.ok(firstSentence.includes('plain language'), 'the plain-language claim is inside the snippet window, not past it'); +}); + test('why metadata is self-consistent and points at the dedicated /why-webjs social card', () => { const m = generateMetadata({ url: 'https://webjs.dev/why-webjs' }); assert.equal(m.openGraph.title, m.title, 'og:title matches the <title>'); From 3eedf19cd41bcd7fee3342c2e6349a54c8803362 Mon Sep 17 00:00:00 2001 From: Vivek <vivek7405@gmail.com> Date: Fri, 31 Jul 2026 15:19:59 +0530 Subject: [PATCH 4/5] fix(website): make the new assertion discriminate and undo two a11y trades The negative assertion added last commit could never match. The prompt's chevron sits in its own span, so a regex requiring it adjacent to the prose is false whether or not the regression is present, which left the guarantee it is named for held by nothing. Plain substring now. The role sweep also traded one problem for another twice. Two template cards shared an aria-label, so promoting both to landmarks produced a duplicate-named pair where previously the prohibited name was simply ignored; each is now named for the template it shows. And the block the sweep stripped rather than promoted is the one with the real defect: it scrolls horizontally with no tabindex, so revealing it hands a keyboard user a region they cannot reach. It gets the focus stop and the name. The framework claim needed the comparative removed, not the qualifier restored: dist/webjs-core.js is what the main entry resolves to and it lives in node_modules too, so scoping the sentence there never made 'rather than a compiled bundle' true. The readable source is the part that is true, so that is all it claims now. --- website/app/page.ts | 6 +++--- website/app/what-is-webjs/page.ts | 2 +- website/test/ssr/why-webjs-ssr.test.ts | 5 ++++- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/website/app/page.ts b/website/app/page.ts index 603046c0b..da727bc23 100644 --- a/website/app/page.ts +++ b/website/app/page.ts @@ -232,7 +232,7 @@ export default function LandingPage() { <div class="flex-1 grid place-items-center px-6 py-10 group-has-[:checked]/stage:hidden"> <like-button count="3"></like-button> </div> - <pre class="hidden group-has-[:checked]/stage:block flex-1 m-0 p-5 overflow-x-auto font-mono text-xs leading-[1.72] text-left"><code>${highlight(USAGE_SAMPLE)}</code></pre> + <pre class="hidden group-has-[:checked]/stage:block flex-1 m-0 p-5 overflow-x-auto font-mono text-xs leading-[1.72] text-left" role="region" tabindex="0" aria-label="like-button usage"><code>${highlight(USAGE_SAMPLE)}</code></pre> <div class="px-4 py-3 border-t border-border text-center font-mono text-xs leading-[1.5] text-fg-subtle"> Server-rendered first, then upgraded. Click it. </div> @@ -384,7 +384,7 @@ export default function LandingPage() { <span class="text-xs font-medium leading-none text-accent">Full-stack</span> <h3 class="font-display font-bold text-lg leading-[1.25] m-0">Pages + API + components</h3> <p class="m-0 text-sm leading-[1.6] text-fg-muted">SSR pages, web components, server actions, Drizzle, streaming, and a browsable feature gallery. Auth (login, sessions, a protected route) ships as a gallery card. The default.</p> - <pre class="scroll-thin m-0 px-3.5 py-3 overflow-x-auto rounded-lg border border-border bg-bg-subtle font-mono text-xs leading-[1.6] text-fg-muted" role="region" tabindex="0" aria-label="Example files">app/page.ts + <pre class="scroll-thin m-0 px-3.5 py-3 overflow-x-auto rounded-lg border border-border bg-bg-subtle font-mono text-xs leading-[1.6] text-fg-muted" role="region" tabindex="0" aria-label="Example files in a full-stack app">app/page.ts components/counter.ts actions/posts.server.ts</pre> <div class="cmd-foot pt-2 mt-auto font-mono text-xs leading-[1.6] text-fg-muted max-w-full min-w-0"><copy-cmd>npm create webjs@latest my-app</copy-cmd></div> @@ -393,7 +393,7 @@ actions/posts.server.ts</pre> <span class="text-xs font-medium leading-none text-accent">Backend (API)</span> <h3 class="font-display font-bold text-lg leading-[1.25] m-0">Route handlers + Database</h3> <p class="m-0 text-sm leading-[1.6] text-fg-muted">A backend-only app, no UI or SSR. File-based route handlers, modules, middleware, rate limiting, WebSockets, a database, and a backend-features gallery.</p> - <pre class="scroll-thin m-0 px-3.5 py-3 overflow-x-auto rounded-lg border border-border bg-bg-subtle font-mono text-xs leading-[1.6] text-fg-muted" role="region" tabindex="0" aria-label="Example files">app/api/users/route.ts + <pre class="scroll-thin m-0 px-3.5 py-3 overflow-x-auto rounded-lg border border-border bg-bg-subtle font-mono text-xs leading-[1.6] text-fg-muted" role="region" tabindex="0" aria-label="Example files in an API app">app/api/users/route.ts app/api/chat/route.ts middleware.ts</pre> <div class="cmd-foot pt-2 mt-auto font-mono text-xs leading-[1.6] text-fg-muted max-w-full min-w-0"><copy-cmd>npm create webjs@latest my-api -- --template api</copy-cmd></div> diff --git a/website/app/what-is-webjs/page.ts b/website/app/what-is-webjs/page.ts index 5022bb4f2..264bd8c1b 100644 --- a/website/app/what-is-webjs/page.ts +++ b/website/app/what-is-webjs/page.ts @@ -161,7 +161,7 @@ const PROSE = 'text-fg-muted text-base leading-[1.7] m-0'; const CAPABILITIES = [ { title: 'AI-first, readable end to end', - body: 'Predictable file conventions and an explicit .server.ts boundary decide the shape of an app, so an agent edits one route without reading the whole app, and ordinary words are enough to ask for a new one. Every app ships an AGENTS.md contract that Claude Code, Cursor, Copilot, Gemini, and opencode read from one source, and the framework itself is plain JavaScript with JSDoc in node_modules rather than a compiled bundle.', + body: 'Predictable file conventions and an explicit .server.ts boundary decide the shape of an app, so an agent edits one route without reading the whole app, and ordinary words are enough to ask for a new one. Every app ships an AGENTS.md contract that Claude Code, Cursor, Copilot, Gemini, and opencode read from one source, and the framework itself sits in node_modules as plain JavaScript with JSDoc you can open and read.', }, { title: 'Web components, server-rendered', diff --git a/website/test/ssr/why-webjs-ssr.test.ts b/website/test/ssr/why-webjs-ssr.test.ts index 531a7ed23..890301723 100644 --- a/website/test/ssr/why-webjs-ssr.test.ts +++ b/website/test/ssr/why-webjs-ssr.test.ts @@ -42,7 +42,10 @@ test('the plain-language section pairs a prompt against the files the convention // their correspondence, and the accessible name each scrollable block needs. const out = await renderToString(Why()); assert.ok(out.includes('Let customers book a table'), 'the prompt panel carries a request written in ordinary words'); - assert.ok(!/> Build a page/.test(out), 'the prompt does not name a page, which the file panel would then have to match one for one'); + // Plain substring, not a regex spanning the chevron: the prompt's > lives in + // its own span, so anything asserting the two are adjacent can never match + // and would sit here green while the regression it names is fully present. + assert.ok(!out.includes('Build a page'), 'the prompt does not name a page, which the file panel would then have to match one for one'); for (const file of ['app/book/page.ts', 'app/staff/bookings/page.ts', 'modules/bookings/actions/create.server.ts', 'db/schema.server.ts']) { assert.ok(out.includes(file), `the file panel answers the prompt with ${file}`); } From b7928acf811de38fc0e145f41fef8b44d83e6651 Mon Sep 17 00:00:00 2001 From: Vivek <vivek7405@gmail.com> Date: Fri, 31 Jul 2026 15:33:42 +0530 Subject: [PATCH 5/5] fix(website): back out two overreaching fixes and pin the a11y rule site-wide Two corrections from the last round were themselves the regression, and checking both premises confirmed it. The compiled-bundle clause is back to exactly its pre-change wording. It appears five times across the site and in root AGENTS.md, so editing one instance left the page disagreeing with its own FAQ, with the FAQPage schema that FAQ feeds, and with /why-webjs. Whether the claim holds everywhere is a real question and not this PR's; the PR now takes no position on it. The toggled usage block is back to carrying nothing. Its content is one 37-character line that never becomes a scroll container at any width a reader will use, so the tabindex added for it was a permanent tab stop on inert content rather than the rescue of an unreachable region. The negative prompt assertion was a tripwire for one literal under a message promising much more, so it now asserts the property the section actually claims: the prompt panel contains no framework vocabulary at all, which no rewording walks past. And the name rule is pinned across all three pages that render code blocks, in its own file, since the rule belongs to the site rather than to one page and covering one page is how the other two drifted. --- website/app/page.ts | 2 +- website/app/what-is-webjs/page.ts | 2 +- website/test/ssr/pre-block-a11y.test.ts | 71 +++++++++++++++++++++++++ website/test/ssr/why-webjs-ssr.test.ts | 19 ++++--- 4 files changed, 82 insertions(+), 12 deletions(-) create mode 100644 website/test/ssr/pre-block-a11y.test.ts diff --git a/website/app/page.ts b/website/app/page.ts index da727bc23..f940b6113 100644 --- a/website/app/page.ts +++ b/website/app/page.ts @@ -232,7 +232,7 @@ export default function LandingPage() { <div class="flex-1 grid place-items-center px-6 py-10 group-has-[:checked]/stage:hidden"> <like-button count="3"></like-button> </div> - <pre class="hidden group-has-[:checked]/stage:block flex-1 m-0 p-5 overflow-x-auto font-mono text-xs leading-[1.72] text-left" role="region" tabindex="0" aria-label="like-button usage"><code>${highlight(USAGE_SAMPLE)}</code></pre> + <pre class="hidden group-has-[:checked]/stage:block flex-1 m-0 p-5 overflow-x-auto font-mono text-xs leading-[1.72] text-left"><code>${highlight(USAGE_SAMPLE)}</code></pre> <div class="px-4 py-3 border-t border-border text-center font-mono text-xs leading-[1.5] text-fg-subtle"> Server-rendered first, then upgraded. Click it. </div> diff --git a/website/app/what-is-webjs/page.ts b/website/app/what-is-webjs/page.ts index 264bd8c1b..5022bb4f2 100644 --- a/website/app/what-is-webjs/page.ts +++ b/website/app/what-is-webjs/page.ts @@ -161,7 +161,7 @@ const PROSE = 'text-fg-muted text-base leading-[1.7] m-0'; const CAPABILITIES = [ { title: 'AI-first, readable end to end', - body: 'Predictable file conventions and an explicit .server.ts boundary decide the shape of an app, so an agent edits one route without reading the whole app, and ordinary words are enough to ask for a new one. Every app ships an AGENTS.md contract that Claude Code, Cursor, Copilot, Gemini, and opencode read from one source, and the framework itself sits in node_modules as plain JavaScript with JSDoc you can open and read.', + body: 'Predictable file conventions and an explicit .server.ts boundary decide the shape of an app, so an agent edits one route without reading the whole app, and ordinary words are enough to ask for a new one. Every app ships an AGENTS.md contract that Claude Code, Cursor, Copilot, Gemini, and opencode read from one source, and the framework itself is plain JavaScript with JSDoc in node_modules rather than a compiled bundle.', }, { title: 'Web components, server-rendered', diff --git a/website/test/ssr/pre-block-a11y.test.ts b/website/test/ssr/pre-block-a11y.test.ts new file mode 100644 index 000000000..4c891b083 --- /dev/null +++ b/website/test/ssr/pre-block-a11y.test.ts @@ -0,0 +1,71 @@ +/** + * The accessible name on every code block the marketing pages render. + * + * A <pre> maps to ARIA role `generic`, and ARIA prohibits an author-supplied + * name on `generic`, so a bare <pre aria-label="..."> gives a spec-following + * screen reader a name it will not announce. Every block that carries a name + * therefore carries an explicit `role="region"` to make that name one ARIA + * permits, and because a named region is a landmark, the names have to be + * unique per page or they collapse into an ambiguous pair in the landmark list. + * + * This lives in its own file rather than inside each page's test because the + * rule is a property of the SITE, not of one page. The pages here were fixed + * together, and pinning them together is what stops the next one from drifting + * back: the sweep that fixed them originally covered one page, and the other + * two went unobserved until a review caught it. + */ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { renderToString } from '@webjsdev/core/server'; +import Home from '#app/page.ts'; +import WhatIsWebJs from '#app/what-is-webjs/page.ts'; +import Why from '#app/why-webjs/page.ts'; + +/** Every `<pre …>` open tag in the rendered HTML, attribute order as authored. */ +function preTags(html: string) { + return html.match(/<pre\b[^>]*>/g) ?? []; +} + +function nameOf(tag: string) { + return tag.match(/aria-label="([^"]*)"/)?.[1]; +} + +const PAGES = [ + { name: '/', render: () => Home() }, + { name: '/what-is-webjs', render: () => WhatIsWebJs() }, + { name: '/why-webjs', render: () => Why() }, +]; + +for (const page of PAGES) { + test(`every named code block on ${page.name} carries a role that permits the name`, async () => { + const tags = preTags(await renderToString(page.render())); + assert.ok(tags.length > 0, 'the page renders at least one code block'); + const named = tags.filter(nameOf); + assert.ok(named.length > 0, 'the page renders at least one NAMED code block, so this test has something to check'); + for (const tag of named) { + // Read the role by attribute, not by position: an order-sensitive regex + // would red on correct markup that simply wrote the attributes the other + // way round. + assert.match(tag, /\brole="region"/, `a named pre is missing role=region, so its name is one ARIA prohibits: ${nameOf(tag)}`); + } + }); + + test(`no two code blocks on ${page.name} share a landmark name`, async () => { + const named = preTags(await renderToString(page.render())).map(nameOf).filter(Boolean); + assert.deepEqual([...new Set(named)], named, `duplicate landmark names on ${page.name}: ${named.join(', ')}`); + }); +} + +test('a code block with no name needs no role, so it adds no landmark', async () => { + // The home page's toggled usage block holds one short line that never becomes + // a scroll container at a real viewport width. It carries no name and no + // focus stop on purpose, and promoting it would add an empty-ish landmark and + // a permanent tab stop on content nothing can interact with. + const tags = preTags(await renderToString(Home())); + const unnamed = tags.filter((t) => !nameOf(t)); + assert.ok(unnamed.length > 0, 'the home page still renders an unnamed code block'); + for (const tag of unnamed) { + assert.doesNotMatch(tag, /\brole="region"/, 'an unnamed block claims a landmark role it has no name for'); + assert.doesNotMatch(tag, /\btabindex=/, 'an unnamed, non-scrolling block takes a tab stop for nothing'); + } +}); diff --git a/website/test/ssr/why-webjs-ssr.test.ts b/website/test/ssr/why-webjs-ssr.test.ts index 890301723..e66fe53b6 100644 --- a/website/test/ssr/why-webjs-ssr.test.ts +++ b/website/test/ssr/why-webjs-ssr.test.ts @@ -42,20 +42,19 @@ test('the plain-language section pairs a prompt against the files the convention // their correspondence, and the accessible name each scrollable block needs. const out = await renderToString(Why()); assert.ok(out.includes('Let customers book a table'), 'the prompt panel carries a request written in ordinary words'); - // Plain substring, not a regex spanning the chevron: the prompt's > lives in - // its own span, so anything asserting the two are adjacent can never match - // and would sit here green while the regression it names is fully present. - assert.ok(!out.includes('Build a page'), 'the prompt does not name a page, which the file panel would then have to match one for one'); for (const file of ['app/book/page.ts', 'app/staff/bookings/page.ts', 'modules/bookings/actions/create.server.ts', 'db/schema.server.ts']) { assert.ok(out.includes(file), `the file panel answers the prompt with ${file}`); } assert.ok(out.includes('<span class="ml-2 font-mono font-medium text-xs leading-none text-fg-subtle">files</span>'), 'the file listing is labelled files, not terminal'); - // <pre> maps to role generic, where ARIA prohibits an author-supplied name, - // so every named scrollable block on this page carries an explicit role. - const named = out.match(/<pre[^>]*aria-label=/g) ?? []; - const region = out.match(/<pre[^>]*role="region"[^>]*aria-label=/g) ?? []; - assert.equal(named.length, 4, 'the page renders its four named code blocks'); - assert.equal(region.length, named.length, 'every named pre carries role=region, so the name is one ARIA permits'); + + // The section's whole claim is that the prompt needs no framework vocabulary, + // so assert that property of the prompt panel itself rather than tripwiring + // one historical phrasing, which any reworded regression would walk past. + const prompt = out.slice(out.indexOf('aria-label="A plain-language prompt'), out.indexOf('Where the conventions put it')); + assert.ok(prompt.includes('Let customers'), 'the slice really is the prompt panel'); + for (const jargon of ['page.ts', '.server.ts', 'route', 'component', 'schema', 'action']) { + assert.ok(!prompt.includes(jargon), `the prompt says nothing about ${jargon}, so it reads as a request rather than a spec`); + } }); test('the /why-webjs description answers in the snippet window a SERP actually shows', async () => {