← Back to Blog

I Measured Every File In The 110,000-Star DESIGN.md Catalog. Fifty-Four Of Them Say "WCAG" And Not One States A Contrast Ratio.

· Updated August 24, 2026 · 36 min read A drafting table lit from one side, holding a stack of plain paper sheets, a folded grey swatch, a steel rule and a single warm-brass strip, arranged with wide empty space between them.

A few months ago somebody had a very good idea. Instead of teaching an AI coding agent to design by describing what you want in a prompt every single time, write the design system down once as a markdown file, drop it at your project root, and let the agent read it. Google Labs published a draft specification for the format out of Stitch. A curated collection of them appeared on GitHub. In under five months that collection passed 109,000 stars.

The idea is right. I use a version of it every day. But I did what I always do before recommending something, which is measure it, and the measurement turned up a gap large enough to change how you should use these files.

Then I did the more useful thing, which is write the missing pieces. This post is both halves: what the DESIGN.md catalog actually contains, and the kit that covers everything it does not, from search indexing through structured data through WCAG 2.2 AA through picking a host.

Updated August 24, 2026. A month of building sites to that kit turned up three whole layers it was missing and four things wrong on my own site. Both are in the second half, under "What a month of building to it changed."

What a DESIGN.md is

A plain markdown file describing a visual system in enough detail that a language model can build UI from it. No tooling, no build step, no dependency. You copy one into your project root and say "build the pricing page using DESIGN.md."

It works because a design system is mostly prose and numbers, and prose and numbers are what these models are good at. A Figma file has to be interpreted. A markdown file with --accent: #533afd in it does not.

The reference collection is VoltAgent/awesome-design-md. Read off the GitHub API on August 24, 2026: 109,887 stars, 12,535 forks, MIT licensed, created March 31 2026, last pushed July 31 2026. The companion site is getdesign.md, run by the same team, which also built VoltAgent and Refine.

The catalog, counted

The site advertises 300-plus analyses. The repository, which is the part you can actually read and verify, contains 74 directories under design-md/. Each holds a DESIGN.md, a preview.html, and a preview-dark.html.

I downloaded all 74 and measured them.

Measure Value
Files 74
Median size 28,200 bytes, about 546 lines
Smallest 4,336 bytes
Largest 43,867 bytes
Brands covered AI platforms, developer tools, fintech, automotive, retail, media, plus a 1996 and a 2001 retro pair

These are not thin summaries. The median file is 28 KB of specific, structured description. That is real work, and the collection is genuinely useful as a starting point.

What is inside one

Every file opens with YAML frontmatter carrying a version marker, a name, a long prose description, then machine-readable colors: and typography: maps. Here is the shape, from the payments-brand file:

---
version: alpha
name: Stripi-Inspired-design-analysis
description: An inspired interpretation of Stripi's design language ...
colors:
  primary: "#533afd"
  primary-deep: "#4434d4"
  ink: "#0d253d"
  ink-mute: "#64748d"
  canvas: "#ffffff"
  canvas-soft: "#f6f9fc"
  hairline: "#e3e8ee"
typography:
  display-xxl:
    fontFamily: "sohne-var, 'SF Pro Display', system-ui, sans-serif"
    fontSize: 56px
    fontWeight: 300
    lineHeight: 1.03
    letterSpacing: -1.4px
---

Then nine markdown sections: Visual Theme, Colors, Typography, Layout, Elevation and Depth, Shapes, Components, Do's and Don'ts, Responsive Behavior, and an iteration guide for prompting. Colors are split into Brand and Accent, Surface, Text, and Semantic, each entry a bullet with a human name, a token reference, a hex value, and a sentence saying where it is used.

