diff --git a/website/app/page.ts b/website/app/page.ts index 4e79712f1..f940b6113 100644 --- a/website/app/page.ts +++ b/website/app/page.ts @@ -103,7 +103,7 @@ function codeWindow(title: string, sample: string) { return html`
${DOTS}${title}
-
${highlight(sample)}
+
${highlight(sample)}
`; } @@ -216,7 +216,7 @@ export default function LandingPage() {
${DOTS}components/like-button.ts
-
${highlight(HERO_SAMPLE)}
+
${highlight(HERO_SAMPLE)}
@@ -232,7 +232,7 @@ export default function LandingPage() {
- +
Server-rendered first, then upgraded. Click it.
@@ -384,7 +384,7 @@ export default function LandingPage() { Full-stack

Pages + API + components

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.

-
app/page.ts
+            
app/page.ts
 components/counter.ts
 actions/posts.server.ts
npm create webjs@latest my-app
@@ -393,7 +393,7 @@ actions/posts.server.ts
Backend (API)

Route handlers + Database

A backend-only app, no UI or SSR. File-based route handlers, modules, middleware, rate limiting, WebSockets, a database, and a backend-features gallery.

-
app/api/users/route.ts
+            
app/api/users/route.ts
 app/api/chat/route.ts
 middleware.ts
npm create webjs@latest my-api -- --template api
diff --git a/website/app/what-is-webjs/page.ts b/website/app/what-is-webjs/page.ts index c784d9164..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 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', @@ -207,7 +207,7 @@ function codeWindow(title: string, sample: string, label: string) { return html`
${DOTS}${title}
-
${highlight(sample)}
+
${highlight(sample)}
`; } diff --git a/website/app/why-webjs/page.ts b/website/app/why-webjs/page.ts index 2b4f4e8c6..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 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.'; return { title, description, @@ -118,7 +118,7 @@ export default function Why() {

The framework, readable in node_modules

${DOTS}terminal
-
$ ls node_modules/@webjsdev/core/src
+              
$ 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)
             

Your app code, served to the browser as written

${DOTS}terminal
-
$ curl localhost:5001/components/counter.ts
+              
$ curl localhost:5001/components/counter.ts
 import { WebComponent } from '@webjsdev/core';
 
 class Counter extends WebComponent({ count: Number }) {
@@ -166,6 +166,51 @@ 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
+
> Let customers book a table, save the
+  bookings, and give staff today's list.
+
+# no framework terms, no file names, no
+# architecture. just the thing you want.
+
+
+
+

Where the conventions put it

+
+
${DOTS}files
+
app/book/page.ts
+  the page, server-rendered to real HTML
+app/staff/bookings/page.ts
+  the staff view, its own route
+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 +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.

+

+ 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. +

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

 maps to ARIA role `generic`, and ARIA prohibits an author-supplied
+ * name on `generic`, so a bare 
 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 `
` open tag in the rendered HTML, attribute order as authored. */
+function preTags(html: string) {
+  return html.match(/]*>/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 e6a9e886e..e66fe53b6 100644
--- a/website/test/ssr/why-webjs-ssr.test.ts
+++ b/website/test/ssr/why-webjs-ssr.test.ts
@@ -24,6 +24,48 @@ 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('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'); + 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('files'), 'the file listing is labelled files, not terminal'); + + // 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 () => { + // 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 ');