Make any site readable by AI agents
Agents read a page through its accessibility tree and through screenshots.
Legacy pages fail the first: a <div onclick> is not a button, an icon link
has no name, and a field's label is the table cell beside it. To an agent,
those controls are missing or unnamed.
@stunt-double/wao (Web Agent Optimiser) repairs them in place. Before, in
Chromium's aria snapshot of a legacy page:
- link:
- /url: /cart
- cell:
- textbox
- text: Buy nowAfter WAO:
- link "Shopping cart":
- /url: /cart
- cell:
- textbox "Email"
- button "Buy now"How it works
- Attributes only. WAO never replaces, moves or restyles a node. Event handlers, framework state and what a screenshot shows are untouched.
- Never overwrites the page. Automatic repairs only fill in attributes the page has not set. Only rules you write can override.
- Reversible.
restore()puts every attribute back exactly as it was. - Keeps up. A
MutationObserverrepairs content that single-page apps add later, including inside open shadow roots. - Small. About 4 KB gzipped, with no dependencies, and no network requests.
Install
As a script tag on any site, serving dist/wao.global.js from the package
(also exported as @stunt-double/wao/script):
<script src="/wao.global.js" defer></script>Or as a package in a bundled app. Until the @stunt-double scope is available
on npm, add @stunt-double:registry=https://npm.pkg.github.com and a GitHub
token with read:packages to your .npmrc.
npm install @stunt-double/waoimport { optimise } from "@stunt-double/wao";
const wao = optimise();The npm entry has no side effects: nothing runs until you call optimise. The
script build runs it once the DOM is ready and exposes the handle as
window.wao.
In an agent's browser
You don't need to own a site to use WAO on it. An agent harness can inject the script build into every page it opens, so the agent reads the repaired tree. With Playwright:
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
const script = readFileSync(
fileURLToPath(import.meta.resolve("@stunt-double/wao/script")),
"utf8",
);
await context.addInitScript({ content: script });With Puppeteer, pass the same content to page.evaluateOnNewDocument. Pair it
with the browser toolset, whose read_page reads
the same tree.
What it repairs
| Repair | What it does |
|---|---|
clickables | Gives a non-semantic element with a click handler role="button" and tabindex="0", so Enter and Space click it. |
names | Names an unnamed control from a tooltip, an icon font class, the text beside a field, a field's name or a link's URL. |
landmarks | Gives a page with no <main> role="main" on its one obvious candidate. |
Each repaired element is marked with data-wao, so an agent can tell inferred
names from ones the page wrote. Switch a repair off with
optimise({ repairs: { landmarks: false } }).
Rules
Heuristics can't know what a control means. Rules can, and they override the page:
optimise({
rules: [
{ selector: "#hdr .links", role: "navigation", label: "Primary" },
{
selector: ".btn-go",
label: "Check out",
description: "Pays for everything in the basket",
},
],
});With the script build, set window.waoOptions before the script loads.
Reports
report() lists every change in effect and what WAO found but could not fix
(unnamed-control, image-without-alt, no-main). Each issue is a good
candidate for a rule, or for a fix in the site itself.