$this->pattern('article-fixtures') ->seed(1201) ->count(5) ->withThumbnail() ->build(fn ($i) => $this ->content()->slug('article-'.$i));
Every WordPress project starts empty
Clone the repo, run it, and there's nothing to look at. So someone spends an afternoon clicking placeholder content into wp-admin — content nobody else has, that vanishes on the next reset. What you build against isn't what anyone else has. If you've used database seeders in Laravel or Rails, this is that idea — brought to WordPress content.
What most teams do today
- Generate random dummy content — a dashboard button fills the site with Lorem Ipsum, but running it twice duplicates everything, and nothing tracks what it made.
- Click content in by hand — nobody else has it, and it's gone on the next reset.
- Copy the live database down — puts real customer data on laptops, stale within a week.
- Share a .sql or export file — opaque; nobody can see what changed in a pull request.
With Muster 🍪
Write one PHP class that says "this site has an About page, five articles, and a main menu." Commit it. Everyone — including CI — runs one command and gets the same site, in a minute.
Run it again tomorrow: nothing duplicates, nothing needs a wipe first. Muster recognises what it already made — and it never touches content it didn't make, so it's safe even in a database with real client pages.
Your first Muster
If you can write a WordPress plugin, you can write a Muster.
One class, one run() method
Every Muster is a plain PHP class. Give each piece of content a stable key() — that's how Muster recognises it again on the next run, even if you rename the slug.
class SiteMuster extends Muster{ public function run(): void { $this->page() ->key('page:about') ->title('About us') ->slug('about-us') ->save(); }}
See exactly what will happen first
--dry-run is a safe rehearsal: Muster reports what it would create, update or leave alone — and writes nothing until you're happy with the plan.
$ wp capstan seed --dry-run Plan: CREATE post [site] page:about → about-us Summary: create=1 update=0 keep=0
One command, real WordPress content
Drop --dry-run and Muster writes the plan for real — through ordinary WordPress functions like wp_insert_post(), so hooks fire and plugins behave exactly as they would in wp-admin.
$ wp capstan seed --seed=1234 Muster prints the plan, then writes.✔ About page created.
No duplicates. No drift. That's the whole idea.
Muster recognises the page it already made and leaves it alone. Change the class, run it again, and only the difference updates — safe to run as often as you like, on any machine.
$ wp capstan seed --seed=1234 Plan: KEEP post [site] page:about → about-us Summary: create=0 update=0 keep=1
Real sites aren't one About page
Repeated posts with featured images, menus linking real pages, comment threads, ACF fields generated straight from your acf-json — every everyday content shape, one builder pattern.
$this->menu('Main Menu') ->key('menu:main') ->postItem($about, 'About') ->link('Contact', '/contact/') ->location('main-menu') ->save();
$this->post('event')->fill([ 'post_title' => 'Launch', 'post_name' => 'launch', 'acf' => ['hero_title' => 'Hello'], 'key' => 'event:launch',])->save();
$this->post('event') ->key('event:example') ->title('Example event') ->acf($this->acfFor('event')) ->save();
$this->comment($about) ->key('comment:about:welcome') ->author('Fixture Editor') ->content('Welcome!') ->status('approve') ->save();
$this->recipe(ArticleRecipe::class) ->featured() ->count(2) ->create();
The declarative manifest
A whole site in one manifest
For the common whole-surface case — some terms, populated content per type, a page per template, a menu per location — declare a manifest instead of writing each builder. assemble() derives all of it through the same primitives, so determinism, ownership, and plan/apply are identical to a hand-written seed.
Each section runs in its own group — wp capstan seed --only=posts:event re-seeds just the events. And anything the manifest can't express — bespoke relationships, conditional content — is a plain Muster class you call() alongside it.
A manifest for the bulk, a class for the exceptions.
public function run(): void{ $this->assemble([ 'terms' => ['topic' => 3], 'posts' => [ 'article' => [ 'count' => 5, 'thumbnail' => true, 'terms' => ['topic' => 'rotate'], ], 'event' => ['count' => 5], ], 'pages' => 'templates', // per template 'menus' => 'locations', // per location ]);}
Content you can trust
Seeding only helps if the seed is trustworthy. Muster makes content declarative, idempotent, and deterministic — plain English for "describe the end state, and re-running it never makes a mess."
🔁 Re-run it forever, safely
A stable key, not the slug, is identity — a second run updates instead of duplicating, even after a rename.
🎲 Same seed, same site
Randomness and time are explicit inputs. Two machines, two months apart, produce identical content.
👀 See it before it happens
Every run plans first — create, update, keep, or delete — and conflicts stop the write.
🔐 It only touches what it made
An ownership record means resetting fixtures never deletes a client's real content in the same database.
⚓ WordPress-native, no ORM
Builders write through wp_insert_post() and friends — hooks fire, caches invalidate, plugins behave normally.
👥 Fixtures live in code review
A colleague can read the diff of your content in a pull request — not a sentence anyone says about a .sql file.
Getting started
- Install Muster as a dev dependency — it's for local setup, CI and disposable test sites, never production.
- Run
wp capstan make musterfirst — it scaffolds a starting file from your theme's real post types and templates, so you're editing, not starting blank. - Always try
--dry-runbefore your first real run. - Muster refuses to run in production — there's no override. A safety net, not a limitation.