That last part is the good design decision in the whole format. A hex value alone tells an agent nothing. Ink Mute (#64748d): Helper text, captions, table labels tells it exactly where the color goes. Copy that habit into your own tokens.

The naming tell, and why it matters

Nineteen of the 74 files are named X-Inspired-design-analysis rather than X-design-analysis, and inside several of them the brand name itself is altered. The payments file calls the company "Stripi" throughout. The other 55 name the brand directly.

That inconsistency is the repository quietly acknowledging a real problem. These files describe real companies' visual identities. Using one to learn, or on an internal tool, is fine. Shipping a customer-facing site that reproduces a competitor's exact look invites trademark and brand-confusion trouble that has nothing to do with the license on the markdown.

The MIT license covers the file. It does not cover the trade dress the file describes. Take the structure, not the fingerprint.

What getdesign.md sells

The catalog is free to browse. The paid tier is a private DESIGN.md extracted from a website of your choosing, at $39 at the time of writing, listed as reduced from $59, one-time, no subscription. It includes light and dark themes, theme preview pages, and a private Discord channel. There is a $249 tier, listed as reduced from $299, that bundles a full starter codebase with authentication, payments, AI chat, blog, email and analytics.

I mentioned this service in passing in an earlier post about GitHub's 2026 star leaderboard and wrote at the time that the private option had no published price. It does now. That is the correction, and the price is fair for what it is.

Now the part I measured, which is the part that matters

I searched all 74 files for accessibility content. Here is what came back.

Check Result
Files mentioning WCAG 54 of 74
Files mentioning contrast at all 55 of 74
Files stating a contrast ratio (4.5:1, 3:1, or 7:1) 0 of 74
Files naming a WCAG version or success criterion number 0 of 74
Files citing a 44 pixel touch target 65 of 74
Files citing the 24 by 24 pixel AA minimum 3 of 74
Files mentioning prefers-reduced-motion 0 of 74
Files mentioning a focus ring, focus state, or :focus-visible 25 of 74

Three of those rows deserve unpacking.

Fifty-four files invoke WCAG and none states a ratio. The word appears as a badge, almost always attached to touch-target sizing. An agent reading one of these files is told the palette is accessible and given no number against which to check that claim. "WCAG AAA" with no criterion attached is not a specification, it is reassurance.

The 44 versus 24 confusion is systematic. Sixty-five files cite a 44 pixel target. Some label it "WCAG AAA," which is correct: SC 2.5.5 Target Size (Enhanced) is Level AAA in WCAG 2.1. Others label it "WCAG AA (at least 44 by 44)," which is not correct. The AA requirement is SC 2.5.8 Target Size (Minimum), 24 by 24 CSS pixels, new in WCAG 2.2. Three files mention 24 by 24. Zero name either criterion by number. A design brief that hands your agent the AAA number while calling it AA is teaching it the wrong floor in both directions: too strict where it does not need to be, and silent about the one it actually has to meet.

Zero files mention reduced motion. Every one of them specifies transitions, easing and hover behavior. None specifies the prefers-reduced-motion block that turns those off for people who need them off. That is not a WCAG AA failure by itself, but it is a five-line omission repeated 74 times.

The palettes themselves

Text against surface is the check nobody skips and everybody fails, so I ran it properly.

I parsed the ## Colors section of every file, split the entries into text roles and surface roles by their own subsection headings, discarded border and hairline tokens (borders are held to 3:1, not 4.5:1) and inverse "on-dark" tokens (they pair with brand colors, not surfaces), then computed the WCAG contrast ratio of each remaining text token against every surface declared in the same file, keeping the most favorable result. Fifty-four files parsed cleanly, giving 231 text tokens.

Twenty-three of those 231 tokens, spread across 18 files, cannot reach 4.5:1 against any surface their own file declares. Ten percent.

Two examples so you can check my arithmetic:

  • In the workflow-automation brand's file, Mute is #c5c0b1, described as "lowest-priority text, fine print, low-emphasis captions." Against that file's own Canvas of #fffefb that is 1.8:1.
  • In the telecom brand's file, Body is #7e7e7e, described as "secondary body text on light surfaces, captions, metadata, supporting copy." Against #ffffff that is 4.06:1. Close, and still under the line.

A fairness note, because this matters. These are properties of an extracted markdown file, not audits of those companies' live websites. On the real site the same value may only ever appear at a size where the 3:1 large-text threshold applies, or in a context that is genuinely exempt. What I am measuring is what the file hands your agent, and what the file hands your agent is a token labelled for captions and body copy with no ratio attached and no surface constraint. The agent will use it wherever captions go.

That is the gap in one sentence: a DESIGN.md describes appearance beautifully and specifies almost nothing you can fail.

So write the other five files

DESIGN.md solves one layer. A shipped site needs five more, and every one of them is the same idea: plain markdown at your project root that the agent reads before it writes a line.

Here is the set I use, and what goes in each.

SEO.md

The rules that are verifiable from rendered HTML. Titles at 50 to 60 characters. Meta descriptions at 150 to 170 characters with a hard ceiling of 170, because past that Google truncates the part you wrote for the click. That number is not advice on my site, it is a build gate: audit-meta-descriptions.mjs --gate runs inside my deploy script and blocks the deploy on a single violation. A rule you enforce by hand is not a rule.

The sitemap section is where most generated sites quietly go wrong. lastmod has to be true. Emit it from a real dateModified field, never from the build clock. A sitemap where all 1,166 URLs changed today is a sitemap that stops being read. If a page has no modification date, emit its publish date, and if you have neither, omit lastmod entirely rather than invent one.

SCHEMA.md

Structured data, written as an entity graph rather than a pile of blocks. Give every recurring entity a stable @id and reference it by @id everywhere else.

https://example.com/#organization        Organization, site-wide
https://example.com/#person              Person, the author or owner
https://example.com/#website             WebSite, plus SearchAction
https://example.com/{path}#webpage       WebPage, one per page
https://example.com/{path}#breadcrumb    BreadcrumbList, one per page

On this site that graph currently produces 390 Person references, 386 SoftwareApplication, 349 BreadcrumbList, 160 WebPage, 81 matched Question and Answer pairs, and 22 Organization nodes across the templates, all pointing at the same handful of @id values rather than repeating anonymous copies.

The rule that took me longest to learn is worth stating flatly: emit exactly one page-type node per page. I used to ship a ProfilePage block alongside two anonymous WebPage blocks on the same URL. Three nodes competing to answer "what is this page." That was enough to break the binding between the person entity and the bio page it was supposed to anchor. One node, and Person.mainEntityOfPage pointing at that node's exact @id.

Two more that generic validators get wrong. FAQ parity is mandatory: every Question in your FAQPage must exist as visible text, worded the same way. And shipping and return fields belong on physical goods only, detected by the presence of gtin, mpn, isbn, productID or itemCondition. Adding them to a Service or LocalBusiness node is a false positive that a lot of tools will cheerfully recommend.

AEO.md

Answer Engine Optimization, meaning being retrievable and quotable by AI answer engines rather than only by search crawlers. The publishable surface is small and mostly one-time work:

/llms.txt and /llms-full.txt, /AGENTS.md, /feed.xml and /feed.json, /ai.txt, /.well-known/security.txt (RFC 9116), and /.well-known/api-catalog (RFC 9727). Then a markdown twin of every substantive page, advertised in the head:

<link rel="alternate" type="text/markdown" href="https://example.com/blog/slug.md" title="Markdown source">

If your host lets you negotiate content, serve markdown to Accept: text/markdown and set Vary: Accept on those responses. Skip the Vary header and a CDN will eventually collapse the markdown and HTML entries into one cache object, after which some visitors get markdown in a browser and some agents get HTML, at random, and it is miserable to reproduce.

On crawler posture, take an allow-list rather than a deny-list unless you have a reason. Blocking every AI crawler and then wondering why you are never cited in an AI answer is self-inflicted. Blocking them on purpose, because your writing is your product, is a legitimate business decision. Doing it by accident is the only wrong answer.

What actually makes a page quotable is simpler than the file list: answer the question in the first two sentences of the section, make each heading the question a person would type, put the number in the sentence rather than only in a chart, keep dateModified honest, and cite your sources with real links. Pages that cite get cited.

A11Y.md

This is the file that keeps a generated site out of trouble, and it is the exact hole the DESIGN.md catalog leaves.

Target WCAG 2.2 Level AA. Because 2.2 is backward compatible with 2.1, conforming to 2.2 AA covers 2.1 AA as well, with one exception worth knowing: SC 4.1.1 Parsing was removed in WCAG 2.2, because the failures it described are handled by browsers now. If a checklist still gates on 4.1.1, that checklist is 2.1-era and you can stop spending effort there.

The six criteria that are new at AA in 2.2 are the ones a generated site fails most often, because they describe patterns that are everywhere in modern layouts:

  • 2.4.11 Focus Not Obscured (Minimum): a sticky header or a cookie banner must not fully cover the element that has focus. This is the most commonly introduced 2.2 failure, because sticky headers are the default in every template.
  • 2.5.7 Dragging Movements: any drag interaction needs a single-pointer alternative.
  • 2.5.8 Target Size (Minimum): 24 by 24 CSS pixels, with documented exceptions for inline links and essential controls.
  • 3.2.6 Consistent Help: if a help mechanism appears on multiple pages, it appears in the same relative order.
  • 3.3.7 Redundant Entry: do not ask for the same information twice in one process.
  • 3.3.8 Accessible Authentication (Minimum): no puzzle, memory task or transcription without an alternative, and allow paste into password fields.

Around those sit the 2.1 AA criteria that carry forward. My own accessibility audit tool checks 36 distinct criterion and check-name pairs across 31 success criteria, and 19 of those pairs can produce a critical AA failure rather than a warning, which is the split that matters when you are triaging: 1.1.1, 1.2.2, 1.3.1, 1.4.4, 2.1.1, 2.2.1, 2.2.2, 2.4.1, 2.4.2, 2.4.4, 2.4.6, 2.4.7, 3.1.1, 3.3.2 and 4.1.2 are the ones that break the page for somebody rather than merely degrading it. (Those numbers used to read 37 and 22 in this post. I re-counted them against the file this week, and two of the 22 turned out to be unreachable. That is the second half of this article.)

And then the rules that stop an agent creating new problems: compute contrast rather than estimating it, never emit outline: none without a :focus-visible replacement, never let aria-label contradict the visible label (SC 2.5.3, which is what breaks voice control), never nest an interactive element inside another, never set tabindex above zero, and include a prefers-reduced-motion block in every stylesheet you generate.

DEPLOY.md

Where it lands, and what has to pass before it does. I re-read the three big free tiers this week rather than trusting what I remembered.

Cloudflare Pages Netlify Vercel
Free tier shape free plan 300 credits Hobby
Builds 500 a month, 1 concurrent, 20 min timeout credit-metered 100 deployments a day
Files per deploy 20,000 not published not published
Max asset size 25 MiB not published not published
Custom domains 100 per project included with SSL 50 per project
Requests no published bandwidth cap on free credit-metered up to 1,000,000 edge requests
Redirects 2,000 static, 100 dynamic _redirects or netlify.toml vercel.json
Header rules 100 rules, 2,000 chars each _headers or netlify.toml vercel.json
Next tier up Workers Paid $5/mo Personal $9/mo, 1,000 credits Pro $20/user/mo
Commercial use on the free tier allowed allowed not allowed

That last row is the one that decides it for most small businesses. Vercel's own documentation states that the Hobby plan "restricts users to non-commercial, personal use only." A business site on Hobby is out of policy no matter how little traffic it gets. Cloudflare Pages and Netlify carry no equivalent restriction. If you are shipping something commercial and want to pay nothing, it is a two-horse race, and Vercel is the one you pay for.

Netlify has also moved to a credit model rather than published bandwidth and build-minute allowances, so the familiar "100 GB and 300 build minutes" framing no longer describes it. Free is 300 credits, Personal is $9 a month for 1,000, and the per-credit conversion is not on the pricing page. Budget by watching your first month, not by arithmetic.

Whichever you pick, put a gate in front of the build:

node predeploy-check.mjs && npx @11ty/eleventy

Fail the build on a meta description over 170 characters, a page with zero or two <h1> elements, a hero image path that does not exist on disk, an internal link to a URL not in the build output, a JSON-LD block that does not parse, a sitemap lastmod later than today, an image without width and height, or an outline: none with no :focus-visible nearby.

One hard-won note on gates: a gate that cannot find its target and passes anyway is worse than no gate. I shipped one of those. It searched for a marker string, never found it because the file was stored with different line endings, and printed "all blocks in sync" and exited zero for months. Every "not found" branch must increment an error counter and exit non-zero.

What a month of building to it changed

Everything above is what I published the first time. Then I built to it, on my own site and on client sites, and the file turned out to be short one whole layer and two half-layers. Here is what got added, and what measuring my own work turned up while I was adding it.

The five files became nine

COMPONENTS.md, BRAND.md and CONTENT.md are new. The URL of this post still says five, because that is what it was called when it shipped and a live URL is worth more than a tidy number.

The layer nobody writes down

Every site I build ships the same six floating things: a message button and its panel, an accessibility dialog, a cookie banner, a skip link, a sticky header, and a mobile call bar. Not one of them had a written spec anywhere in the kit, and they are exactly where a generated site fails WCAG 2.2, because they are the elements that cover other elements.

So I read the source of eight sites I maintain and counted.

Check Result across the eight
Panels hidden with transform, an offset position or opacity alone, rather than display:none, hidden, visibility or inert 4 of 8
Those same panels also carrying aria-hidden="true" around focusable elements 4 of 8
Any floating element using env(safe-area-inset-bottom) 0 of 8
Sticky headers setting html { scroll-padding-top } 3 of 8
Elevation hovers gated behind @media (hover: hover) and (pointer: fine) 3 of 8
Focus returned to the trigger on every close path, not just Escape 4 of 8

The first row is the one that matters. A panel hidden by sliding it off-screen is still in the tab order. Tab through a closed page on four of my own sites and you walk into an invisible contact form: five or six focus stops on controls nobody can see, on top of a screen reader announcing a form that is not there. The fix is one line, and the reason it survives is that a test asserting el.hidden === true passes while the button is visibly on screen, because author CSS beat the browser's own [hidden] rule. Put [hidden] { display: none !important } at the top of every stylesheet and screenshot every hidden state.

The safe-area row has a simple explanation and no excuse: a fixed element at bottom: 16px sits inside the home-indicator strip on any notched phone. Three sites in the set do use env(safe-area-inset-bottom) somewhere. None of the three uses it on the button that floats above the fold on every page.

The panel spec that came out of this is in COMPONENTS.md in the kit, with working CSS and JavaScript, the focus trap, the inert background, the arithmetic behind the panel's max-height, and the ten traps behind the rules. The one I would put on a poster: a panel that covers the page must be aria-modal, trap focus, inert the background and lock scroll. A panel that does not cover the page must do none of those. Half-doing it is where almost every real defect lives.

The icon pack, and the arithmetic nobody does

BRAND.md is new and it exists because a favicon is nine files, not one, and three of them have a gate you can check.

The maskable icon is where this goes wrong quietly. Android launchers apply a mask, and the W3C spec defines the guaranteed-visible area as a circle with a radius of two fifths of the icon size, which is 409.6 pixels across on a 512 pixel icon. So the thing that has to fit is the mark's diagonal, not its width:

scale = (0.8 x size) / hypot(markWidth, markHeight)

The common shortcut, which is to declare "purpose": "maskable" on the same full-bleed file you already had, is what I was doing. I measured the cost on a real wordmark: 9,755 of 107,285 mark pixels, 9.1%, fall outside the safe circle and get cut. Nobody sees it unless somebody installs the site.

Then I ran the same check across every site in my Downloads folder and found the rest of it.

Check Result
Sites declaring maskable on a file that is byte-identical to the non-maskable one, or on a file that does not exist several, mine included
Sites combining "purpose": "any maskable" on one file, which cannot be correct in both roles 3
apple-touch-icon.png shipping with transparency, which iOS composites onto black 4
Manifests missing a 192px icon or a start_url, so the site is not installable at all 3
og:image declared as 1200x630 while the file is a different shape 2, including this site's default, which was 4032x3024 and 1.28 MB

That last one is mine, it was live, and it means every link to a page here without its own hero image was being centre-cropped out of a portrait photograph. All of it is fixed now: real maskable icons at both sizes, a genuine 1200x630 share image, theme-color set to the actual chrome colour in each theme instead of the accent twice, and a manifest that carries id, scope and prefer_related_applications.

The generator is one script and it needs nothing but ImageMagick: make-icon-pack.sh. It takes a logo and a brand colour, emits the whole pack, and then runs three gates: every opaque icon reads mean alpha exactly 1, zero mark pixels fall outside the safe circle, and the share image is exactly 1200 by 630. It refuses to finish if any of them fails.

A few things it encodes that cost me time. On Windows, convert is the FAT-to-NTFS volume converter, so every command starts with magick. magick identify -format emits no trailing newline, so read returns 1 and set -e kills the script on its first line, and once you add the newline it comes out as CRLF and every string comparison fails silently. And a 256-pixel frame in a .ico costs 270 KB on its own, while 48, 32 and 16 together land at about 15 KB regardless of the artwork, because ICO frames are uncompressed bitmaps.

sameAs is an identity claim, and most sites use it as a link list

The original SCHEMA.md gave sameAs one line. It needed a page, because the field does not mean "related links". It means these URLs are the same entity, and a machine reads it that way.

The version of this that taught me the lesson: two co-owned laundromats in neighbouring towns, different addresses, different phone numbers, each listing the other in sameAs. That markup asserts one business at two addresses, which is a real knowledge-graph merge risk, and it validates perfectly. The fix was to remove the sibling from sameAs and keep it as a visible cross-link under a heading that says what it actually is: a separate business, co-owned, not another profile of this one. Not under a heading called "Profiles."

Useful property while you are cleaning this up: sameAs does not have to be reciprocal. Removing one direction weakens the identity claim materially, so a one-sided fix is most of the benefit rather than half a fix.

The rest of the expanded section is the catalogue of anchors that actually move an entity, by vertical, and the pattern across all of them is worth stating on its own: a regulator's own record of you outranks any directory you can sign up for. A state licence lookup, a bar directory, a FINRA or SEC record, an ABMS verification, a SAM.gov entity page. One of those is worth five profile pages you filled in yourself. And an empty profile is worse than no profile, because it is one more page that disagrees with the others about who you are.

The other half of that section is @id discipline, which is the thing you can measure. On this site one identifier, the Person node for me, is referenced 707 times, and 1,586 references across the corpus resolve to 392 distinct nodes. Every one of 346 tool pages points its author at the same node instead of describing a new person. That ratio is the entire goal of a schema file, and it is invisible in any validator. If you want the evidence base for why the node count matters more than the node contents, it is nine entity-binding bugs that pass every schema validator, found across twelve sites.

Then I ran my own checks on my own site

This is the part I would rather not write, which is how I know it belongs here.

345 of 348 tool pages carry date-only values in their JSON-LD datePublished and dateModified. "2026-05-13" passes schema.org and passes the Rich Results Test, and Search Console rejects it with Invalid datetime value and marks the rich result ineligible. It wants the full ISO 8601 form with an offset. That is the single largest outstanding defect in this repository and it is in the kit as a rule I was not following.

336 of those pages carry a BreadcrumbList with no @id. That is currently safe, because nothing on those pages references the breadcrumb by identifier. The moment anything does, Google's parser invents a phantom empty BreadcrumbList at the identifier that does not exist and reports a missing itemListElement. I have shipped that bug twice before on other sites, both times by adding a reference without adding the matching identifier. Breadcrumb depth has its own set of failure shapes, and this one is not on that list because no tool catches it.

Two of the "critical" flags in my own accessibility tool could never fire. The tool marks certain checks as critical AA failures, and it computes that panel by filtering for checks whose severity is fail. Two checks carried the critical flag on a severity expression that could only ever produce warn: captions on prerecorded video, and focus visibility. So a site with a captionless video and outline: none across its whole stylesheet reported zero critical failures, correctly according to the code and wrongly according to the file. Both are fixed, and the fix is impact-gated rather than a label swap: a muted video has no audio to caption and stays a warning, while an audible one with no captions track is now a real Level A failure; and outline: none with a :focus-visible replacement somewhere on the page stays a warning, while outline: none with no replacement anywhere in the served HTML is now a failure. Ten cases in an isolated harness, all behaving as specified.

And the deep links in the kit did not run anything. The kit told readers to append ?url= to a tool address. That parameter fills the field. The one that runs the audit is &autorun=1, and I had never checked, because I always click the button. Worse, on two of the tools the kit named, the field was called urlInput rather than url, so the shared handler could not see it and the parameter was silently discarded into an empty form. That handler now recognises four more field names, which fixes deep linking on eight tools rather than two.

The two frontmatter fields this very post was missing were dateModified and heroAlt. Without the first, the post's structured data quietly claimed it had never been updated. Without the second, its hero image shipped with the entire 137-character headline as alt text, on a post whose argument is that these files claim accessibility without meeting it. Across 1,168 posts: the two fields the build pipeline requires have 100% coverage. The two it merely permits have 3.2% and 2.1%. Same author, same format. That is the whole argument for gates in one table, and I am the counter-example.

Before any of this, you need the tools that build it

None of the above happens without a working terminal, and that is its own afternoon. The install, one script per platform with every package-name difference worked out, is in the AI terminal kickstart. What to do with it once it is installed, from plan mode through the hooks that make an agent deterministic, is the workflow piece.

Two package details from those scripts are worth knowing before you start a site build, because a green installer run does not mean you have them. On Windows, pdftotext and qpdf are Chocolatey-only, so a winget-only machine ends up without them and the installer records that as skipped rather than failed. And nothing anywhere verifies that ImageMagick actually carries its WebP delegate, which the icon script above depends on. One command settles it: magick identify -list format | grep -i webp.

The whole thing as one file

All nine blocks, plus a DESIGN.md template with the accessibility rules baked in and an AGENTS.md stub that points an agent at all of them, are in one downloadable file:

agent-site-standards-kit.md

It is free, needs no signup, and you are welcome to adapt it. Split it into nine files at your project root, or leave it as one STANDARDS.md; agents read either shape. Fill in every FILL: marker, because an unfilled marker is the single most common reason an agent invents a value instead of using yours.

Version 2.0 runs to 2,173 lines, more than four times the file this post originally shipped, and a large share of the added weight is the traps: the symptom, the cause, and the rule that prevents it, for every failure that got past a validator and into production. Those are the parts worth reading twice.

Then one prompt does the work:

Read STANDARDS.md. Build this site to it. Work presentation-only where the file
says presentation-only, keep every change additive and reversible, and verify
contrast by computation rather than by eye.

Do not take the agent's word for it

The failure mode with any of this is the agent reporting that it complied. Check with something that is not the agent. Every tool below runs in a browser, needs no signup and no account.

Append ?url=https://yoursite.example&autorun=1. Both parameters matter: the first fills the field, the second is what actually starts the audit.

What you want to know Run
Everything in one pass: 12 result panels across SEO, schema, E-E-A-T, voice, mobile, performance, accessibility, indexing and retrieval, scored into six buckets, plus a generated fix prompt Mega Analyzer
Eight parallel dimensions: ranking authority, page checks, AEO, performance, security, trust, compliance and helpful-content, rising to ten where field performance and storefront checks apply Mega SEO Analyzer
Thirteen AEO probes: schema, attribution, retrievability, freshness, accessibility, tokenizer, prompt injection, AI-bot meta, speakable, author signals, feed foundation, agent capability declarations, agent-commerce eligibility Mega AEO Analyzer
Ten local probes: NAP consistency, LocalBusiness schema, service area, reviews, hyperlocal, hours, geo, multi-location, categories, directory presence Mega GEO Analyzer
Nine security categories: headers, TLS including post-quantum hybrid key exchange, DNS with SPF, DKIM, DMARC and CAA, server, posture, library versions, privacy, exposed paths, and pattern mapping Mega Security Analyzer
Up to ten URLs side by side as a score matrix Mega Batch
WCAG 2.1 and 2.2 AA: 36 criterion and check-name pairs across 31 success criteria, with the 19 that can produce a critical AA failure separated from warnings WCAG Accessibility Audit
Whether your JSON-LD graph has dangling references and orphan nodes Schema JSON-LD Graph Linter
Whether your FAQ schema matches the visible page text FAQ Schema Parity
Whether your sitemap lastmod values are honest Sitemap Lastmod Truthfulness
Whether your llms.txt matches the spec llms.txt Quality Scorer
Whether an agent can actually use your site Agent-Ready Audit

The full catalog is 346 tools: 266 audits, 43 generators and 37 utilities, all at jwatte.com/tools. Every one of them is free, runs in your browser, and stores nothing.

And here is the table I would rather have had a year ago, which is what none of them check.

Standard in the kit Automated coverage
Computed colour contrast ratio None. Not one tool in the catalog computes a ratio from a rendered page. Every "4.5:1" in a tool's output is reference text.
Visible breadcrumb trail versus the JSON-LD version None as a dedicated tool. One orchestrator checks presence and depth.
prefers-reduced-motion Covered, but by the theme audit, not by the accessibility audit.
Focus visible, target size, reflow Source-level heuristics. They never read a linked stylesheet, so a pass means "not detected".
Whether the copy is true None. That one stays yours.

That first row is not modesty. Contrast is the most-cited accessibility number on the web and it is the one thing a static checker structurally cannot do, because it needs a rendered page, composited alpha, and the element's own opacity multiplied through. A checker that skips those inflates and deflates in both directions: one naive sweep produced 540 phantom failures at around 1.01:1 by reading oklch() values as RGB, and a separate one passed a caption at an effective 2.41:1 because it ignored a 0.6 opacity inherited from a base stylesheet.

Five checks no tool will do for you, and all five take a minute:

  1. Tab through the page without touching the mouse. Can you reach and operate everything, and can you always see where you are?
  2. Set the page to greyscale. Does anything stop making sense?
  3. Zoom to 200%, then set the viewport to 320 pixels wide. Does anything scroll sideways?
  4. Open the floating panel and press Tab. Does focus stay inside it, and does Escape put it back on the button that opened it?
  5. Read the generated copy out loud. If you would not say it, do not ship it.

Where this approach genuinely falls short

Markdown standards do not produce taste. I took apart seventeen expensive real estate sites recently and wrote up what actually made them expensive. The answer was seven numbers and a photography budget. A standards file can carry the seven numbers. It cannot buy the photography, and it cannot make the decision that the section gap should be 240 pixels rather than 96.

A file the agent reads is not a file the agent obeys. Long context degrades. The rules that survive are the ones that also exist as a gate: a script that exits non-zero, a test that fails, an audit you run afterward. Write the file for guidance and build the gate for enforcement, and never confuse the two.

And version 2.0 is now big enough to be its own context problem. At 126 KB it is somewhere north of thirty thousand tokens, which is a real cost to pay on every turn and a real amount of text to stay coherent across. That is exactly why the file tells you to split it into nine and why AGENTS.md points at them individually. A styling task should be reading DESIGN.md and COMPONENTS.md, not the deploy chapter. If your agent supports path-scoped rules, scope them; if it does not, hand it the one block the task needs. A standards file you paste in full every time is a standards file that gets skimmed.

Every number in the deploy table will drift. Free tiers change quarterly. Re-read the pricing page before you commit a client to one.

And the accessibility numbers here are a floor, not a finish line. Passing every automated check is roughly a third of the work. The rest is a keyboard, a screen reader, and somebody who actually uses one.

If you want the full version of this argument, from an empty domain to a working, accessible, indexed site for about a hundred dollars, that is what The $97 Launch covers chapter by chapter, including the WCAG, Schema.org and llms.txt material this post compresses into five files.

Related reading

Fact-check notes and sources

  • Repository figures for VoltAgent/awesome-design-md, 109,887 stars, 12,535 forks, MIT, created March 31 2026, last pushed July 31 2026: read from https://api.github.com/repos/VoltAgent/awesome-design-md on August 24, 2026. Star counts drift daily.
  • The count of 74 directories, the median file size of 28,200 bytes and 546 lines, the range from 4,336 to 43,867 bytes, and every accessibility count in the table: computed locally after downloading all 74 DESIGN.md files from the main branch on August 24, 2026. Anyone can reproduce this by cloning the repo and grepping. The WCAG-mention count is a case-insensitive match on "wcag"; the ratio count is a regex for 4.5:1, 3:1 and 7:1; the reduced-motion count matches both prefers-reduced-motion and the phrase "reduced motion."
  • The contrast analysis: text and surface tokens were parsed from each file's ## Colors section by subsection heading, with border, hairline, divider and inverse "on-dark" tokens excluded, then each text token was scored against every surface in the same file using the WCAG relative-luminance formula, keeping the best result. 54 of 74 files parsed cleanly, yielding 231 text tokens, of which 23 across 18 files fell below 4.5:1 against every surface available to them. Files that did not parse used a different bullet format and were excluded rather than guessed at.
  • The two named token examples are quoted verbatim from the files' own descriptions. As stated in the post, they describe an extracted markdown file and are not an audit of any company's live website.
  • getdesign.md pricing of $39 for a private DESIGN.md and $249 for the starter kit, both one-time: read from getdesign.md/request on August 24, 2026.
  • The DESIGN.md format originates with Google Stitch; Google Labs published the draft specification in 2026 (Google Labs on DESIGN.md).
  • WCAG criteria, levels and version history: WCAG 2.2 at the W3C. SC 2.5.8 Target Size (Minimum) is Level AA at 24 by 24 CSS pixels and is new in 2.2; SC 2.5.5 Target Size (Enhanced) is Level AAA at 44 by 44 and dates to 2.1; SC 4.1.1 Parsing is marked obsolete and removed in 2.2. What's New in WCAG 2.2 is the short version.
  • Cloudflare Pages free-plan limits, 500 builds a month, 1 concurrent build, 20-minute timeout, 20,000 files, 25 MiB per asset, 100 custom domains, 100 header rules, 2,000 static and 100 dynamic redirects: Cloudflare Pages limits, read August 24, 2026. The page publishes no free-tier bandwidth cap.
  • Vercel Hobby terms including up to 1,000,000 edge requests, 100 deployments a day, 50 domains per project, and the quoted non-commercial restriction: Vercel Hobby plan, read August 24, 2026. The restriction is stated there and detailed in Vercel's fair-use guidelines.
  • Netlify free tier at 300 credits and Personal at $9 a month for 1,000 credits: Netlify pricing, read August 24, 2026. Netlify does not publish a per-credit conversion on that page.
  • Schema type counts, the 170-character meta-description gate, the one-page-node rule and the gate-that-passed-anyway story are all from this site's own build.

Added in the August 24, 2026 update. Everything below was measured on that date.

  • The accessibility tool's own numbers, corrected: src/tools/wcag-accessibility-audit/index.html contains 51 check emit sites resolving to 36 distinct criterion and check-name pairs across 31 success criteria, of which 19 pairs carry the critical flag. The earlier "37 and 22" in this post was a miscount. Separately, two of those flagged pairs sat on severity expressions that could only ever produce a warning, so they could never reach the critical panel: SC 1.2.2 Captions and SC 2.4.7 Focus Visible. Both are now impact-gated, verified with a ten-case harness that evaluates the shipped lines rather than a copy of them: an audible captionless video fails and a muted one warns; outline: none with no :focus-visible anywhere in the served HTML fails, and with a replacement present it warns.
  • The component audit covers eight sites I build and maintain, read from their source. The counts are of files in those repositories, not of live rendered pages, so a rule delivered by a stylesheet not present in the repository would not be counted. The sites themselves are not named: they are client businesses that did not ask to be a case study, and nothing in the finding depends on which is which.
  • The maskable safe zone is defined in the W3C Web Application Manifest specification as "a circle with center point in the center of the icon and with a radius of 2/5 (40%) of the icon size", which is where the diagonal-fit arithmetic comes from. purpose values and the rule that an unrecognised purpose alone causes the icon to be ignored are from MDN's manifest icons reference. Chromium installability requirements, including that icons must contain both a 192px and a 512px entry, are from MDN's PWA installability guide.
  • Favicon requirements (square, at least 8x8, larger than 48x48 recommended, crawlable by Googlebot-Image, stable URL, recrawl in days to weeks) are from Google's favicon documentation. The 1200x630 and 1.91:1 share-image guidance is from Facebook's sharing image documentation, which does not commit to WebP support, which is why the kit says to ship PNG or JPEG.
  • The 9.1% clipping figure was measured, not estimated: a 512-pixel mask was rasterised as a circle of radius 204.8 pixels centred on the icon, the mark's non-plate pixels were counted, and 9,755 of 107,285 fell outside it. The .ico byte figures are deterministic because ICO frames are uncompressed bitmaps: 48+32+16 frames produced a 15,086-byte file from four different source logos, and adding a 256 frame took the same file to 285,478 bytes.
  • mask-icon being obsolete: it is absent from MDN's link-type reference, which also states that shortcut icon is non-conforming and must not be used. Safari has used the regular favicon for pinned tabs since Safari 12.
  • This site's own defects, all read from source on the day: 345 of 348 tool pages carrying date-only datePublished or dateModified in their JSON-LD; 336 of 348 carrying a BreadcrumbList with no @id; the default og:image being a 4032x3024, 1,285,108-byte photograph declared in the markup as 1200x630; theme-color declared twice with the same value in both media queries; and no maskable icon file existing despite the manifest declaring one. The share image, the maskable icons at both sizes, the manifest and the theme colours are fixed as of this update. The date-only JSON-LD across the tool pages is not; it is a 345-file edit and it is queued.
  • Frontmatter coverage across 1,168 posts in src/blog/: heroImage 1,168, pageDescription 1,168, dateModified 37, heroAlt 25. Counted by grepping the frontmatter block of every file.
  • The @id reuse figures: 1,586 @id references across the source tree resolving to 392 distinct values, with the canonical Person node referenced 707 times. Counted by extracting only real <script type="application/ld+json"> blocks, with fenced code regions in Markdown masked first. A naive grep -c '"@type"' over the same tree returns 3,535 against a true 3,011, because it also counts schema examples inside code fences in teaching posts and advice strings inside tool JavaScript.
  • The deep-link defect: src/js/tool-url-autorun.js fills a tool's input from ?url= but only clicks the run button when autorun=1 is also present. It also matched only five input element ids, so tools whose field is named urlInput or siteUrl silently discarded the parameter. Four more names are now recognised. Both the kit and the table above use the two-parameter form.
  • "No tool computes a contrast ratio" was established by grepping the whole src/tools/ and src/js/ tree for relativeLuminance, contrastRatio, getContrast and the literal coefficient 0.2126, which returns nothing. The literal 4.5:1 appears in six files, and in all six it is prompt text or a static reference row rather than a computation. The two contrast-measurement failure modes cited (540 phantom failures from reading oklch() values as RGB, and a caption passing at an effective 2.41:1 because element opacity was not multiplied through) are from the retrofit post.
  • The kit itself: version 2.0 is 2,173 lines and about 126 KB, against 573 lines and 28 KB for version 1.0. It is passthrough-copied, so the file served at /downloads/agent-site-standards-kit.md is byte-identical to the one in the repository.

This post is informational, not legal or accessibility-compliance advice. Automated checks cannot establish WCAG conformance on their own; treat them as a floor and test with real assistive technology. Mentions of the repositories, services, hosts and brands named are nominative fair use. No affiliation is implied, nothing here is sponsored, and I have no commercial relationship with any of them.

← Back to Blog

Accessibility Options

Text Size
High Contrast
Reduce Motion
Reading Guide
Link Highlighting
Accessibility Statement

J.A. Watte is committed to ensuring digital accessibility for people with disabilities. This site conforms to WCAG 2.1 and 2.2 Level AA guidelines.

Measures Taken

  • Semantic HTML with proper heading hierarchy
  • ARIA labels and roles for interactive components
  • Color contrast ratios meeting WCAG AA (4.5:1)
  • Full keyboard navigation support
  • Skip navigation link
  • Visible focus indicators (3:1 contrast)
  • 44px minimum touch/click targets
  • Dark/light theme with system preference detection
  • Responsive design for all devices
  • Reduced motion support (CSS + toggle)
  • Text size customization (14px–20px)
  • Print stylesheet

Feedback

Contact: jwatte.com/contact

Full Accessibility Statement • Privacy Policy

Last updated: April 2026