Architecture
Wonderpress is a set of separate projects with deliberate boundaries. This page is the contract: who owns what, so the seams stay clean as everything evolves.
The layout of a project
~/projects/acme/ ← the environment root (development environment)├── .wonderpressrc ← backend, namespace, resolved versions├── .wp-env.json ← wp-env backend config (Docker)├── composer.json / phpcs.xml ← project-level lint tooling├── AGENTS.md / CLAUDE.md ← generated agent conventions└── wp-content/ └── themes/<theme>/ ├── .wonderpress/ │ └── manifest/ │ ├── partials/*.json ← component dictionary │ ├── page-templates/*.json ← page-template contracts │ └── schema/*.json ← JSON Schemas for editor completion ├── src/partials/ ← generated partial classes ├── partials/ ← view templates ├── blocks/<slug>/ ← opt-in Gutenberg wrappers ├── static/ ← Static Kit's tree (src/ and dist/) ├── vendor/ ← Composer deps, including wonderpress-core ├── theme.json ← design tokens, source of truth └── functions.php, inc/ ← per-project PHP
WordPress core files install around this. Wonderpress never touches them.
Ownership boundary
| Concern | Owner | Where it lives |
|---|---|---|
| PHP partials and templates (the render layer) | Wonderpress CLI | themes/<theme>/partials, themes/<theme>/src |
| Partial manifests (always emitted) | Wonderpress CLI | themes/<theme>/.wonderpress/manifest/partials/*.json |
block.json + render.php (opt-in wrapper) | Wonderpress CLI | themes/<theme>/blocks/<slug>/ |
The static/ tree (layout, .staticrc) | Static Kit | themes/<theme>/static |
| Component style stubs and JS behavior classes | Static Kit | static/ (created via delegation) |
src/ → dist/ asset compilation | Static Kit | static/src, static/dist |
Block registration, partial base classes, wonder_* helpers | wonderpress-core | themes/<theme>/vendor/wndrfl/wonderpress-core |
Per-page asset convention (dist/{css,js}/<body_id>) | wonderpress-core | vendor/…/inc/assets.php |
| Baseline theme supports (html5, align-wide, title-tag…) | wonderpress-core | vendor/…/inc/setup.php |
Menu locations, image sizes, text domain, style.css | Theme | themes/<theme>/inc/setup.php, style.css |
A partial is not the same thing as a block
A partial is a Wonderpress rendering primitive: a button, a section, a link. It turns properties into HTML, is often composed inside other partials, and has no inherent relationship to the editor.
A block is a formal WordPress notion: block.json plus register_block_type() is what surfaces something in the Gutenberg inserter. Its whole reason to exist is editor availability.
The two are orthogonal, so the system treats them that way. The partial and its manifest are the always-on output. The block is an opt-in editor wrapper (--block) whose render.php delegates straight back to the partial. A block is a partial you also chose to expose in Gutenberg, and the relationship never runs the other direction. A block cannot exist without a partial; a partial needs no block.
Blocks are dynamic and server-rendered: render.php news up the partial class and echoes ->render(). The PHP partial is the only source of HTML. Editor presence is one buildless script in wonderpress-core (editor-preview.js) that registers each block with inspector controls drawn from the manifest schema and previews via ServerSideRender. Generated block.json carries no editorScript. There is no per-block JSX and no editor bundler.
Manifests are the source of truth
Every component the CLI creates gets a manifest at .wonderpress/manifest/partials/<slug>.json inside the theme, recording what the component is and exactly which files were written for it:
{ "name": "Call_To_Action", "slug": "call-to-action", "block": "wonderpress/call-to-action", "acf_compatible": false, "properties": [{ "name": "body", "type": "string", "required": true, "description": "" }], "artifacts": { "class": "src/partials/class-call-to-action.php", "view": "partials/call-to-action.php", "block": "blocks/call-to-action/block.json", "render": "blocks/call-to-action/render.php", "style": "static/src/scss/components/_call-to-action.scss", "script": "static/src/js/components/CallToAction.js" }}
partial list, partial remove, block create, block list, and block remove all read manifests rather than scanning source files, so the deletion list for a removal is exactly what creation recorded. The manifest never advertises a file that does not exist: style and script are recorded only when Static Kit actually wrote them.
Field definitions live in the manifest, and the PHP class $_properties and block.json are generated from it. Edit the manifest, then run wonderpress partial sync <Name>. Editing the generated files by hand creates drift, which wonderpress partial check-drift (and wonderpress lint) will catch. See Partials.
Page templates have manifests too, under .wonderpress/manifest/page-templates/, declaring composition and the editor contract. See Page templates. Each manifest points at a generated JSON Schema via $schema, so your editor completes keys and flags mistakes while you type.
wonderpress-core is a dependency of the theme
Core supplies the runtime the CLI’s output is written against: Abstract_Partial, the wonder_* helpers, and the init pass that calls register_block_type() on every block.json the CLI emitted. The CLI writes the block; core registers it.
It is a Composer dependency declared in the theme’s own composer.json and installed to wp-content/themes/<theme>/vendor. It was an mu-plugin until 2.0.0, and two things moved it:
- Nothing in it needs mu-plugin load order. Its earliest hook is
init. - It cannot function without a theme. It registers blocks in the active theme’s
blocks/directory and resolves partial views throughlocate_template(). A dependency that cannot run without its dependent belongs inside it.
The test for what belongs in core is whether a project edits it. Plumbing that ships identically everywhere (the per-page asset convention, baseline theme supports) lives in the package, so changing a convention is a composer update rather than an edit to every site ever shipped. Design surface stays in the theme: templates, theme.json, the partial views, and the decisions in inc/setup.php. Anything moved into the package ships with a filter that lets a project opt out (wonderpress_asset_candidates, wonderpress_theme_supports, wonderpress_disable_emojis, wonderpress_dequeue_block_css).
Two dependency trees, two policies
- Static Kit’s
node_modulesis never committed. It is a build-time toolchain that never ships. The development environment’s.gitignoreenforces this. Do not “fix” a missingstatic/node_modulesby committing it; run the CLI ornpm installinsidestatic/. - The theme’s
vendor/andcomposer.lockARE committed.vendor/holds runtime PHP the site cannot serve a page without. Committing it keeps a deploy a file copy, with no Composer on the server, whilecomposer updatestays the upgrade lever.
Delegate, don’t scaffold
The CLI never writes into static/ directly. When a command needs something to exist there, it calls the Static Kit CLI, which owns the location and format: template create delegates page entries, partial create delegates the style stub and opt-in behavior class, partial remove delegates removal, and static compile delegates compilation. If the Static Kit layout changes, Wonderpress inherits it instead of drifting. See Static Kit.
style.css deserves its own footnote: it is WordPress theme identity metadata (the Theme Name: header) and it is never enqueued. Front-end CSS is the Static Kit bundle.