Stage 3: Build
Goal: the site runs locally. Real items render through core functions. Enquiries save, then forward, then purge. The structure tests are green.
Read first: the project's AGENTS.md, then references/architecture.md.
Steps
- Local site. Follow the project's
README.md, section "First run". Then check:ddev wp eval 'echo wp_get_environment_type();'must printlocal. If it doesn't, seereferences/local-env.md. Until it printslocal, the site thinks it is production: it can be indexed and can email real customers. - Rename the example content type. Do this before content exists.
<prefix>_offerbecomes the business's word (tour, service, course): the post type, the rewrite slug and the labels inmodules/content.php, plus the function names (_get_offers,_render_offer_card), theparts/offer-card.phpfile and the tests. Post type names have a 20-character limit. - Model the content. Find every field the brief needs. Register each as post meta with a sanitize callback and an auth callback. Prices are numbers in meta. 0 means "on request", never "free". If editors need richer field screens, add ACF (free) on top, but read values only through core functions.
- Data files → generator (when the owner supplies many items):
- keep one Markdown or JSON file per item in
docs/data/<type>/; - write an idempotent WP-CLI script that creates or updates posts from those files;
- give it a
--dry-run; - run it with
ddev wp eval-file - < scripts/<name>.php. The files are the reviewable truth; the DB is the copy.
- keep one Markdown or JSON file per item in
- Templates call one core function per section.
- Layout lives in the theme: templates,
parts/, skin overrides. - Data, queries, prices, policy sentences and forms come from the core.
- Guard each call with
function_exists(), so a template never fatals if the plugin is missing.
- Layout lives in the theme: templates,
- Enquiries. Keep the pipeline in
modules/enquiry.php(references/forms-and-leads.md).- Add fields in three places together: the form, the handler and the details box.
- Set the inbox and the sender in
wp-config.phpconstants, never in code.
- Policy text (terms, deposits, cancellation): put it in the core as one function (for example
<prefix>_policy( 'cancellation' )). Templates print that function, and a test fails if a template hard-codes a policy sentence. - New module? Add one file in
modules/, onerequire_onceline in the bootstrap, and updatetests/structure/core.spec.tsin the same commit. - Check and commit.
- Run
npm run test:structureandnpm run lint:php(0 errors). - Ask the wp-rules-reviewer agent to review the diff.
- Commit (Conventional Commits).
- Run
Gate (tick in SITE_STATUS.md)
-
wp_get_environment_type()= local - Core plugin and child theme active; skin loads
- Real items render through core functions; no hard-coded cards, prices or slugs
- Enquiry: saved → emailed → retry → purge; tested end to end in Mailpit
- Structure tests green; PHPCS 0 errors
Never
- Business logic,
WP_Query, prices,pll_*calls or policy text in the theme. - Raw
$_POSTor$_GETwithoutwp_unslash+ a sanitize function; output withoutesc_*. - Third-party API keys or endpoints in HTML or JS. Forward from the server.
- Toggling plugin modules with raw
wp option update. Use the plugin's own UI or CLI, then re-check every page it affects. - Unprefixed functions, options, hooks, meta keys or nonces.