A repeatable pipeline for turning a Claude design (HTML/Tailwind, or a Claude Design React/JSX bundle) into a pixel-perfect, production WordPress / WooCommerce theme — fast, consistent across many client sites, and built to pass the official Theme Check / theme-review bar.
The target architecture is Keel, a lean hybrid-classic base theme: the PHP
template hierarchy + a named hook system render the front end, theme.json holds every
design token, and interactive sections are server-rendered React blocks (Interactivity
API). There is no Tailwind in the shipped theme and no Full-Site-Editing .html
templates.
The idea: stop rebuilding designs by hand in wp-admin. Instead, treat WordPress-native
output as a target format. The Keel base theme is cloned per client (tbt → the client
slug); the design's real computed styles are folded into theme.json tokens + scoped
CSS, and each section is ported to a server-rendered block (editable attributes) or a
pattern, composed into one full-page pattern per page (home, services, about, …). The
client creates a Page and inserts the pattern; content stays editable, layout stays intact.
Fidelity is then proven by automated pass/fail gates (static lint + a seeded headless
WordPress render with computed-style diffing and DOM assertions), not by eyeballing.
Built for an agency that designs with Claude but has clients who specifically want WordPress. Editability is agent-native — server-rendered blocks + patterns, no page builders (Elementor/Bricks), and no
wp:htmlfor content.
Disclaimer. This is an independent, community project. It is not affiliated with, endorsed by, or sponsored by Anthropic. "Claude" and "Anthropic" are trademarks of Anthropic, PBC, used here only to describe interoperability. "WordPress" and "WooCommerce" are trademarks of their respective owners.
License. This repository is licensed GPL-2.0-or-later (see
LICENSE), matching the themes it generates. No third-party fonts/images/JS are bundled.
- How it works
- Why Keel (hybrid-classic)
- Repository layout
- Prerequisites
- Quick start
- The base theme
- Converting a design
- Client-editable content
- Header, footer & menus
- WooCommerce
- Theme-review compliance
- Scripts reference
- Quality gates
- Verification
- FAQ
Three moving parts: a reusable base theme, a scaffolder that clones it per client, and a packager that produces an upload-ready zip.
flowchart LR
A[Claude design<br/>HTML/Tailwind or JSX] --> C
B[base-theme/<br/>Keel starter] -->|new-site.sh<br/>clone + rewrite + build| C[sites/<client>/]
C -->|Claude converts:<br/>theme.json tokens + blocks + patterns| D[Themed site]
D -->|npm run build<br/>wp-scripts| E[build/blocks/*]
E -->|lint + seed + assert + style-diff + validate-blocks| Q{QA gates green?}
Q -->|no — fix| C
Q -->|yes| F[dist/<client>.zip]
F -->|wp-admin upload| G[Live WordPress site]
The end-to-end run is wrapped by the /generate-theme skill (asks Theme Name →
Theme URL → WooCommerce, then scaffolds, converts, gates, and packages). By hand:
- Clone the base theme:
scripts/new-site.sh acme "Acme Co"— copiesbase-theme/tosites/acme/, rewrites every identifier from the base slugtbttoacme(text domain,tbt_/Tbt_/TBT_prefixes, block namespacetbt/, CSS--tbt-), and runsnpm install && npm run build. Author defaults to Nextbit Technologies / nextbit.io. - Convert the design (see Converting a design): fold the design's
computed styles into
theme.jsontokens + scoped CSS, port each section to a Keel block or pattern, and compose a full-page pattern per page. - Build the blocks:
npm run build(wp-scripts) →build/blocks/*. - Gate (see Quality gates): lint → seed a headless WordPress → assert DOM invariants → style-diff (computed styles) → validate-blocks → text-parity. All green.
- Package:
scripts/package-site.sh acme→ a cleandist/acme.zip(re-runs the lint). - Upload via wp-admin (Appearance → Themes → Add New → Upload Theme).
The theme is a hybrid-classic theme, not a block theme and not headless:
- PHP template hierarchy + hooks render the front end (
header.php,page.php, …, with a namedtbt_hook()system for chrome). No.htmlFSE templates. theme.jsonv3 is the single token source — colours, fluid type, spacing, widths. Every template and block consumes--wp--preset--*; there is no Tailwind and notailwind.config.jsto keep in sync.- Server-rendered React blocks (
block.jsonapiVersion 3 +edit.js+render.php) carry the editable, repeating, or dynamic sections; front-end interactivity uses the Interactivity API. Becausesave()is null, there is no block-validity ("Attempt recovery") class of bug. - Scoped component CSS (
assets/css/*.css+ per-blockstyle.scss) handles the last few percent — always referencing tokens, never raw values a token covers.
Fidelity comes from the design's real computed styles, extracted headlessly and re-expressed as tokens + scoped CSS (see Converting a design) — not from utility classes.
ClaudeToWP/
├── README.md This file.
├── CLAUDE.md The Keel pipeline playbook (clone → convert → gate → package).
├── base-theme/ The Keel base theme — cloned per client, never edited per-client.
│ ├── style.css Theme header (name, author, license, tags, text domain).
│ ├── theme.json The single token source: palette, fluid type, spacing, widths.
│ ├── functions.php Thin loader: constants + require inc/ modules.
│ ├── header.php footer.php index.php front-page.php page.php single.php
│ │ archive.php search.php 404.php searchform.php comments.php PHP template hierarchy.
│ ├── inc/ setup, enqueue, hooks, template-tags, blocks, woocommerce.
│ ├── template-parts/ site-branding, navigation, content, content-none, entry-meta.
│ ├── blocks/ Server-rendered React block SOURCE (product-showcase); built to /build.
│ ├── patterns/ Section + full-page patterns composing the blocks.
│ ├── woocommerce/ content-product.php + Woo overrides (only where needed).
│ ├── assets/ css/ (base, theme, woocommerce), js/, fonts/ (local), images/.
│ ├── build/ wp-scripts output (generated; shipped in the zip).
│ ├── bin/ The theme's own verify/package/phpcs harness (wp-env).
│ ├── examples/tbt-child/ A child-theme conversion demo.
│ ├── SPEC.md RENAME.md CLAUDE.md DECISIONS.md Theme-internals authority (read these).
│ ├── package.json wp-scripts build (npm run build / start).
│ ├── composer.json phpcs.xml.dist PHPCS: Extra + Docs + WPThemeReview + PHPCompatibility.
│ ├── readme.txt screenshot.png languages/tbt.pot
├── scripts/
│ ├── new-site.sh Clone base-theme → sites/<slug>, rewrite tbt→slug, build blocks.
│ ├── package-site.sh Build (wp-scripts) + lint-gate + runtime-only zip for upload.
│ └── visual-check/ Headless-WordPress QA gates (Puppeteer):
│ ├── lint-theme.mjs Static: specificity risk, wp:html, hotlinks, leftover Tailwind, raw-value/token.
│ ├── seed.mjs Seed Title/Tagline + Pages + nav menu + front page + permalinks.
│ ├── assert.mjs Hard DOM gate: overflow, image load, responsive visibility, mobile nav.
│ ├── style-diff.mjs Computed-style diff (design vs theme); `--extract` emits the porting manifest.
│ ├── validate-blocks.mjs block.json/render.php validity + php -l + zero render-time PHP errors.
│ ├── text-parity.mjs Every design phrase present in the theme.
│ ├── ledger.mjs Runs every gate in order → prints RESULT: DONE.
│ ├── run.mjs Design-vs-theme side-by-side render → dist report.
│ └── example.json Per-site config template: designDir, brand, routes, styleProbes, assert.
├── dist/ Packaged <slug>.zip + visual reports (git-ignored).
└── sites/ Per-client themes (git-ignored; each ships independently).
| Tool | Why | Required? |
|---|---|---|
| Node.js + npm | Build the blocks with @wordpress/scripts (build/blocks/*) |
Yes |
| PHP 8.1+ | Theme runtime; php -l in the gates |
Yes (runtime) |
| Composer | Run PHPCS (Extra + Docs + WPThemeReview + PHPCompatibility) | Optional (recommended) |
@wp-now/wp-now (via npx) |
Boot a real SQLite WordPress for the QA gates | Yes (for gates) |
| Google Chrome | Headless rendering for seed/assert/style-diff (Puppeteer) | Yes (for gates) |
@wordpress/env (Docker) |
WooCommerce + WP_DEBUG QA (npm run env in the theme) |
For stores / deep QA |
| WooCommerce | Shop templates | Only for stores |
Target platform: WordPress 7.0 (floor 6.6), PHP 8.1+. Hybrid-classic theme — not FSE, not headless.
# 1. Scaffold a client theme (clones base-theme, rewrites tbt→slug, builds blocks)
scripts/new-site.sh acme "Acme Co"
# 2. Extract the design's computed styles into a porting manifest (needs a <slug>.json)
node scripts/visual-check/style-diff.mjs --slug acme --extract # -> dist/acme-style-manifest.json
# 3. Convert the design: fold the manifest into theme.json tokens + scoped CSS,
# port each section to a Keel block/pattern (see CLAUDE.md), then rebuild:
(cd sites/acme && npm run build)
# 4. Coding-standards check
(cd sites/acme && composer install && composer lint)
# 5. Run the full DONE ledger — must print RESULT: DONE (see "Quality gates")
(cd sites/acme && npx -y @wp-now/wp-now start --port=8891 --reset --php=8.2 &)
node scripts/visual-check/ledger.mjs --slug acme --theme-url http://localhost:8891
node scripts/visual-check/run.mjs --slug acme --theme-url http://localhost:8891 # then read the side-by-sides
# 6. Replace screenshot.png with the client homepage (≤1200×900, 4:3)
# 7. Package for upload (re-runs the lint gate)
scripts/package-site.sh acme # -> dist/acme.zip
# 8. Upload dist/acme.zip in wp-admin > Appearance > Themes > Add New > Upload Themebase-theme/ is the Keel base theme and the single source of truth. It is never
edited for a specific client — instead it is cloned. It ships:
- The full PHP template hierarchy + template parts (branding, navigation, content),
and a named hook system (
tbt_hook()) for chrome. theme.jsonv3 as the only token source, with a local variable font.- A reference server-rendered block (
product-showcase) and reference patterns (hero-cta,product-grid) demonstrating the per-section recipe. - Its own verify/package tooling (
bin/), a WooCommerce module, and a child-theme demo (examples/tbt-child/). - Everything needed to pass theme review (see below).
Its internals are documented in base-theme/CLAUDE.md, base-theme/SPEC.md, and
base-theme/RENAME.md — read those when working on the theme itself. Because new-site.sh
clones it and rewrites tbt→slug, every client site starts identical and consistent.
Fidelity comes from the design's real computed styles, not from utilities. Full detail
in CLAUDE.md; the shape:
- Extract:
node scripts/visual-check/style-diff.mjs --slug <slug> --extractrenders the design headlessly and writesdist/<slug>-style-manifest.json— computed styles + box metrics for each annotated element. - Tokenise →
theme.json: fold the manifest's repeated values into presets (colours, fluid type, spacing, widths); snap near-duplicates to one token, else mint a named token. No raw value survives into component CSS. - Emit scoped CSS in
assets/css/theme.css(or a block'sstyle.scss), referencing--wp--preset--*, scoped to the section/block class; prefix custom classes (<slug>-*) and qualify anydisplayrule on a core block with the block class so core's later-loaded sheet can't override it. - Port each section to a Keel server-rendered block (editable/repeating/dynamic) or a pattern (static); compose a full-page pattern per page.
- Verify:
style-diff.mjsin diff mode confirms the render matches the design computed-for-computed — the extractor and the gate share one code path.
Each page ships as a full-page pattern. The client creates a Page (Pages → Add New),
inserts the pattern, and the whole design becomes editable blocks in that Page — no
plugin, no wp:html. page.php renders the_content() (title-less — designs carry their
own hero); there is no baked front-page.html (the client sets a static front page the
normal way). Forms / live booking / real maps are left as clean native placeholders for the
relevant plugin block.
Editable fields are block attributes on server-rendered blocks (exposed via RichText /
media controls in edit.js; render.php outputs the escaped, tokenised HTML). This is the
agent-native surface — an agent edits attributes or theme.json, never raw markup. For a
section reused across pages with centrally-controlled design, register it as a synced
pattern and lock structure with "lock": { "move": true, "remove": true }.
Header and footer are PHP (header.php, footer.php) with the shared parts in
template-parts/, rendered on every request via the hook system — edit once, applies
site-wide. Navigation is a classic menu (wp_nav_menu) output by
template-parts/navigation.php; the client edits links under Appearance → Menus and
assigns them to the primary / footer locations. On mobile the menu collapses to a
toggle (.menu-toggle → .primary-menu-container.is-open) driven by the Interactivity API —
the nav is never display:none'd.
WooCommerce support is declared in the theme's Woo module (loaded only when Woo is active).
Styling is via woocommerce_* hooks + tokenised CSS enqueued only on shop pages; PHP
template overrides live in woocommerce/ and are added only where structure must change
(content-product.php, and single-product.php / archive-product.php when needed). The
mini-cart uses Woo's own widget/hooks — there is no .html part.
Generated themes are built to pass the Theme Check plugin and PHPCS (WordPress-Extra
- Docs + WPThemeReview + PHPCompatibility):
- Text domain == theme slug — rewritten per client by the scaffolder across all PHP/JSON and the PHPCS config.
- All globals prefixed with a unique, slug-derived prefix (incl. pattern-file PHP vars).
- Output escaped late (
esc_html,esc_attr,esc_url,wp_kses_post); input sanitized; strings internationalized withtranslators:comments. - Styles/scripts enqueued (versioned, conditional; no hardcoded
<link>/<script>); parent assets viaget_template_*(neverget_stylesheet_*). - Required files ship:
style.css(full header),index.php,theme.json,readme.txt,screenshot.png(≤1200×900, 4:3, the real homepage). - GPL-2.0-or-later. Images bundled in
assets/images/and referenced viaget_theme_file_uri()(never hot-linked); local GPL/OFL fonts declared inreadme.txt.
Self-check: composer install && composer lint and Theme Check on a local WP (the theme's
npm run env). The packager ships the runtime tree only — block source, node_modules,
vendor, Composer/PHPCS/package/wp-env config, bin/, examples/, and the base theme's
docs are all excluded — and warns if readme.txt/screenshot.png are missing.
Clones base-theme/ to sites/<slug>/ and rebrands it:
- Sets the theme name, description, and text domain in
style.css. - Rewrites the base token to the client slug everywhere: text domain, version
constant, enqueue/style handles,
@package, pattern namespaces (agency-base/hero→<slug>/hero), and the PHPCS config. - Rewrites
readme.txttheme name; author/contributor default to Nextbit Technologies / nextbit.io (set in the base theme — override per-site only if asked). - Slug must be lowercase, start with a letter, and contain only letters/numbers/hyphens.
Builds the blocks with wp-scripts (unless --skip-build), runs the static lint as a
blocking gate (SKIP_LINT=1 bypasses), and produces dist/<slug>.zip containing the
runtime tree only, wrapped in a single top-level <slug>/ folder (what WordPress expects).
Ships the PHP hierarchy, inc/, template-parts/, patterns/, woocommerce/, assets/,
and the compiled build/; excludes block source, node_modules/, vendor/, composer.*,
phpcs.xml.dist, package*.json, .wp-env.json, bin/, examples/, and the base theme's
docs. Warns if review assets are missing.
The QA gates — see Quality gates. lint-theme.mjs and style-diff.mjs --extract are browser-free; seed.mjs, assert.mjs, style-diff.mjs (diff mode),
validate-blocks.mjs, text-parity.mjs, and run.mjs drive a headless Chrome against a
running WordPress and are configured by scripts/visual-check/<slug>.json (see example.json).
ledger.mjs runs them all in order and prints RESULT: DONE.
The hard part isn't building the design — it's proving it's right. php -l and PHPCS
pass clean on themes that still have a mobile bar leaking onto desktop, a 0-width hero,
or 27px of horizontal scroll on phones. So fidelity is enforced by pass/fail gates that
must be green before delivery. They live in scripts/visual-check/ and read
scripts/visual-check/<slug>.json (see example.json).
# The whole ledger at once (runs every gate in order → RESULT: DONE):
cd sites/<slug> && (npx -y @wp-now/wp-now start --port=8891 --reset --php=8.2 &) ; cd -
node scripts/visual-check/ledger.mjs --slug <slug> --theme-url http://localhost:8891
# …or individual gates while iterating (same --slug --theme-url):
node scripts/visual-check/lint-theme.mjs --slug <slug> # browser-free
node scripts/visual-check/seed.mjs --slug <slug> --theme-url http://localhost:8891
node scripts/visual-check/assert.mjs --slug <slug> --theme-url http://localhost:8891
node scripts/visual-check/style-diff.mjs --slug <slug> --theme-url http://localhost:8891
node scripts/visual-check/validate-blocks.mjs --slug <slug> --theme-url http://localhost:8891
node scripts/visual-check/text-parity.mjs --slug <slug> --theme-url http://localhost:8891
node scripts/visual-check/run.mjs --slug <slug> --theme-url http://localhost:8891- lint — statically flags: a custom class forcing a non-flex
displaywhile riding on a core flex block (the "mobile CTA on desktop" bug), rawwp:html, hot-linked images, leftover Tailwind utilities, and raw hex/px where atheme.jsontoken exists. Re-run as a blocking step insidepackage-site.sh. - seed — sets the real Site Title/Tagline, creates the Pages + static front page + a nav menu assigned to the theme's locations + pretty permalinks, so the render reflects what ships (Keel's classic nav renders nothing without an assigned menu).
- assert — hard DOM invariants per page × width (1440/768/390): no horizontal overflow, every image loaded, responsive visibility, key sections sized, no char-wrapped buttons, and the mobile menu opens with visible links. Non-zero exit blocks delivery.
- style-diff — the decisive gate: diffs
getComputedStyleof each design element vs its theme counterpart; 0 diffs or it fails.--extractreuses the same collector to emit the porting manifest. - validate-blocks — every built block has valid
block.json+render.php,php -lis clean on every render/pattern, and fetching each seeded page emits zero PHP errors (SSR blocks have nosave(), so there is no isValid check). - text-parity — every design phrase is present in the theme.
- run — renders design vs. theme side-by-side per section →
dist/<slug>-visual-report/. The mismatch % lies; read the side-by-side images at full resolution.
When a gate fails, decide whether it's a real bug or a gate false-positive and fix the right one — never loosen a gate to make a real bug pass.
The gates above are the bar. Round them out with a quick manual pass on a local WordPress before delivery:
- Editability — create a Page, insert the full-page pattern, edit a block attribute in
the normal editor; the change saves and shows (server-rendered blocks, no
wp:html). - Editor parity — the wp-admin editor preview matches the front end (
edit.jsusesServerSideRender+add_editor_style). - WooCommerce (if enabled) — product, single + archive, add to cart, checkout (via the
theme's
npm run envwp-env with Woo active). - Compliance —
composer lintpasses; the Theme Check plugin reports no REQUIRED/WARNING.
Why not headless WordPress + Next.js? Clients here specifically want to edit in wp-admin. Keel keeps the native WP editing experience — the design lands as server-rendered blocks + patterns editable in the block editor.
Why hybrid-classic instead of a block theme (FSE) or Tailwind?
theme.json alone can't express every design, and FSE's save()-validated block markup is
brittle to port into. Keel renders with PHP + hooks (full control), keeps theme.json as the
only token source, and puts editable/dynamic sections in server-rendered blocks — whose
save() is null, so the "Attempt recovery" class of bug can't happen. No Tailwind ships.
Can the base theme itself be submitted to WordPress.org? It's a starter/template; the per-client clone is what ships. Each clone is made self-consistent (slug == text domain, prefixed globals) so it can pass theme review.
Where are the per-client themes?
Under sites/, which is git-ignored — each client theme is deployed/versioned
independently.
Conversion conventions for contributors (and for Claude) live in
CLAUDE.md.