Adding a Brand
PR a new brand to the public catalog — Playwright seed, chrome-mcp verify, hand-curate, lint, submit. About 30 minutes per brand.
Before you start
You need:
- Node 20 or later. Run
node --versionto confirm. - Repo cloned and dependencies installed. From the repo root:
cd site && pnpm install. If you seepnpm: command not found, install it withnpm install -g pnpmfirst. - A brand with an auditable public site. The seed step runs Playwright against a live URL. If the site geo-redirects you, sits behind a login wall, or serves completely different CSS to headless Chromium, you’ll get a skeleton with unreliable tokens. Document that fact in your audit JSON and mark the entry
quality: auto— don’t guess values. - Familiarity with the brand’s visual identity. You don’t need to know every hex code ahead of time — that’s what the tooling extracts — but you should be able to recognize whether the iframe preview in Step 5 looks right. If you’ve never seen the brand’s site, spend 5 minutes browsing it first.
- A clean checkout. Run
git statusbefore starting. The seed script writes directly tosite/src/content/designs/<slug>.md. Starting from a dirty tree makes it harder to see exactly what was generated.
No Anthropic API keys are needed. The curate step runs through your Claude Code Max Plan session (or any Claude Code session), not the Anthropic API.
The 5-step workflow
Each step is covered in its own section below.
- Seed — Playwright opens the brand site, samples CSS, and writes a skeleton
.mdwith[TODO]placeholders. - Verify with chrome-devtools-mcp — Pull live production token values before writing prose. Mandatory.
- Curate — Replace every
[TODO]with actual prose. 15 sections, real hex codes, no fabrication. - Lint + alias — Run the linter and backfill canonical role aliases.
- Submit — Branch, commit, open PR.
Budget about 30 minutes for a brand you know well; 45–60 for an unfamiliar one.
Step 1 — Seed
Run scripts/seed-from-list.mjs from the repo root, passing the brand slug, display name, canonical source URL, categories, and tags:
node scripts/seed-from-list.mjs \
--slug pinterest \
--name Pinterest \
--source-url https://www.pinterest.com \
--categories social,media \
--tags 'light,minimal,sans,grid,image-first'
The script opens a headless Chromium via Playwright, navigates to --source-url, samples computed styles from the DOM (background colors, text colors, font families, font sizes, border radii, spacing), and writes site/src/content/designs/<slug>.md.
What you get:
- Frontmatter populated from the sampled CSS — colors, typography, radii, spacing.
- 15 markdown sections, each containing one or more
[TODO]placeholders. quality: auto— this field upgrades tocuratedonly after you’ve filled in all placeholders and manually verified the tokens.
The seed takes 30–60 seconds. Playwright needs to download a Chromium build on first run — that download happens once and is cached.
If the seed fails with a message about ANTHROPIC_API_KEY: the old scripts/extract/index.mjs pipeline that needed that key was deleted on 2026-05-28. If something still references that path, it’s stale — use seed-from-list.mjs instead.
Batch mode — Adding 5 or more brands at once? Use a JSON list file instead of repeating CLI flags:
# Create seeds/2026-MM-<wave-name>.json with an array of brand objects:
# [{ "slug": "wayfair", "name": "Wayfair", "source_url": "...", "categories": [...], "tags": [...] }, ...]
node scripts/seed-from-list.mjs --in seeds/2026-MM-<wave-name>.json
See the full runbook for the batch workflow and the commit message convention for wave commits.
Step 2 — Verify with chrome-devtools-mcp
The Playwright seed samples a snapshot of computed styles, but it can miss tokens that only appear in interactive states, above-the-fold hero elements that load asynchronously, or CSS custom properties defined in a separate design-system package. The chrome-devtools-mcp probe reads from the live, fully-rendered DOM — it’s the source of truth.
Run the probe in your Claude Code session:
- Open the brand’s homepage in chrome-devtools-mcp using the
new_pagetool. - Run
evaluate_scriptwith the following snippet:
() => {
const cs = (el) => el ? {
bg: getComputedStyle(el).backgroundColor,
color: getComputedStyle(el).color,
font: getComputedStyle(el).fontFamily.split(',')[0].replace(/['"]/g, ''),
size: getComputedStyle(el).fontSize,
weight: getComputedStyle(el).fontWeight,
radius: getComputedStyle(el).borderRadius,
} : null
const all = [...document.querySelectorAll('button, a')].slice(0, 200)
const seen = new Set(), ctas = []
for (const el of all) {
const r = el.getBoundingClientRect()
if (r.width < 80 || r.height < 28 || r.top < 0 || r.top > 1500) continue
const s = getComputedStyle(el)
if (s.backgroundColor === 'rgba(0, 0, 0, 0)') continue
if (seen.has(s.backgroundColor)) continue
seen.add(s.backgroundColor)
ctas.push({ text: el.innerText?.slice(0, 24), bg: s.backgroundColor, color: s.color, radius: s.borderRadius })
if (ctas.length >= 3) break
}
return { body: cs(document.body), h1: cs(document.querySelector('h1')), ctas }
}
- Write the JSON output to
audit-shots/<slug>-tokens.json. - Take a screenshot of the hero viewport with the
take_screenshottool and save it toaudit-shots/<slug>-hero.png.
These two files are audit evidence. They do not get committed (add them to .gitignore if they’re not already excluded), but keep them locally until your PR is merged — maintainers may ask for them during review.
If chrome-devtools-mcp won’t open a new page because a previous session left the browser locked:
pkill -f "chrome-devtools-mcp/chrome-profile"
If the brand site blocks headless browsers or geo-redirects you: document that fact in the audit JSON ("blocked": true, "reason": "geo-redirect to regional domain"), proceed with seed data only, and keep quality: auto. Flag it in your PR description.
What to extract at minimum: CTA background color, CTA text color, CTA border radius, body background, body text color, body font family (exact name, not the CSS stack), and h1 font family. If the brand uses a custom typeface, note the weight and any variable-font axis values you can read from getComputedStyle.
Step 3 — Curate
Open site/src/content/designs/<slug>.md and replace every [TODO] placeholder with real prose. There are 15 sections. All of them need to be filled in — see the quality bar for the minimum ships threshold.
The tokens from Step 2 are authoritative. If the seed frontmatter disagrees with the chrome-mcp audit output, trust the audit. Correct the frontmatter before writing the prose sections.
Structure and tone: Look at site/src/content/designs/stripe.md and site/src/content/designs/are-na.md as reference entries. Both are fully curated. Notice:
- Prose is descriptive and specific, not evaluative or promotional. “Söhne at weight 300” not “beautiful, elegant typography.”
- Each color token is named with its role:
bg,text,brand,border,surface,accent,muted,on-brand. Names map to the canonical 8 roles via thealiases:block (Step 4 handles this automatically). - Hex codes appear in backticks inline in prose —
`#533afd`— whenever the color is being named for the first time in a section. - No marketing voice. Never write “blazing fast”, “industry-leading”, “world-class”, “delightful”, or similar.
Target length: 500–800 lines per entry. Thin entries (under ~300 lines) almost always have placeholder sections that weren’t properly filled in.
The lineage block (typically Section 14 or 15) requires a structured list of source references in this shape:
## 14. Lineage
- **Name:** Stripe Identity System
**Role:** Primary design system
**URL:** https://stripe.com/docs/stripe-js
- **Name:** Söhne Typeface
**Role:** Display + body typeface
**URL:** https://klim.co.nz/retail-fonts/sohne/
Every lineage entry needs a valid URL. A URL that 404s or redirects to a homepage is a merge blocker. If you can’t find a citable URL for a source, omit that entry rather than fabricating one.
Using Claude Code for curation: In your session, paste the seed skeleton and the Step 2 audit JSON into context, then ask Claude to fill in all 15 sections. Give it the audit findings explicitly as authoritative input:
“Curate
site/src/content/designs/<slug>.md. LIVE TRUTH FROM chrome-mcp on 2026-05-28: brand#XXXXXX, body bg#YYYYYY, body font<actual name>, h1 font<actual name>, CTA radiusNpx. THESE ARE AUTHORITATIVE. Replace every [TODO] with prose matching this live truth. Reference entry:site/src/content/designs/stripe.md.”
Don’t let the model fall back to its general training knowledge for token values — it will get them wrong for brands that have rebranded since its training cutoff.
Step 4 — Lint + alias
Run the linter against your new entry:
node packages/design-md/bin/design-md.mjs lint site/src/content/designs/<slug>.md
A clean pass looks like:
✓ No issues found.
15 sections · 8 colors · 4 type roles
Common lint failures:
- YAML parse error — usually a stray single quote or unescaped colon in frontmatter. The error message includes the line number.
- Missing required field —
name,source_url,spec, orqualityis absent. Check the frontmatter top. - < 5 sections — the body was truncated or the seed wrote a malformed section header. Count the
##headings. - missing bg / text / brand — the color role mapping is incomplete. Check that your color tokens include entries for at minimum
bg,text, andbrand(or their aliases).
After the linter passes, backfill the canonical role aliases:
node scripts/add-aliases.mjs site/src/content/designs/<slug>.md
Expected output:
✓ Added aliases to 1 entries
If it says 0 entries updated, the aliases block is already present and correct. If it errors, the linter step likely caught the underlying issue — fix that first.
The aliases: block maps generic role names (background, foreground, primary, etc.) to this entry’s actual token names. It’s what lets downstream tools resolve theme.background to the entry’s bg token without knowing the exact key names. Do not edit the aliases block manually — always regenerate it with add-aliases.mjs.
Step 5 — Submit
Branch first:
git checkout -b content/add-<slug>
Commit message convention:
content(catalog): add <Brand Name> — N → N+1 entries
Where N is the count before your entry and N+1 is after. Check the current count with:
ls site/src/content/designs/*.md | wc -l
Include in the commit body:
- Categories and tags
- Quality level (
autoorcurated) - Any deviations from standard structure (e.g., missing lineage URLs, geo-blocked seed, visual verify pending)
Quality bar for merge:
| Roles covered | Ships? |
|---|---|
| 8 of 8 token roles | Yes — ideal |
| 6 of 8 token roles | Yes — acceptable |
| 5 of 8 token roles | Conditional — maintainer discretion |
| < 5 token roles | No — return to curation |
Open the PR against main. A maintainer will review within a few days. The review checklist is in What we hand-verify before merge below.
What we hand-verify before merge
- Role coverage — at least 6 of 8 canonical roles resolved:
bg,text,brand,border,surface,accent,muted,on-brand. - Accurate hex codes — we re-probe the brand site via chrome-devtools-mcp and compare against the entry. If we find a fabricated or stale value, the PR is sent back.
- All 15 sections present — section headers must match the spec. Missing sections block merge.
- Lineage URLs valid — we check every URL in the lineage block. A 404 or homepage redirect blocks merge.
- Voice — not promotional. We search for “blazing”, “industry-leading”, “world-class”, “delightful”, and similar. Any hits get flagged.
- No
[TODO]placeholders remaining — we grep the body. Even one unresolved placeholder blocks merge regardless of overall quality. qualityfield accurate —quality: curatedrequires all placeholders resolved and a chrome-mcp verification on record. If you have any doubt, usequality: auto.
Common rejection reasons
Fabricated hex codes. We verify every color value against the live site. If the brand rebranded six months ago and your entry uses the old palette, we catch it. Use the chrome-devtools-mcp probe — don’t rely on memory, Google image results, or brand guidelines PDFs that may be out of date.
Thin prose. Eight or more of the 15 sections containing only one or two sentences is not a curated entry. Each section should cover its topic with enough specificity that a developer could implement the behavior without opening the brand’s site.
Broken lineage URLs. Every URL: value in the lineage block gets checked. A 404, an IP address, or a redirect to a homepage all fail. If a source doesn’t have a stable public URL, omit the lineage entry.
Marketing voice. Phrases like “blazing fast”, “world-class design”, “industry-leading”, “delightful experience”, “cutting-edge” will be called out in review. Describe what the design does, not how impressive it is.
No chrome-mcp verification. If your PR description doesn’t mention running the chrome-devtools-mcp probe, a maintainer will ask. Entries added without live verification have a high defect rate — see the 2026-05-28 wave incident above.
quality: curated with placeholders. If [TODO] appears anywhere in the body, quality must be auto. Setting it to curated prematurely is a red flag that the entry wasn’t carefully reviewed before submission.
After merge
Once your PR merges to main:
- Catalog rebuild triggers automatically. The Astro build runs on the hosting provider; no manual step needed.
- Sitemap regenerates. Your brand’s entry page gets a URL at
/design.md/<slug>and appears in the XML sitemap within the next deploy cycle. - Your brand auto-appears in
/design.md/— in search results, category filters, and the full A–Z listing — within about 5 minutes of the deploy completing. - Contributor credit in entry metadata. The
authorfield in the frontmatter is set to your GitHub handle. It appears on the entry page and in the catalog’s contributor list. - CLI and MCP users can fetch it immediately.
npx @webdesignhot/design-md add <slug>andget_design(<slug>)both resolve from the live catalog — no package republish needed.
If you added a brand that was previously listed as a known gap in any docs or launch materials, those references won’t update automatically — file a follow-up issue or PR to remove the “missing” callout.