Common workflows
Task-level recipes that cross the concept pages. Each chunk is complete on its own: the commands in order, the files you touch, and how to check your work. The concept pages (Partials, Page templates, Manifests, Working with ACF) explain why each step is what it is.
Build a composed page template
The flagship workflow: a bespoke page (a landing page, say) rendered from PHP, hydrated from ACF fields, locked in the editor. Four chunks, each useful on its own.
Chunk 1: create the partials the page is made of
wonderpress partial create --name Landing_Hero --acf \ --prop title:string:required --prop photo:imagewonderpress partial create --name Testimonials --acf \ --prop items:repeater --sub items:quote:string:required --sub items:name:string
--acf marks each manifest ACF-compatible, which is what lets the template register fields for them later. No --block here: these sections live on one designed page, so they have no business in the inserter.
If ACF is not installed yet, this is the moment:
ACF_PRO_LICENSE=... wonderpress acf installChunk 2: create the template with its composition
wonderpress template create --name Landing --lock all \ --section hero-main:landing-hero --section quotes:testimonials
That writes three things: template-landing.php with a real render() call per section, the manifest at .wonderpress/manifest/page-templates/template-landing.json, and per-page Static Kit assets (landing.scss, landing.js). The scaffolded PHP already contains:
( new \Wonderpress\Partials\Landing_Hero( wonder_partial_props( 'landing-hero', 'hero-main' ) ) )->render();( new \Wonderpress\Partials\Testimonials( wonder_partial_props( 'testimonials', 'quotes' ) ) )->render();
--lock all records the editor lock in the manifest: nothing on this page moves, and content is edited through the ACF fields.
Chunk 3: wire it up in WordPress
- Create or open the page in the admin.
- Page → Template → Landing, then Update.
- Reload the edit screen so PHP can apply the manifest (lock, ACF group, native panels).
The editor now shows one ACF field group for the template, with hero-main and quotes as group fields built from each partial’s manifest properties. Fill them in, update, view the page.
Chunk 4: style it
The template created static/src/scss/landing.scss and static/src/js/landing.js by delegation. Each partial created its component stub (_landing-hero.scss, _testimonials.scss). Opt the page into the components it renders, which is the tree-shaking decision:
// static/src/scss/landing.scss@use "lib/utilities";@use "components/landing-hero";@use "components/testimonials";
wonderpress static compile --watchCheck your work
wonderpress template validate Landingwonderpress lint
template validate confirms every composition row points at a real partial and is actually rendered by the PHP. lint runs that plus drift and the token bridge.
Add a partial to an existing template
Say the landing page needs a pricing section later.
- Create it, ACF-compatible:
wonderpress partial create --name Pricing --acf --prop plans:repeater --sub plans:label:string --sub plans:price:string - Add a row to the template manifest’s
composition:
{ "id": "pricing", "partial": "pricing" }- Add the render call where it belongs in
template-landing.php:
( new \Wonderpress\Partials\Pricing( wonder_partial_props( 'pricing', 'pricing' ) ) )->render();wonderpress template validate Landing, then reload the editor and Update the page so the reshaped field group saves.@use "components/pricing";inlanding.scssand recompile.
Add page-only fields to a template
For values that belong to the page rather than to any partial (an SEO description, a settings toggle), add a fields row instead of creating a partial:
{ "id": "seo", "label": "SEO", "properties": [ { "name": "meta_description", "type": "string", "format": "textarea", "rows": 3 }] }
Fields rows have no render of their own. Read the values in the template PHP and write the HTML yourself:
seo = wonder_template_composition_field( 'seo' );template validate checks that the row is consumed, so the call is required, which is the point: a declared field nobody reads is a lie waiting to be found.
Add a field to an existing partial
Fields are defined in the manifest, so the change is a JSON edit plus a sync:
- Edit
.wonderpress/manifest/partials/landing-hero.json, add toproperties:
{ "name": "subtitle", "type": "string" }wonderpress partial sync Landing_Heroregenerates$_properties(andblock.jsonif a wrapper exists).- Use
$subtitlein the view,partials/landing-hero.php. - Reload the editor and re-save any page whose ACF group changed shape.
Never add the field in the PHP class or block.json directly; that is drift, and sync will overwrite it.
Expose an existing partial as a block
When a section graduates from one designed page to something editors place anywhere:
wonderpress block create TestimonialsThat emits blocks/testimonials/ (a block.json plus a render.php that delegates to the partial). wonderpress-core registers it on init; if it is missing from the inserter, run composer install in the theme directory. Then mind the placement rule: a screen where editors insert the block should not also carry the partial’s ACF group, so keep the block off templates that locate those fields.
Add JS behavior to a partial
Most partials have no behavior, so it is scaffolded only on request:
wonderpress partial add-js TestimonialsThat delegates a static/src/js/components/Testimonials.js class to Static Kit. Nothing auto-imports it; wire it into each page entry that renders the partial, mirroring the SCSS @use list:
// static/src/js/landing.jsimport Testimonials from './components/Testimonials.js';new Testimonials();
Then wonderpress static compile. The deliberate import is what keeps other pages from shipping JS they never run.
Remove a section from a template
- Delete the composition row from the template manifest and the render call from the PHP.
wonderpress template validate Landingconfirms nothing dangles.- If no other template or page uses the partial, remove it entirely:
wonderpress partial remove Pricingdeletes every artifact its manifest recorded. A wrapping block refuses the removal until you runblock removeor pass--with-block.