Skip to content

Commit b7928ac

Browse files
committed
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.
1 parent 3eedf19 commit b7928ac

4 files changed

Lines changed: 82 additions & 12 deletions

File tree

website/app/page.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ export default function LandingPage() {
232232
<div class="flex-1 grid place-items-center px-6 py-10 group-has-[:checked]/stage:hidden">
233233
<like-button count="3"></like-button>
234234
</div>
235-
<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>
235+
<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>
236236
<div class="px-4 py-3 border-t border-border text-center font-mono text-xs leading-[1.5] text-fg-subtle">
237237
Server-rendered first, then upgraded. Click it.
238238
</div>

website/app/what-is-webjs/page.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ const PROSE = 'text-fg-muted text-base leading-[1.7] m-0';
161161
const CAPABILITIES = [
162162
{
163163
title: 'AI-first, readable end to end',
164-
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.',
164+
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.',
165165
},
166166
{
167167
title: 'Web components, server-rendered',
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/**
2+
* The accessible name on every code block the marketing pages render.
3+
*
4+
* A <pre> maps to ARIA role `generic`, and ARIA prohibits an author-supplied
5+
* name on `generic`, so a bare <pre aria-label="..."> gives a spec-following
6+
* screen reader a name it will not announce. Every block that carries a name
7+
* therefore carries an explicit `role="region"` to make that name one ARIA
8+
* permits, and because a named region is a landmark, the names have to be
9+
* unique per page or they collapse into an ambiguous pair in the landmark list.
10+
*
11+
* This lives in its own file rather than inside each page's test because the
12+
* rule is a property of the SITE, not of one page. The pages here were fixed
13+
* together, and pinning them together is what stops the next one from drifting
14+
* back: the sweep that fixed them originally covered one page, and the other
15+
* two went unobserved until a review caught it.
16+
*/
17+
import test from 'node:test';
18+
import assert from 'node:assert/strict';
19+
import { renderToString } from '@webjsdev/core/server';
20+
import Home from '#app/page.ts';
21+
import WhatIsWebJs from '#app/what-is-webjs/page.ts';
22+
import Why from '#app/why-webjs/page.ts';
23+
24+
/** Every `<pre …>` open tag in the rendered HTML, attribute order as authored. */
25+
function preTags(html: string) {
26+
return html.match(/<pre\b[^>]*>/g) ?? [];
27+
}
28+
29+
function nameOf(tag: string) {
30+
return tag.match(/aria-label="([^"]*)"/)?.[1];
31+
}
32+
33+
const PAGES = [
34+
{ name: '/', render: () => Home() },
35+
{ name: '/what-is-webjs', render: () => WhatIsWebJs() },
36+
{ name: '/why-webjs', render: () => Why() },
37+
];
38+
39+
for (const page of PAGES) {
40+
test(`every named code block on ${page.name} carries a role that permits the name`, async () => {
41+
const tags = preTags(await renderToString(page.render()));
42+
assert.ok(tags.length > 0, 'the page renders at least one code block');
43+
const named = tags.filter(nameOf);
44+
assert.ok(named.length > 0, 'the page renders at least one NAMED code block, so this test has something to check');
45+
for (const tag of named) {
46+
// Read the role by attribute, not by position: an order-sensitive regex
47+
// would red on correct markup that simply wrote the attributes the other
48+
// way round.
49+
assert.match(tag, /\brole="region"/, `a named pre is missing role=region, so its name is one ARIA prohibits: ${nameOf(tag)}`);
50+
}
51+
});
52+
53+
test(`no two code blocks on ${page.name} share a landmark name`, async () => {
54+
const named = preTags(await renderToString(page.render())).map(nameOf).filter(Boolean);
55+
assert.deepEqual([...new Set(named)], named, `duplicate landmark names on ${page.name}: ${named.join(', ')}`);
56+
});
57+
}
58+
59+
test('a code block with no name needs no role, so it adds no landmark', async () => {
60+
// The home page's toggled usage block holds one short line that never becomes
61+
// a scroll container at a real viewport width. It carries no name and no
62+
// focus stop on purpose, and promoting it would add an empty-ish landmark and
63+
// a permanent tab stop on content nothing can interact with.
64+
const tags = preTags(await renderToString(Home()));
65+
const unnamed = tags.filter((t) => !nameOf(t));
66+
assert.ok(unnamed.length > 0, 'the home page still renders an unnamed code block');
67+
for (const tag of unnamed) {
68+
assert.doesNotMatch(tag, /\brole="region"/, 'an unnamed block claims a landmark role it has no name for');
69+
assert.doesNotMatch(tag, /\btabindex=/, 'an unnamed, non-scrolling block takes a tab stop for nothing');
70+
}
71+
});

website/test/ssr/why-webjs-ssr.test.ts

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -42,20 +42,19 @@ test('the plain-language section pairs a prompt against the files the convention
4242
// their correspondence, and the accessible name each scrollable block needs.
4343
const out = await renderToString(Why());
4444
assert.ok(out.includes('Let customers book a table'), 'the prompt panel carries a request written in ordinary words');
45-
// Plain substring, not a regex spanning the chevron: the prompt's > lives in
46-
// its own span, so anything asserting the two are adjacent can never match
47-
// and would sit here green while the regression it names is fully present.
48-
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');
4945
for (const file of ['app/book/page.ts', 'app/staff/bookings/page.ts', 'modules/bookings/actions/create.server.ts', 'db/schema.server.ts']) {
5046
assert.ok(out.includes(file), `the file panel answers the prompt with ${file}`);
5147
}
5248
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');
53-
// <pre> maps to role generic, where ARIA prohibits an author-supplied name,
54-
// so every named scrollable block on this page carries an explicit role.
55-
const named = out.match(/<pre[^>]*aria-label=/g) ?? [];
56-
const region = out.match(/<pre[^>]*role="region"[^>]*aria-label=/g) ?? [];
57-
assert.equal(named.length, 4, 'the page renders its four named code blocks');
58-
assert.equal(region.length, named.length, 'every named pre carries role=region, so the name is one ARIA permits');
49+
50+
// The section's whole claim is that the prompt needs no framework vocabulary,
51+
// so assert that property of the prompt panel itself rather than tripwiring
52+
// one historical phrasing, which any reworded regression would walk past.
53+
const prompt = out.slice(out.indexOf('aria-label="A plain-language prompt'), out.indexOf('Where the conventions put it'));
54+
assert.ok(prompt.includes('Let customers'), 'the slice really is the prompt panel');
55+
for (const jargon of ['page.ts', '.server.ts', 'route', 'component', 'schema', 'action']) {
56+
assert.ok(!prompt.includes(jargon), `the prompt says nothing about ${jargon}, so it reads as a request rather than a spec`);
57+
}
5958
});
6059

6160
test('the /why-webjs description answers in the snippet window a SERP actually shows', async () => {

0 commit comments

Comments
 (0)