Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions website/app/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>
`;
}
Expand Down Expand Up @@ -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" />
Expand All @@ -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>

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This is not the revert I called it. main carries aria-label="like-button usage" on this block, so the PR's net effect here is deleting an accessible name the block already had, while the same PR resolves the identical construct on four other blocks by adding a role instead. Verified against main.

<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>
Expand Down Expand Up @@ -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 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>
Expand All @@ -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 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>
Expand Down
4 changes: 2 additions & 2 deletions website/app/what-is-webjs/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 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',
Expand Down Expand Up @@ -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>
`;
}
Expand Down
60 changes: 57 additions & 3 deletions website/app/why-webjs/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 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.';

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Attaches the claim to the wrong noun. What you describe in plain language is the app, not the framework. This is the exact sentence the snippet-window test pins.

return {
title,
description,
Expand Down Expand Up @@ -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
Expand All @@ -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 }) {
Expand Down Expand Up @@ -166,6 +166,51 @@ Counter.register('counter');
</div>
</section>

<section class="py-16">
<div class="max-w-6xl mx-auto px-6">
<div class="max-w-3xl mx-auto mb-12 text-center">
<h2 class="font-display font-bold text-h2 leading-[1.12] tracking-[-0.03em] my-3 text-balance">Describe what you want in plain language</h2>
<p class="text-fg-muted text-base leading-[1.6] m-0">
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.
</p>
</div>
<div class="grid grid-cols-1 wide:grid-cols-2 gap-4 items-stretch max-w-3xl mx-auto">
<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">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" role="region" tabindex="0" aria-label="A plain-language prompt with no technical terms in it"><code><span class="text-accent">&gt;</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>
</figure>
</div>
<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}>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 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
<span class="text-fg-subtle"> the table it all reads and writes</span></code></pre>
</figure>
</div>
</div>
</div>
</section>

<section class="py-16">
<div class="max-w-6xl mx-auto px-6">
<div class="max-w-3xl mx-auto text-center">
Expand All @@ -177,6 +222,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.
</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 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
output to reverse engineer when something breaks. You open the file,
Expand Down
71 changes: 71 additions & 0 deletions website/test/ssr/pre-block-a11y.test.ts
Original file line number Diff line number Diff line change
@@ -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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Narrates this PR's review history in permanent source. The rule is worth stating; how it was arrived at is not.

*/
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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

The premise is wrong at the width WCAG reflow names. The sample is about 266px of text and the content box at a 320px viewport is about 230px, so it does overflow below roughly 356px. The test then pins that wrong premise as an invariant, so the block cannot be given a name or a focus stop without editing this test.

// 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');

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This documents one block and observes a set. It passes if the usage block gains a name while any other unnamed block exists, which is exactly when the property it describes stops being observed, and it reds with this message if that block is legitimately named.

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');
}
});
Loading
Loading