-
Notifications
You must be signed in to change notification settings - Fork 69
feat(website): sell plain-language prompts and cross-model quality #1202
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
e3317f3
b57b139
00487b5
3eedf19
b7928ac
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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.'; | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
|
|
@@ -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 }) { | ||
|
|
@@ -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">></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"> | ||
|
|
@@ -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, | ||
|
|
||
| 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. | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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'); | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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'); | ||
| } | ||
| }); | ||
There was a problem hiding this comment.
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.