Built with
Published by Zaid Al-Dabbagh on LinkedIn (opens in a new tab),
Scanners give you rule IDs. They don't tell you who's affected. The A11Y Cheat Sheet translates all 104 automated scanner rules into plain language about the people a failure affects. The content runs on Sanity. This is how we use agents to draft those cards, how a person reviews every one before it's published, and how WCAG Watch keeps an eye on the standards every morning.
Turning 104 accessibility rules into structured data
Every format reads the same fields, web cards, Read aloud and the A4 print, so the fields have to be predictable.
Sanity features
- Structured content & custom schema
- Validation
- Portable Text
Every rule is a custom wcagRuleCard document. Instead of one big rich-text blob, each rule has distinct fields: who it affects, Do and Don't code snippets, a 30-second manual test, and what automated scanners miss.
Because the shape never changes, the front end never has to guess what's coming. If someone pastes a broken video URL or forgets alt text on an illustration, Studio validation catches it before it's published.
In Sanity's docs
Readers only see what a person published
The site reads published cards only, so nothing an agent drafts reaches a reader until a person publishes it.
Sanity features
- GROQ
- Studio structure
Agents draft, people publish. The site reads published documents only, so an unreviewed AI draft can't reach a reader.
The cheat sheet lives in a public dataset, so its cards show on a11yuniversity.org/a11y-cheat-sheet without a sign-in.
The site fetches published documents only. That comes from the client's published perspective, not the query, so it's set once for every read. Drafts never show up, including AI drafts nobody has reviewed yet.
*[_type == "wcagRuleCard" && defined(ruleId)]{
ruleId, headline, oneLiner, severity,
"related": related[defined(@->ruleId)]->{ruleId, headline}
}The system at a glance
Sanity holds the Studio and a public dataset, and the Studio writes to it. A Next.js app on Vercel reads that dataset for a11yuniversity.org. The Studio also calls a Draft API in the same Next.js app, which asks the LLM service to draft a card, and it calls Sanity's Agent Actions directly to draft WCAG 3.0 summaries. Each morning a scheduled Sanity Function calls the same app, which checks axe-core and W3C for anything new.
Transcript
The system at a glance
The whole system on one page, left to right: Sanity, then Vercel, then the site, the LLM service, and axe-core and W3C. Each step's number is on its boxes.
- The Studio. Inside Sanity, the Studio is where we write and edit content.
- The public dataset. The Studio writes to it. It holds the cheat sheet's cards and this page, and anyone can read it.
- One Next.js app. In the middle, Vercel runs one Next.js app. It reads the dataset with GROQ, Sanity's query language, and asks only for published documents, so a draft never reaches a reader.
- The site. On the right, that app serves a11yuniversity.org, with no sign-in.
- The Draft API and the LLM service. The last line runs from the Studio to the Draft API. When an editor presses Draft with AI, the Studio calls this API inside the same app, and it asks the LLM service to write the card. The chapter on AI drafts walks through it.
- Agent Actions. Draft summary with AI, the newer agent, calls Sanity's Agent Actions straight from the Studio, with no API of ours in between. The chapter Agents you can see covers it.
- WCAG Watch. Each morning a scheduled Sanity Function calls the app, which checks axe-core and W3C for anything new. The chapter WCAG Watch covers it.
How a card reaches readers
The Studio edits the public dataset, which holds the rule cards and this page. Next.js runs one GROQ query against it, published documents only, and caches the result for two minutes under the wcagRuleCard tag. That cache feeds a11yuniversity.org.
Transcript
How a card reaches readers
One card, from the Studio to readers, left to right.
- One public dataset. In Sanity Studio, an editor works on the public dataset, which holds the rule cards and this page.
- One query, published only. Next.js on Vercel runs one GROQ query against the public dataset, using the published perspective. A draft is never included: only what someone has deliberately published.
- A two-minute cache. The result is kept for two minutes under a tag called wcagRuleCard, so the site doesn't ask Sanity for every page view.
- The site. That cache feeds a11yuniversity.org.
Drafts never leave the Studio, because the site reads the published perspective. And the two minutes rarely matter now: when a card is published, a Sanity Function tells the site straight away, and the site drops that tag from its cache. The chapter Live within seconds of publishing explains it.
How a card reaches readers
How a card reaches readers
Transcript
How a card reaches readers
One card, from the Studio to readers, left to right.
- One public dataset. In Sanity Studio, an editor works on the public dataset, which holds the rule cards and this page.
- One query, published only. Next.js on Vercel runs one GROQ query against the public dataset, using the published perspective. A draft is never included: only what someone has deliberately published.
- A two-minute cache. The result is kept for two minutes under a tag called wcagRuleCard, so the site doesn't ask Sanity for every page view.
- The site. That cache feeds a11yuniversity.org.
Drafts never leave the Studio, because the site reads the published perspective. And the two minutes rarely matter now: when a card is published, a Sanity Function tells the site straight away, and the site drops that tag from its cache. The chapter Live within seconds of publishing explains it.
In Sanity's docs
Live within seconds of publishing
A Sanity Function tells the cheat sheet the moment a card is published, so readers see it within seconds, not minutes.
Sanity features
- Functions
Every card in the A11Y Cheat Sheet — the rule cards and the WCAG 3.0 draft cards — is a Sanity document, and the site keeps what it reads for two minutes. Publish a card, and a Sanity Function running on Sanity’s own infrastructure tells the site straight away; the site drops that kind of card from its cache, so the next reader gets the new version within seconds.
The Function holds no content and writes nothing. Document Functions skip drafts by default, so only a published card wakes it. It passes on one thing, the kind of card that changed, with a secret only it and the site share. Deleting a card takes it off the cheat sheet just as quickly.
defineDocumentFunction({
name: 'refresh-cheat-sheet',
event: {
on: ['create', 'update', 'delete'],
filter: '_type in ["wcagRuleCard", "wcag3DraftCard"]',
projection: '{_id, _type}',
resource: {type: 'dataset', id: 'qsyof5o0.production'},
},
})On the site, a route checks the secret, maps the card's type to its cache tag and calls revalidateTag. The next read fetches fresh from Sanity.
AI drafts, reviewed by a person
AI can draft a new card straight into the Studio. Nobody can publish it until someone has read and corrected it.
Sanity features
- AI drafts with human review
Draft with AI is a document action in the Studio. It asks a model to write a card from the scanner’s own description of the rule and the rule’s WCAG tags. The result is saved as an unpublished draft marked “AI draft — not yet reviewed”, and publishing stays blocked until a person ticks it as reviewed.
The model doesn’t pick the success criteria. Those come straight from the rule’s tags. The site reads the published perspective, so learners never see anything unreviewed.
The Studio holds no secret, because anyone could read one in a browser. It proves who's asking by writing a one-time nonce on the card's draft. The Draft API reads that back from the Content Lake with its own token, so only someone who can edit the card can ask for a draft.
Treat model output as untrusted input. It's validated first, saved as a draft and blocked from publishing until a person ticks Reviewed. The agent can draft, but it can't publish.
Draft with AI, step by step
An editor presses Draft with AI. The Studio writes a one-time nonce on the card's draft, then asks the Draft API for a draft. The Draft API reads the nonce back from the Content Lake to confirm the request came from someone who can edit, then sends axe-core's description of the rule to the LLM service. The service returns the headline, why it matters, the fixes and the test. The success criteria come from the rule's WCAG tags, not the model. The Studio saves the fields to the draft, marked as an AI draft that hasn't been reviewed, and publishing is blocked until a person reads it, corrects it and ticks Reviewed.
Transcript
Draft with AI, step by step
Draft with AI on a scanner-rule card, top to bottom, across the Editor, Sanity Studio, the Content Lake where Sanity stores everything, our Draft API in Next.js, and the LLM service.
- The Editor presses Draft with AI. On a card that already has its rule ID and the scanner's own description of the rule.
- The Studio writes a one-time nonce. Before asking for anything, it writes a random code onto the card's draft in the Content Lake.
- The Studio asks for a draft. It sends the Draft API the rule ID and that nonce.
- The Draft API reads the nonce back. From the Content Lake, with its own server token.
- The nonce matches. Only someone who can edit the card could have written it, so the request is genuine. That's how the Studio proves who it is without a secret in its code, which anyone could read in a browser. The nonce is good for two minutes, and the API limits how often it can be asked.
- The Draft API asks the LLM service. It sends axe-core's description of the rule to the LLM service.
- The LLM service writes the card. It sends back the headline, why it matters, the fixes and the test, in the house voice. The note under it is the rule that matters: the success criteria come from the rule's WCAG tags, not the model, so the model can't invent which part of WCAG a rule belongs to.
- The Draft API hands the fields back. It doesn't save anything itself.
- The Studio saves the draft. As the editor, so the card's history shows who did it, marked as an AI draft that hasn't been reviewed. From here, publishing is blocked until a person ticks Reviewed.
- The Editor reviews it. They read the draft, correct what needs correcting, and tick Reviewed.
- The Editor publishes. Now that a person has reviewed it, Publish is allowed.
- The card is published. It reaches the Content Lake as published, and from there the site.
Draft with AI, step by step
Draft with AI, step by step
Transcript
Draft with AI, step by step
Draft with AI on a scanner-rule card, top to bottom, across the Editor, Sanity Studio, the Content Lake where Sanity stores everything, our Draft API in Next.js, and the LLM service.
- The Editor presses Draft with AI. On a card that already has its rule ID and the scanner's own description of the rule.
- The Studio writes a one-time nonce. Before asking for anything, it writes a random code onto the card's draft in the Content Lake.
- The Studio asks for a draft. It sends the Draft API the rule ID and that nonce.
- The Draft API reads the nonce back. From the Content Lake, with its own server token.
- The nonce matches. Only someone who can edit the card could have written it, so the request is genuine. That's how the Studio proves who it is without a secret in its code, which anyone could read in a browser. The nonce is good for two minutes, and the API limits how often it can be asked.
- The Draft API asks the LLM service. It sends axe-core's description of the rule to the LLM service.
- The LLM service writes the card. It sends back the headline, why it matters, the fixes and the test, in the house voice. The note under it is the rule that matters: the success criteria come from the rule's WCAG tags, not the model, so the model can't invent which part of WCAG a rule belongs to.
- The Draft API hands the fields back. It doesn't save anything itself.
- The Studio saves the draft. As the editor, so the card's history shows who did it, marked as an AI draft that hasn't been reviewed. From here, publishing is blocked until a person ticks Reviewed.
- The Editor reviews it. They read the draft, correct what needs correcting, and tick Reviewed.
- The Editor publishes. Now that a person has reviewed it, Publish is allowed.
- The card is published. It reaches the Content Lake as published, and from there the site.
In Sanity's docs
Agents you can see
Sanity’s Agent Actions draft a WCAG 3.0 card at a press. Every run is recorded, and the studio manager gets an email saying what it wrote and what’s waiting for review.
Sanity features
- Agent Actions
- AI drafts with human review
- Studio structure
Each WCAG 3.0 draft card in the Studio has a Draft summary with AI button. It sends Sanity’s Agent Actions W3C’s own words for that guideline — its statement, and every requirement with when it applies and its exceptions — and asks for the card’s Core purpose and two to four Key takeaways.
This is a different route from Draft with AI. That one calls an LLM service through our own Draft API, so it needs a server, a key and a nonce to prove who's asking. Agent Actions run on the editor's own Studio session, with no key in the browser and no API of ours in between.
The model's output is untrusted input, so the request uses noWrite: nothing lands on the card straight away. The Studio checks what comes back first: a purpose under 600 characters, two to four takeaways, nothing copied from W3C word for word. Only then does it write a draft, as the person who pressed the button, marked as an AI draft. Publishing stays blocked until someone ticks Reviewed.
const draft = await client.withConfig({apiVersion: 'vX'}).agent.action.generate({
schemaId: '_.schemas.default',
// A blank card, not this one
targetDocument: {operation: 'create', _type: 'wcag3DraftCard'},
noWrite: true, // ask for the words; write nothing yet
instruction,
instructionParams,
target: {include: ['purpose', 'takeaways']},
})vX is Sanity's experimental API version, not a placeholder. The call targets a blank card on purpose: pointed at the existing card, the agent adds to its takeaways instead of writing them afresh.
Every run is recorded: who started it, on which card, how long it took and what it wrote. The Studio’s Agents section lists each agent — what it reads, what it writes, whether a person checks its work — with its runs. Whether a draft was accepted as written, edited, discarded or is still waiting is worked out from the card itself, so nobody has to log it.
When a run finishes, the site emails the studio manager: the card, what the agent wrote, a link to review it, and what else is still waiting or has been published this week. The Studio sends only the run’s id; the site reads the run back and sends one email per run.
Draft summary with AI, step by step
An editor presses Draft summary with AI on a WCAG 3.0 card. The Studio records a run, then sends Sanity's Agent Actions W3C's words for the guideline and asks for the Core purpose and takeaways without saving anything. Agent Actions return the words. The Studio checks them: the purpose fits, there are two to four takeaways, nothing is copied from W3C. If they fit, the Studio saves them to the card's draft as the editor, marked as an AI draft that hasn't been reviewed; if not, the card is left as it was. Either way the run is finished, and the website emails the studio manager what happened. Publishing stays blocked until a person reads the draft and ticks Reviewed. Once it's published, the run settles as Accepted if the agent's words went out as written, or Edited if a person changed them.
Transcript
Draft summary with AI, step by step
Draft summary with AI, the agent that drafts a WCAG 3.0 card, top to bottom, across the Editor, Sanity Studio, Sanity's Agent Actions, the Content Lake and our Website.
- The Editor presses Draft summary with AI. On a WCAG 3.0 draft card in Sanity Studio. If the card already has words, the Studio asks first whether to replace them.
- The Studio starts a run. It writes a small record: which agent, which card, who pressed it and when. The Agents section and the run emails are built from these records.
- The Studio calls Agent Actions. It sends W3C's own words for the guideline, its statement and each requirement with when it applies and its exceptions, plus another finished card as the voice to match. It asks for the Core purpose and Key takeaways with noWrite, so nothing is saved yet. The call runs on the editor's own Studio session: there's no key in the browser.
- Agent Actions return the words, and the Studio checks them. A Core purpose and two to four takeaways, written as if into a blank card, so the agent never just adds to what's there. Then the note: the purpose must fit, at 600 characters or fewer; there must be two to four takeaways, each short enough; and it mustn't copy a run of W3C's words or repeat the card's current takeaways. If anything fails, the card is left exactly as it was and the run is marked failed, with the reason.
- The Studio saves the draft. If it passes, the words go into the card's draft, as the editor, marked as an AI draft that hasn't been reviewed. The published card isn't touched.
- The Studio finishes the run. It records how long it took and what it wrote.
- The Studio tells the Website. It sends just the run's id: nothing secret, and nothing the Website couldn't check for itself.
- The Website reads the run back. It only goes on for a run that's just finished and hasn't been reported.
- The Website emails the studio manager. One email: what the agent wrote, a link to review it, what else is waiting and what's been published this week. Then the note: publishing is blocked until a person ticks Reviewed. The agent can draft, but it can't publish.
- A person decides. The Editor reads the draft, ticks Reviewed and publishes.
- The card is published. It goes to the Content Lake as published, and on to the cheat sheet. Then the last note: the run settles itself. When it's next looked at, it's Accepted if the agent's words went out as written, Edited if a person changed them first, or Discarded if the draft was thrown away.
Draft summary with AI, step by step
Draft summary with AI, step by step
Transcript
Draft summary with AI, step by step
Draft summary with AI, the agent that drafts a WCAG 3.0 card, top to bottom, across the Editor, Sanity Studio, Sanity's Agent Actions, the Content Lake and our Website.
- The Editor presses Draft summary with AI. On a WCAG 3.0 draft card in Sanity Studio. If the card already has words, the Studio asks first whether to replace them.
- The Studio starts a run. It writes a small record: which agent, which card, who pressed it and when. The Agents section and the run emails are built from these records.
- The Studio calls Agent Actions. It sends W3C's own words for the guideline, its statement and each requirement with when it applies and its exceptions, plus another finished card as the voice to match. It asks for the Core purpose and Key takeaways with noWrite, so nothing is saved yet. The call runs on the editor's own Studio session: there's no key in the browser.
- Agent Actions return the words, and the Studio checks them. A Core purpose and two to four takeaways, written as if into a blank card, so the agent never just adds to what's there. Then the note: the purpose must fit, at 600 characters or fewer; there must be two to four takeaways, each short enough; and it mustn't copy a run of W3C's words or repeat the card's current takeaways. If anything fails, the card is left exactly as it was and the run is marked failed, with the reason.
- The Studio saves the draft. If it passes, the words go into the card's draft, as the editor, marked as an AI draft that hasn't been reviewed. The published card isn't touched.
- The Studio finishes the run. It records how long it took and what it wrote.
- The Studio tells the Website. It sends just the run's id: nothing secret, and nothing the Website couldn't check for itself.
- The Website reads the run back. It only goes on for a run that's just finished and hasn't been reported.
- The Website emails the studio manager. One email: what the agent wrote, a link to review it, what else is waiting and what's been published this week. Then the note: publishing is blocked until a person ticks Reviewed. The agent can draft, but it can't publish.
- A person decides. The Editor reads the draft, ticks Reviewed and publishes.
- The card is published. It goes to the Content Lake as published, and on to the cheat sheet. Then the last note: the run settles itself. When it's next looked at, it's Accepted if the agent's words went out as written, Edited if a person changed them first, or Discarded if the draft was thrown away.
WCAG Watch
A scheduled Function checks the standards every morning, and a public changelog shows exactly what changed.
Sanity features
- Functions
The cards explain rules that keep moving. axe-core releases new versions, and W3C corrects WCAG 2.2 and publishes WCAG 3 drafts. So every morning a scheduled Sanity Function starts WCAG Watch. The Function holds no content: it calls a route on the site with a secret the two share, and the site does the reading.
defineScheduledFunction({
name: 'wcag-watch',
timeout: 60,
env: {WCAG_WATCH_URL: 'https://www.pandaux.com.au/api/wcag-watch/run'},
event: {expression: '0 21 * * *'},
})Each source becomes a list of lines, compared with what the watch saw last. When something new is published, the difference is saved as a document and shown on a public changelog (opens in a new tab), like a code review: removed lines in red, added lines in green, and the words that differ in bold.
Each publication is reported once. The studio manager gets an email with the link, and a newsletter is drafted for subscribers, ready to review and send. A switch in the Studio pauses it, and the Studio lists each run. It doesn’t touch the cards yet.
WCAG Watch, step by step
Each morning a scheduled Sanity Function calls a route on the site with a shared secret. The route checks whether the watch is paused, reads axe-core and W3C, and compares each with the last version it saw in Sanity. If something new has been published, it saves the change as a public changelog entry in the Content Lake, then emails the studio manager a link and drafts a newsletter for subscribers. A person reviews and sends the newsletter.
Transcript
WCAG Watch, step by step
One morning's watch, top to bottom, across the scheduled Function, the site's route, the Content Lake, axe-core and W3C, and the studio manager.
- The Function calls the site. With a secret only the two share.
- The route reads its state. If the watch is paused, it stops here. Otherwise it reads what it saw last.
- It asks the sources. axe-core's latest release, WCAG 2.2 and the WCAG 3 Working Draft.
- The sources answer. A source that can't be read is skipped for the day. Each one that answers is compared line by line.
- The change is saved. As a changelog entry, with the run, before anyone's told, so an email that fails never reports it twice.
- The newsletter is drafted. With the changelog link. It waits for a person to read and send it.
- The studio manager gets an email. What changed, a link to the changelog, and that a newsletter is waiting.
WCAG Watch, step by step
WCAG Watch, step by step
Transcript
WCAG Watch, step by step
One morning's watch, top to bottom, across the scheduled Function, the site's route, the Content Lake, axe-core and W3C, and the studio manager.
- The Function calls the site. With a secret only the two share.
- The route reads its state. If the watch is paused, it stops here. Otherwise it reads what it saw last.
- It asks the sources. axe-core's latest release, WCAG 2.2 and the WCAG 3 Working Draft.
- The sources answer. A source that can't be read is skipped for the day. Each one that answers is compared line by line.
- The change is saved. As a changelog entry, with the run, before anyone's told, so an email that fails never reports it twice.
- The newsletter is drafted. With the changelog link. It waits for a person to read and send it.
- The studio manager gets an email. What changed, a link to the changelog, and that a newsletter is waiting.
In Sanity's docs
Querying and auditing content via Sanity’s MCP server
Our developer tooling queries the dataset through Sanity’s MCP server to check what’s actually stored.
Sanity features
- MCP server
- GROQ
When a card’s video didn’t show up, the first check was whether it had been saved and published. One query through the MCP server answered that before anyone touched the code. Renaming “Card Deck” to “Cheat Sheet” started the same way, with a query for every document still using the old name.
Our developer tooling runs reads whenever it needs to, and only writes to a shared dataset when asked. Moving this page into the public dataset worked that way: one query to check it wasn’t there yet, then one write.
Checking the data before the code
A developer reports that a card's video isn't showing. The developer tooling sends a GROQ query through Sanity's MCP server for the card's draft and published versions. The Content Lake returns the card as it's stored, and the tooling reports whether it's saved and published before any code is touched. Reads run whenever they're needed; writes to a shared dataset happen only when asked.
Transcript
Checking the data before the code
Checking the data before touching the code, across the Developer, the developer tooling, Sanity's MCP server and the Content Lake.
- The Developer reports a problem. They report that a card's video isn't showing. The tempting thing is to go straight into the code and hunt for a bug.
- The tooling asks the data first. It sends a GROQ query through Sanity's MCP server, asking for the card's draft and its published version. MCP, the Model Context Protocol, is how a developer tool is given safe, direct access to a service like Sanity.
- The MCP server runs the query. Against the Content Lake.
- The Content Lake answers. It returns the card exactly as it's stored.
- The result comes back. The MCP server passes it to the tooling.
- The tooling answers before touching code. It tells the Developer whether the video is saved and published. Often that's the whole answer: the video was added to the draft but never published, so the site was right not to show it.
The note at the bottom is the house rule. Reads run whenever they're needed, because looking can't break anything. Writes to a shared dataset happen only when someone asks, because changing what readers and clients see is a person's call. Renaming the Card Deck to the Cheat Sheet worked the same way: first a query for every document still using the old name, then the changes, once asked.
Checking the data before the code
Checking the data before the code
Transcript
Checking the data before the code
Checking the data before touching the code, across the Developer, the developer tooling, Sanity's MCP server and the Content Lake.
- The Developer reports a problem. They report that a card's video isn't showing. The tempting thing is to go straight into the code and hunt for a bug.
- The tooling asks the data first. It sends a GROQ query through Sanity's MCP server, asking for the card's draft and its published version. MCP, the Model Context Protocol, is how a developer tool is given safe, direct access to a service like Sanity.
- The MCP server runs the query. Against the Content Lake.
- The Content Lake answers. It returns the card exactly as it's stored.
- The result comes back. The MCP server passes it to the tooling.
- The tooling answers before touching code. It tells the Developer whether the video is saved and published. Often that's the whole answer: the video was added to the draft but never published, so the site was right not to show it.
The note at the bottom is the house rule. Reads run whenever they're needed, because looking can't break anything. Writes to a shared dataset happen only when someone asks, because changing what readers and clients see is a person's call. Renaming the Card Deck to the Cheat Sheet worked the same way: first a query for every document still using the old name, then the changes, once asked.
In Sanity's docs
Where it goes from here
WCAG Watch is live. Next, it updates the cards too.
Sanity features
- Agent Actions
- Functions
WCAG Watch already checks the standards every morning and reports what changed. Its next step is updating the cards itself: finding the cards a change touches, updating them with the date and source, and publishing them together as one Content Release. Those updates will go out without a person reading them first, with a pause switch and a one-step revert, and that will be the one exception to a person publishing. Agent Actions’ Translate will also put the cards into other languages, each translation a draft until someone has read it. Both will show up in the Agents section with their runs.