The Savvy Systems logoThe Savvy SystemsDESIGN. CONNECT. SIMPLIFY.

From design file to live website

A reusable kit for building a client’s WordPress site with Claude: design and approve it in Claude Design, then build it in Claude Code with rules, templates, a 15-step build order, launch checklists and every lesson learned. Proven on a live build. Start here, then work through Build, Launch and Fix it.

Start here

The site kit: design file to a launched WordPress site

Built by
Claude Design for the look, then Claude Code in Local
Lands as
Native Elementor V4 pages with global styles
Edited by
Anyone, in the Elementor editor
Proven on
A live client site, Oct 2026

A folder of instructions, templates, checklists and tools that Claude reads before it builds anything. The design and content change for each client; the process doesn’t. It covers the whole job, from an empty Local site to a launched domain with payments, email and DNS handled.

0% read

Copy the kit into each new site, fill in the blanks, and Claude follows the same rules, build order and checks every time. Start with what’s in it and how to begin, then work through Build, Launch and Fix it.

What’s in the kit

CLAUDE.mdRules + build order. Claude reads it first, every session.
BUILD-LOG.mdWhat’s done, decisions, custom code, what’s left.
page-spec.mdEvery page, section by section, with links.
design-tokens.mdColors, fonts, sizes, radii, buttons.
LAUNCH-PLAYBOOK.mdLocal → host → live, as checklists.
LESSONS.mdEvery gotcha, with its fix.
RESPONSIVE-PROMPT.mdPhones and tablets, desktop untouched.
tools/DNS snapshot and email-recipient check.

Every file is in , ready to copy.

Start a new site

  • Design it in Claude Design first. Get the owner’s approval on every page there, where changes are quick and cheap. Export the approved design as HTML; it becomes the kit’s visual source of truth.
  • Create the site in Local with HTTPS on. Note the PHP version; the host must match it.
  • Make a handoff/ folder next to app/ and copy the kit into it.
  • Add the inputs: reference/ (the approved design, each page labeled by its URL), the filled-in page-spec.md and design-tokens.md, data/ CSVs plus an image-map.csv, every image in images/, and optional phone/tablet previews.
  • Fill in the site rules in CLAUDE.md, and delete the optional parts that don’t apply.
  • Open Claude Code in the Local site folder and give it one instruction:
Read handoff/CLAUDE.md and follow it. Start with step 1 of the build order
and check with me before moving to the next step.

Optional: connect Claude to Elementor first (WP Admin → Elementor → MCP) so it can build through Elementor’s own tools.

The rules that make it work

Site rules — per client

  • Pricing, and what’s bought online vs. quoted.
  • Contact details allowed on public pages.
  • Names never to use (suppliers, manufacturers).
  • Brand colors by product line.
  • Shipping, tax, payments, business details, social links, email routing.

Standing rules — every site

  • Claude never types passwords or keys; the owner pastes them in.
  • No permanent deletes; trash and tell the owner.
  • Downloads need permission; back up before editing data directly.
  • Local is the source of truth; host-only fixes are scripts that also run on Local.
  • Check in a real browser before saying “done”; stop for review every 4–5 pages.

Quality rules

Real Elementor widgets and containers, never one big HTML widget. Every color, font and size from the globals. Match the reference by measuring at 1440px, not by eye. Mockup-only code (placeholder images, fake forms, page switchers) is replaced with real WordPress features. All custom code lives in one must-use plugin, each piece numbered and explained in the build log.

Something broken? Jump to the fix

Pick what you’re seeing. It opens the lesson with the fix.

01Build

Plugins and every build step

The plugin stack the build ran on, the design step in Claude Design, the 15 build steps in Claude Code as they actually happened, the issues that came up along the way, and the mobile rule.

0% read

Each step lists what was done, which plugins it used, and any issue it hit, with a link to the fix.

The plugin stack

Everything the build ran on, what each piece did, and the one thing to watch with it.

Page builder

Elementor + Elementor Pro

Every page, header, footer, form and pop-up, built as native atomic (V4) elements on global styles.

Watch: Pro license per site; reconnect after a domain change.

Theme

Hello Elementor

Near-empty theme, so global styles aren’t fighting theme CSS.

Watch: Nothing to configure; that’s the point.

Shop

WooCommerce

Products, cart, checkout, tax and shipping, plus Coming soon mode during review.

Watch: Share link breaks behind a page cache.

Quotes

Request a Quote plugin (YITH Premium)

Quote-only product lines: quote button instead of price and Add to Cart.

Watch: Single-site license; Cc on its emails needs a filter.

Payments

Card payment gateway

Card payments, kept disabled until launch except one $1 live test.

Watch: Saving keys can reset other gateway settings.

Spam protection

Cloudflare Turnstile

Protects forms, login and checkout.

Watch: Every hostname must be allowed, or nothing submits.

Email

FluentSMTP

Sends store and form emails through the real mail provider.

Watch: Connect on the host only; match SPF.

SEO

Rank Math

Titles, descriptions, sitemap and share images.

Watch: Needs full image URLs; set share images on key pages.

Privacy

Cookie banner (e.g. Complianz)

Consent banner with the least-tracking default.

Watch: List every outside service in the Privacy Policy.

Custom code

Must-use plugin

One file holding every piece of custom code, numbered and explained in the build log.

Watch: Template overrides need checking after plugin updates.

Build toolsClaude DesignLocalClaude CodeWP EngineWP-CLI over SSHHeadless browser checksElementor MCP (optional)

Step by step: what we did

The build as it actually happened: the design step in Claude Design, then the 15 build steps in Claude Code, with what was done, which plugins each used, and the issues that came up. Tick steps off when you reuse it; progress is saved.

0 of 15 done
  1. 00

    Design it in Claude Design

    Designed every page in Claude Design and iterated with the owner until it was approved, while changes were still quick. Exported the approved design as HTML into handoff/reference/ with each screen labeled by its URL, then pulled its colors, fonts and sizes into design-tokens.md and its sections and links into page-spec.md.

    Claude Design
    Handoff to Claude Code

    From here the approved design is the visual source of truth. Claude Code builds it in WordPress and matches it; it doesn’t redesign it.

  2. 01

    Set up Local

    Created the site with HTTPS on, installed Elementor + Pro on Hello Elementor, WooCommerce and the quote plugin, set permalinks to Post name.

    LocalElementor ProHello ElementorWooCommerce
    Issue we hit

    A site built on http pushes to the host looking completely unstyled.

  3. 02

    Load the globals

    Loaded every color, font, type size, spacing step and radius from design-tokens.md into Elementor Site Settings before any page existed.

    Elementor Pro
    Issue we hit

    Kit CSS lost to Elementor’s own rules on specificity; it needed .elementor / .e-con prefixes and a helper script to write it.

  4. 03

    Media

    Renamed every image and uploaded it with alt text from image-map.csv. Nothing hotlinked.

    Media Library
  5. 04

    Store settings

    Address, currency, tax shown as an amount (never the rate), pickup or quoted shipping, payment gateway installed but disabled.

    WooCommercePayment gateway
  6. 05

    Imports

    Products, locations, FAQs and documents imported from the CSVs, in order.

    WooCommerce
  7. 06

    Quotes

    Quote buttons on quote-only lines, with price and Add to Cart hidden there, and the quote form fields set.

    Quote plugin
    Issue we hit

    A true Cc on quote and store emails needed the email-headers filter.

  8. 07

    Header and footer

    Theme Builder templates with the mega menus from the design; a hamburger with tap-to-open sections at 1024px and below.

    Elementor Pro
  9. 08

    Templates

    Single product, product archive and custom post type templates; plugin template overrides kept in the must-use plugin folder.

    Elementor ProMust-use plugin
  10. 09

    Pages

    Built in the order in page-spec.md, matched to the reference at 1440px by measuring, with a stop for review every 4–5 pages.

    Elementor Pro
    Issue we hit

    Editing page data directly left search and share images showing old content until the plain-HTML copy was rebuilt.

  11. 10

    Forms and pop-ups

    Elementor forms with notification recipients from the site rules, Turnstile on every form, timed pop-ups.

    Elementor ProTurnstile
    Issue we hit

    The pop-up blocked itself: “show up to X times” counted before it ever appeared.

  12. 11

    Cart and checkout

    Styled to match, with bought and quoted items handled separately and payment options in the right order.

    WooCommercePayment gateway
    Issue we hit

    Payment order lives in its own setting; only the real checkout shows the truth.

  13. 12

    Legal and privacy

    Privacy Policy listing every outside service, Terms, Accessibility, and a cookie banner.

    Cookie banner
  14. 13

    SEO

    Titles and descriptions, sitemap, redirects for old URLs, share images on key pages.

    Rank Math
    Issue we hit

    Pages without a featured image shared an icon, and root-relative links broke share images.

  15. 14

    Review pass

    Every page at 1440, 768 and 375px in a headless browser; every URL crawled for errors; build log updated.

    Headless browser
  16. 15

    Launch

    Pushed to the host, ran the post-push fixes, gave reviewers logins, ran the $1 payment test, connected email, moved licenses and the domain.

    WP EngineFluentSMTP
    Issue we hit

    The reviewer’s share link only opened the first page behind the host’s cache.

Mobile rule

Desktop1025px+Never changes during mobile work
Tablet768–1024px2 cards per view, 32px sides
Phone767px and below1 card + peek, ~20px sides

Becomes a swipe carousel

  • Product grids, feature cards, galleries, showcases.
  • Location cards, “you may also need”, testimonials.
  • Always with visible arrow buttons and a “1 of X” count, never a hidden-scrollbar strip.

Stays stacked

  • FAQ accordions, every form, the footer.
  • Long text, spec tables, document lists, cart and checkout.
  • Long lists collapse into closed accordions with the item count.

Body text at least 16px, every tap target at least 44px, no sideways scrolling at any width. Work one page at a time with before/after screenshots at 375px and a 1440px check that desktop didn’t move. The full paste-in version is RESPONSIVE-PROMPT.md in the kit files.

02Launch

Local → host → live

Eight phases as checklists, from the first push to the domain move. Written for Local + WP Engine; the order holds for any host.

0% read

Open a phase to tick its items. Progress is saved, so you can pick up where you left off.

Launch: Local → host → live

Written for Local + WP Engine; the order holds for any host. Open a phase to tick its items.

0 done
1. Before the first push +
2. Create the host environment +
3. Right after every push +
4. Reviewer access +
5. Payment live test (shops) +
6. Email sending +
7. Licenses after a domain change +
8. Domain move and go-live +
03Fix it

Lessons, by what you’re seeing

Every gotcha from the build, grouped by the symptom you’d notice, with the cause and the fix. Plus the testing habits that caught them.

0% read

Find the symptom, open it, and follow the fix. Read these before pushing, testing payments or moving DNS.

Lessons, by what you’re seeing

Each one cost real time once. Open the one that matches the symptom.

Pushes and hosting: the pushed site looks unstyled or old +
Unstyled

Local on http makes the push save the WordPress Address as http://. Browsers then block the styles on the https page. Turn on HTTPS in Local first; after a push, fix Settings → General, regenerate Elementor CSS, replace http:// with https://, clear the cache.

Wiped

A database push replaces everything on the host: payment keys, license activations, host-only users, the mailer connection, anything built there. Local stays the source of truth; host-only changes go in a script that also runs on Local.

Old pages

The host page cache can serve old pages for about 10 minutes. Test with ?nc=123 on the URL.

WP-CLI

Over SSH there’s no user, so Elementor refuses saves. Always pass --user=1.

Dev email

The admin email and the store “From” address come over as Local’s dev address. Change both after the first push.

Coming soon: the reviewer’s share link only opens the first page +
Why

The host’s page cache ignores WooCommerce’s share cookie, so every page after the first is served as Coming soon.

Fix

Reviewers log in instead (seamless login or a Shop Manager user), or the host excludes the cookie from cache.

Harmless

The public Coming soon page logs one Elementor console error. It disappears at launch.

Elementor V4: search or share images show old content +
Stale copy

Editing _elementor_data directly leaves Elementor’s plain-HTML copy stale. Rebuild it after direct edits.

Share images

Root-relative image links break share images, and pages without a featured image share their first picture, often an icon. Make URLs absolute and set a social image on key pages.

Specificity

Kit CSS can lose to Elementor’s base rules. Prefix with .elementor / .e-con, and write kit CSS with a helper script.

Data formats

Copy V4 formats (headings, links, form email fields) from an existing element instead of guessing.

Exact match

Matching a reference usually comes down to line heights, list margins and indents. Measure both; aim for within 1px.

Pop-ups: the pop-up never shows, or shows too often +
No “OR”

Triggers can’t be combined with OR. Use a small script that opens the pop-up only when Elementor’s own rules allow it.

Blocks itself

“Show up to X times” counting on open counts before it appears. Count on close and record showings yourself.

Visibility test

A fixed pop-up has no offsetParent; check with the pop-up’s own visibility method.

Close button

The built-in × is 16px. Give it a 44px tap area.

Payments: gateway settings changed after saving keys +
HTTPS

Live mode needs HTTPS, so key entry and the $1 test happen on the host.

Resets

Saving or activating keys can reset the payment page mode, method names and descriptions. Re-check the whole screen every time.

Order

Payment method order is a separate setting. Check the real checkout.

Test emails

A logged-in test fills checkout with that user’s details, so the confirmation goes to them.

Email: site emails don’t arrive +
SPF

Check MX and SPF first. An SPF record ending -all means only the listed provider can send.

Mailbox access

Adding the business inbox as a WordPress user doesn’t let the site send as it. That needs the mailbox owner, the mail admin, or a verified sending service.

Real Cc

Store email “Recipient” takes a comma list; a true Cc needs the email-headers filter. V4 forms have their own To / Cc / Bcc.

Notice

Changing the admin email sends a notice to the old address.

Licenses: Elementor Pro shows “Mismatch” +
Elementor Pro

After a domain change, Disconnect then Connect & Activate on the new site. Other sites stay connected.

Single-site

Premium plugins limited to one site must be deactivated on the old site first.

Turnstile: forms say “Unable to connect to website” +
Fix

Every hostname (Local, sandbox, live) must be listed in the widget’s Hostname Management, or checkout, login and forms can’t submit.

DNS: company email stopped after the domain move +
Snapshot

A public lookup can’t list every record. Export the zone file as well as running the snapshot script.

Email records

Copy MX, SPF, DKIM, DMARC, autodiscover and verification records before moving DNS.

Testing habits that paid off

Crawl every URL

Status, PHP output, old local addresses, http:// links.

Headless browser at 1440 and 375px

Console errors, failed requests, sideways scroll, tap-target sizes.

Side-by-side with the reference

Cropped per section, with measured section heights.

Emails without sending

Intercept pre_wp_mail so nothing goes out.

Real behavior for timed things

Fresh browser profile per pop-up scenario, expectations written down.

Optional: the Elementor MCP

Using the Elementor MCP with the kit (optional) +
Connect

WP Admin → Elementor → Elementor MCP → enable, choose Claude Code, generate the prompt and paste it into Claude Code. It needs the atomic editor (V4) on.

Watch for

HTML-widget shortcuts instead of native elements, code folded into generic snippets, and Claude choosing prices. The kit’s rules already forbid all three; check anyway.

Angie

Elementor’s agent plugin handles the WordPress side (media, WooCommerce, ACF, PHP). Run one agent bridge per site, not two.

Kit files

Every template and tool, plus a glossary

Read any kit file, copy it into a new site’s handoff/ folder, and look up any plugin or tool mentioned in the guides.

0% read

Bracketed items in the templates are filled in per client.

If you only remember this

Put the kit in a handoff folder, fill in the site rules, and let Claude follow the build order one checked step at a time. Local is the source of truth, Claude never touches passwords or keys, and every push is followed by the same fixes. Launch from the checklists, and read the lessons before pushing, testing payments or moving DNS.

The kit files

Pick a file to read it, then copy it into your new site’s handoff/ folder. Bracketed items are filled in per client.

# [SITE NAME] — WordPress build (Claude Code instructions)

> **Template.** Replace every `[BRACKETED]` item, delete the example lines you don't need, and save this as
> `handoff/CLAUDE.md` in the new site's Local folder. Sections marked *(optional)* can be deleted if they don't apply.

You are building the approved [SITE NAME] design as a real WordPress site in **Local**, using **Elementor Pro** on the
**Hello Elementor** theme[, with **WooCommerce**][ and **[QUOTE PLUGIN, e.g. YITH Request a Quote Premium]**]. Read
this whole file before doing anything, then follow the build order at the bottom. Re-read it at the start of every
session; you have no memory between sessions. `BUILD-LOG.md` is the record of what's already done.

## What's in this folder

| Path | What it is |
|---|---|
| `CLAUDE.md` | This file: rules and build order. |
| `BUILD-LOG.md` | What's built, what's left, every piece of custom code. Keep it current. |
| `LAUNCH-PLAYBOOK.md` | Pushing to the host, reviews, payments test, email, licenses, domain move, go-live. |
| `LESSONS.md` | Gotchas from earlier builds. Read it before pushing, testing payments or editing Elementor data directly. |
| `page-spec.md` | Every page: URL, sections in order, headings, buttons and links, which Elementor widget builds each part. |
| `design-tokens.md` | Colors, fonts, type scale, spacing, radii. Load these as Elementor globals first. |
| `reference/[DESIGN FILE].html` | The approved design. **Visual source of truth.** Each screen is wrapped in `data-wp-page="/slug/"`. |
| `data/*.csv` | Import files: [products, quote items, locations/dealers, FAQs, documents, image rename map…]. |
| `images/` | Every image the design uses. Rename per `data/image-map.csv` before uploading. |
| `mobile-guide/` *(optional)* | Phone/tablet previews to follow at 1024px and below. |
| `tools/` | Reusable scripts (DNS snapshot, email-recipient check, any build scripts you add). |

## Site rules (never break these)

Fill these in for the business. The examples show the kind of rule that saved time on earlier builds.

1. **Pricing:** [e.g. Retail prices only. Never show trade/wholesale prices. Under every price show "[PRICING NOTE]".]
2. **Contact details on public pages:** [e.g. No email address on any public page; phone [PHONE] and forms only. The
   business inbox may appear only in form/notification settings.]
3. **Names never to use:** [e.g. supplier or manufacturer names that must not appear in product names, descriptions,
   alt text, filenames or URLs.]
4. **Brand colors by product line:** [e.g. Color A = brand/product line 1; Color B = product line 2 only. Never mix
   them on one component.]
5. **What can be bought online vs. quoted** *(optional, shops)*: [e.g. Line A → Add to Cart and checkout. Line B →
   quote only, no price, no cart.]
6. **Shipping** *(optional)*: [e.g. pickup at [ADDRESS] (free) or "ship to my site (quoted)". No live rates.]
7. **Sales tax** *(optional)*: [rule, and what customers see, e.g. "Sales tax $X", never the rate or city.]
8. **Payments** *(optional)*: [gateway name; test/live keys; gateway disabled until launch except for one small live
   test, which is voided the same day.]
9. **Business details:** [NAME] · [ADDRESS] · [HOURS] · [PHONE] · [languages, if shown].
10. **Social links:** [list, in order]. [Anything never to link.]
11. **Email routing:** [e.g. store and form notifications go To [OWNER EMAIL], Cc [SALES EMAIL]. Customer emails go
    only to the customer.]

## How to build (quality rules)

- **Real Elementor widgets and containers.** Never paste a page as one big HTML widget. A small HTML/Code widget is
  OK only for something no widget can do (e.g. a logo marquee); note it in `BUILD-LOG.md`.
- **Global styles.** Every color, font and size comes from the Elementor globals created from `design-tokens.md`. If
  the design needs a one-off value, note it in `BUILD-LOG.md`.
- **Match the reference exactly**: copy, order, colors, spacing. When asked to match a section, compare it side by
  side with the reference at 1440px and measure (heights, gaps, font sizes, line heights), don't eyeball it.
- **Images:** upload to the Media Library with the filename and alt text from `image-map.csv`. Never hotlink.
- **Mockup-only code in the reference isn't copied:** placeholder image slots, `localStorage`, page switchers, fake
  form submissions, fake captchas. Replace with real WordPress/WooCommerce/Elementor features (Elementor Forms +
  Cloudflare Turnstile for spam protection).
- **Custom code** lives in one must-use plugin (`wp-content/mu-plugins/[site]-site.php`), each piece numbered and
  explained in plain English in `BUILD-LOG.md` → "Custom code used". Template overrides go in
  `mu-plugins/[site]-templates/`. Styling goes in Elementor Site Settings → Custom CSS.
- **Visibility:** pages stay Draft until the owner approves, or the whole site stays behind WooCommerce
  **Coming soon** mode with pages published (use one approach and record which in `BUILD-LOG.md`).
- **Keep `BUILD-LOG.md` current**: what was built, anything that couldn't be matched, every custom code piece, every
  decision the owner made (with the date and their words when it matters).

## How Claude works on this project (standing rules)

- **Passwords and keys are never typed or pasted by Claude**, and never asked for in chat. The owner signs in, and
  pastes API keys, license keys and payment keys into the site themselves.
- **No permanent deletes** (emptying trash, hard-deleting orders, posts or media). Move to trash and tell the owner.
- **Downloads need permission:** say what file, from where and why, then wait for a yes.
- **Back up before editing data directly** (page data, options, plugin files): save a copy in the scratchpad first.
- **Check before claiming done:** render the page in a real (headless) browser, screenshot it, and check console
  errors, failed requests, sideways scrolling and 44px tap targets. Say plainly what was and wasn't checked.
- **Emails are tested without sending** (intercept `wp_mail`), unless the owner asks for a real test.
- **Local is the source of truth.** Anything built directly on a hosted copy (sandbox/staging) must also be added to
  Local, or the next database push wipes it. Prefer a re-runnable script that works on both.
- **Mobile work changes 1024px and below only.** Desktop (1025px+) must not change unless asked.
- **Stop for review** every 4–5 pages, and whenever a request is ambiguous or a rule conflicts with the reference.

## Build order

Finish and check each step before moving on.

1. **Check setup:** WordPress, Elementor + Elementor Pro (licensed), Hello Elementor active, [WooCommerce], [quote
   plugin (licensed)], Settings → Permalinks = Post name. **Turn on HTTPS in Local now** (see `LESSONS.md`).
2. **Globals:** colors, fonts, type scale, spacing and radii from `design-tokens.md` into Elementor Site Settings.
3. **Media:** rename and upload images per `image-map.csv`.
4. **Store settings** *(shops)*: address, currency, tax (rule 7), shipping (rule 6), payments (rule 8, disabled).
5. **Products / content imports:** [list the CSVs in order].
6. **Quotes** *(optional)*: quote buttons on quote-only categories; hide price and Add to Cart there; quote form fields
   = [FIELDS].
7. **Header + footer:** Elementor Theme Builder templates; desktop mega menus/dropdowns as in the reference; mobile
   hamburger with tap-to-open accordions.
8. **Templates:** single product, product archive, any custom post type templates.
9. **Pages:** in the order listed in `page-spec.md`.
10. **Forms & pop-ups:** [contact/quote, applications, submissions…]. Set notification recipients per rule 11.
11. **Cart & checkout** *(shops)*: styled to match; buy items and quote items handled separately.
12. **Legal & privacy:** Privacy Policy (list every outside service: maps, spam protection, fonts, video, payments),
    Terms, Accessibility, disclaimers; cookie banner (e.g. Complianz) with the least-tracking default.
13. **SEO:** titles ~60 characters, descriptions ~155, share images on key pages, sitemap, redirects for old URLs.
14. **Review pass:** every page at 1440, 768 and 375px; every URL loads with no PHP warnings or JS errors; update
    `BUILD-LOG.md`; tell the owner it's ready for review.
15. **Hosting and launch:** follow `LAUNCH-PLAYBOOK.md`.

## Mobile rule

Applies at **1024px and below only**. Never change the desktop layout (1025px and up) while doing mobile work.

**Breakpoints:** desktop 1025px+ · tablet 768–1024px · phone 767px and below.

**Header:** hamburger at 1024px and below. In the bar: logo, cart icon (shops), main call-to-action button. Dropdowns
become tap-to-open accordions inside the hamburger panel; no hover-only menus. Desktop-only top bars are hidden.

**Layout:**
- Multi-column sections stack into one column on phones.
- No fixed pixel widths, and nothing may cause sideways scrolling at any width.
- Images scale to their container (max-width 100%, object-fit cover).
- Side padding is about 20px on phones and 32px on tablets.

**Card rows become swipe carousels** (product grids, feature cards, galleries, showcases, location cards, "you may
also need", testimonials). Phone: one card with a peek of the next. Tablet: two cards with a peek. Swipe, plus visible
left/right arrow buttons. Desktop keeps its grid.

**Keep stacked (not carousels):** FAQ accordions, all forms, footer, long text, spec tables, document lists.

**Text and tap targets:** body text at least 16px on phones; buttons, links, menu items and form fields at least
44 × 44px.

**How to work:** one page at a time, with before/after screenshots at 375px plus a 1440px check that desktop didn't
change. Stop every 4–5 pages for review.
# [SITE NAME] — Build log

> **Template.** Save as `handoff/BUILD-LOG.md`. Claude keeps it current. Newest work goes under its own dated heading;
> "Still to do" and "Custom code used" are updated in place, and their numbers are kept so earlier references match.

## Setup ([DATE])
- WordPress [version], Elementor [version] + Pro [version] ([license plan]), Hello Elementor, [WooCommerce], [plugins].
- Local site URL: `https://[site].local` (HTTPS on). PHP [version].
- Visibility: [Drafts until approval / published behind Coming soon].

## Globals ([DATE])
- Colors, fonts, sizes and radii loaded from `design-tokens.md`: [list variable names or point to the kit].

## Pages
### [Page name] ([DATE]) [✅ approved by OWNER / 🟡 waiting for review]
- Built from: [containers/widgets]. Matched the reference at 1440px; phone pass at 375px.
- Couldn't match: [anything, and why].

## Decisions from the owner
- [DATE] [Name]: "[their words]" → [what changed].

## Hosting
- Environment: [host] `[env name]` (`https://[address]`), PHP [version].
- Pushes: [DATE] (files + database). Post-push fixes done: [list].
- Reviewer access: [Seamless Login / user / share link].
- Wiped by a database push: [payment keys, licenses, mailer connection, host-only users…].

## Still to do
**Waiting on the owner or others**
1. [item]

**At launch**
2. [item] (see `LAUNCH-PLAYBOOK.md` section [n])

**Known gaps (small)**
3. [item]

## Custom code used
### `[site]-site.php` (must-use plugin): what each piece does, in plain English
1. **[Name].** [What it does, why, which hooks/selectors.]

### Template overrides (`mu-plugins/[site]-templates/`)
- [file]: [why it's overridden; compare after plugin updates].

### Custom CSS (Site Settings → Custom CSS)
- [area]: [selectors].

## Couldn't match
- [item]: [reason].
# [SITE NAME] — Page-by-page build spec

> **Template.** One section per page, in build order. Claude builds exactly what's listed here, in this order, with
> the copy from the reference design. Anything not listed gets asked about, not invented.

## Global: header (Theme Builder → Header)
- Top bar (desktop only): [phone, hours, languages, cart link].
- Logo + tagline: [file, tagline text, which widths show the tagline].
- Menu: [items in order; which are mega menus or dropdowns and what they contain].
- Call to action: [button text → link].
- Phone/tablet: hamburger with tap-to-open accordions; in the bar: [logo, cart, CTA].

## Global: footer (Theme Builder → Footer)
- Columns: [logo + blurb, link lists, contact details, social icons in order].
- Legal row: [links, in order]; on phones: [e.g. 2×2 grid, no separators, 44px tall].

## Pages (build in this order)

### 1. [Page name] — `/[slug]/`
- **Sections, top to bottom:**
  1. [Hero: eyebrow, H1, text, button(s) → links, image].
  2. [Section: heading, cards (how many, what's on each), button → link].
- **Widgets:** [containers/grid, heading, text, button, image, loop grid, accordion, form…].
- **Data:** [which CSV or post type feeds it].
- **Phone:** [carousels, stacking, anything special].
- **SEO:** title "[≤60 chars]", description "[≤155 chars]", share image [file].

### Legal pages
- [Privacy Policy (list every outside service), Terms, Accessibility, Disclaimer] — text in `reference/[file]`.

## Links that must work
- [Every button and menu link with its destination, e.g. "Get a Quote" → `/contact/`.]
- [Old site URLs → new URLs (301 redirects).]
# [SITE NAME] — Design tokens

> **Template.** Load these into Elementor Site Settings (global colors, fonts and variables) before building any
> page. Every color, font and size on the site comes from here.

## Colors
| Name | Hex | Used for |
|---|---|---|
| [brand-primary] | #[......] | [buttons, accents, product line A] |
| [brand-primary-dark] | #[......] | [hover, small text on light backgrounds] |
| [brand-tint] | #[......] | [light backgrounds, badges] |
| [secondary / product line B] | #[......] | [only on product line B] |
| [charcoal / ink] | #[......] | [headings, dark sections] |
| [body] | #[......] | [body text] |
| [muted] | #[......] | [captions, notes] |
| [on-dark] | #[......] | [text on dark backgrounds] |
| [surface] | #[......] | [section backgrounds] |
| [line] | #[......] | [borders] |
| white | #FFFFFF | |

## Fonts
- Display: [family, weights]. Body: [family, weights]. Loaded from [Google Fonts / self-hosted].

## Type scale
| Token | Desktop | Phone | Weight / line height / letter spacing |
|---|---|---|---|
| fs-h1 | [clamp(..)] | | [900 / 1.08 / -1px] |
| fs-h2 | | | |
| fs-h3 | | | |
| fs-body | [15px] | [16px min] | |
| fs-small | | | |
| fs-eyebrow | | | [uppercase, letter spacing] |

## Spacing
[4, 8, 12, 16, 24, 32, 40, 48, 64] px · section padding [64 desktop / 40 phone] · gutters [40 / 24 / 16].

## Radii
[r-sm 6, r-md 10, r-lg 12, r-xl 16, pill 999].

## Layout
Content max width [1400px]; narrow text [760px]; card padding [24px]; grid gap [24px].

## Buttons
| Style | Background | Text | Border | Radius | Padding | Hover |
|---|---|---|---|---|---|---|
| Primary | | | | | | |
| Secondary / outline | | | | | | |
# Launch playbook: Local → host → live

Written for **Local + WP Engine**, but most steps apply to any host. Tick items off and note dates in `BUILD-LOG.md`.
The reasons behind each step are in `LESSONS.md`.

---

## 1. Before the first push

- [ ] **Local uses HTTPS** (Local → site → SSL → Trust, and the site URL starts `https://`). If Local is http, the push
      saves the hosted site's WordPress Address as `http://`, and browsers then block the page styles.
- [ ] **No absolute local addresses in live content.** Search the database for the `.local` domain and fix:
  - Elementor page data and templates (use root-relative `/wp-content/...` links)
  - Elementor's plain-HTML page copies (rebuild them from the page data)
  - redirects
  - plugin "return to shop" links

  Leave revisions and attachment GUIDs alone.
- [ ] **Things that need a full URL are generated, not stored:**
  - the email header logo (filter it to the Media Library URL)
  - share images (turn root-relative into full URLs)
- [ ] **Error check on Local:**
  - every published URL returns 200 with no PHP warnings
  - no JS errors or failed requests on key pages at 1440 and 375px
  - the PHP error log has nothing new
- [ ] **Site root is clean:** no preview/test HTML files or `local-xdebuginfo.php` (move them out).
- [ ] **`mu-plugins/` and `uploads/` are present.** Check there's no push-ignore file excluding them.
- [ ] **Record what the push will wipe on the host:** payment keys, license activations, host-only users, the mailer
      connection, and anything built directly on the host.

## 2. Create the host environment (WP Engine)

- [ ] **Create the environment.** A sandbox (free) is fine for review, but **sandboxes can't use a custom domain**. For
      launch, convert it to a billable site or create a production environment.
- [ ] **Match Local's PHP version** in the environment's Overview → Updates → PHP.
- [ ] **Connect Local:** Local → Connect → WP Engine → log in (Local adds its own key).
- [ ] **Push:** Local → site → **Push** → existing site → environment, with **Files: All files** and **Database ticked**.

## 3. Right after every push (with the database)

- [ ] **Settings → General:** both addresses `https://[host address]`. Then Settings → Permalinks → Save.
- [ ] **Elementor → Tools → Regenerate CSS & Data.** Then **Replace URL** from the old address (and from `http://` to
      `https://`) if anything still points there.
- [ ] **Clear the host cache** (admin bar → WP Engine → Caching → Clear all).
- [ ] **Public visitors see Coming soon.** Logged-in or share-link visitors see the full site, styled.
- [ ] **Licenses** (see section 7): Elementor Pro, plus any premium plugins.
- [ ] **Cloudflare Turnstile:** add the host address to the widget's allowed hostnames, with the same keys. Until
      then, checkout, login and forms fail the check.
- [ ] **Re-enter what the push wiped:** payment keys, mailer connection, host-only users.
- [ ] **Crawl every page:**
  - 200 status, no PHP output
  - no old local addresses
  - no `http://` links to the host
  - headless-browser check for blocked style sheets and JS errors

## 4. Reviewer access (owner/client)

- [ ] **Don't rely on WooCommerce's share link on WP Engine.** The page cache ignores its cookie, so only the first
      page opens. Instead, either:
  - the reviewer logs in through **WP Engine Seamless Login** (portal → environment → **WP Admin**), which creates
    their WordPress admin user automatically and survives database pushes; or
  - create a **Shop Manager** login on the host (wiped by the next database push); or
  - ask WP Engine support to bypass the page cache for the `woo-share` cookie.
- [ ] **Send the reviewer the host address** (not the `.local` one, which only works on your computer), with how to
      log in and a note that logged-out visitors see Coming soon.

## 5. Payment gateway live test *(shops)*

- [ ] **Keys are entered by the owner on the host** (live mode usually needs HTTPS, so not on Local).
- [ ] **After saving the keys, re-check the gateway screen:**
  - the payment page option (Redirect vs Embedded)
  - the enabled methods
  - the method's name and description (activating keys can reset them to plugin defaults)
- [ ] **Payment option order:** e.g. Pay at pickup first, then card. Check it on the live checkout, not just in settings.
- [ ] **Hidden test product** (private, $1, not taxable, one per order). Add it with `/cart/?add-to-cart=[ID]` while
      logged in.
- [ ] **Pay $1, then check:** Order Received page; the order is Processing with the gateway's transaction note.
- [ ] **Void it the same day** in the gateway's portal (before the batch settles). Mark the order **Cancelled** with a
      note. Don't also press WooCommerce's Refund button.
- [ ] **Turn the gateway off again** until launch, if the rules say so. Re-check that the wording and order survived
      the save.

## 6. Email sending

- [ ] **Find the mail provider:** `dig MX [domain]` and `dig TXT [domain]`. An SPF record ending `-all` means the
      site must send through that provider, or a sending service added to DNS.
- [ ] **Recipients** (see `tools/email-recipients-check.php`):
  - store emails (new, cancelled and failed orders, quote requests, quote status, gateway alerts)
  - form notifications
  - the WordPress admin email (often still a Local dev address after a push)
  - customer emails go only to the customer
- [ ] **Connect a mailer** (e.g. FluentSMTP) **on the host only.** Either:
  - **Microsoft 365:** an Entra app with redirect `https://[site]/wp-json/fluent-smtp/outlook_callback` and delegated
    Mail.Send, Mail.Send.Shared and offline_access, authenticated as the sending mailbox (needs that mailbox or an
    admin); or
  - **Google Workspace:** FluentSMTP's Gmail connection, with a Google Cloud OAuth app; or
  - **A sending service** (SMTP2GO, Brevo, Postmark): verify the domain with its DNS records, then an API key.
- [ ] **From address:** a real address on the domain, with **Force From Email**. Set WooCommerce's "From" address too.
- [ ] **Test:** one test email to yourself, then one real store email and one form email to the owner.
- [ ] **If the domain's DNS will move soon,** do this after the move (the records would only be redone).

## 7. Licenses after a domain change

- [ ] **Elementor Pro "Mismatch":** on the new site, **Disconnect**, then **Connect & Activate**. Disconnect only
      affects the site you click it on. Check how many sites the plan covers.
- [ ] **Single-site premium plugins** (e.g. YITH): deactivate on the old site (or in the vendor account), then
      activate on the new one. "Maximum activations reached" means the old activation is still in use.
- [ ] **Note in `BUILD-LOG.md` where each license is active,** and that launch needs one for the live domain.

## 8. Domain move and go-live

**Before**
- [ ] **Save the current DNS.** Run `tools/dns-snapshot.sh [domain]`, **and** export the full zone file from the
      registrar/DNS host (a public lookup can't see every record).
- [ ] **Decide where DNS will live.** Safest: keep DNS where it is and change only `@` and `www`. If DNS moves, copy
      **every email record first** (MX, SPF, DKIM, DMARC, autodiscover, verification TXT), or company email stops.
- [ ] **Final push from Local to the production environment,** then all of section 3.

**Go live**
- [ ] **Add the domain** (`@` and `www`) in the host, update DNS to the host's values, and wait for SSL.
- [ ] **Replace the host address** with `https://[domain]` everywhere (Elementor → Tools → Replace URL), then spot-check
      pages, images and maps.

**Straight after**
- [ ] **Email:** connect the mailer (section 6), and check normal company email still sends and receives.
- [ ] **Turnstile:** add the live hostname.
- [ ] **Licenses** on the live domain (section 7).
- [ ] **Payment gateway:** keys, wording and order checked. **Enable it at launch.** Add it to the Privacy Policy.
- [ ] **WooCommerce Coming soon → Live,** and turn the share link off.
- [ ] **Remove sample content and test products.** Switch tax to the automated service if a stand-in table was used.
- [ ] **Check:**
  - cookie banner pages
  - the sitemap submitted
  - share images
  - redirects from the old site's URLs
- [ ] **Remove temporary access:** SSH keys added for remote work, and temporary reviewer users.
- [ ] **Compare live DNS with the snapshot.** Log the launch in `BUILD-LOG.md`.

---

## Working on the host directly (SSH + WP-CLI)

Use this when a change must be made on the host without a push (e.g. to keep payment keys).

1. Create a dedicated key: `ssh-keygen -t ed25519 -f ~/.ssh/[host]_[site] -N "" -C "claude-code [site]"`.
2. The owner adds the **public** key in WP Engine (Profile → SSH keys, or the environment's "SSH login: Add SSH Key").
3. Connect: `ssh -i ~/.ssh/[host]_[site] -o IdentitiesOnly=yes [env]@[env].ssh.wpengine.net "wp ..."`.
   - Run scripts with `wp eval-file - --user=1` (the user is needed, or Elementor refuses saves).
   - Each command takes about a minute, so bundle the work.
   - Back up a file before replacing it, and compare checksums with Local afterwards.
4. **Run the same script on Local,** so the next push keeps the change.
5. **Purge the cache.** The page cache can serve old pages for up to 10 minutes, so add `?nc=[random]` when testing.
6. **Delete the key** in WP Engine when the remote work is finished.
# Lessons from earlier builds

Each item says what happened, why, and what to do instead. Most cost real time once.

## Hosting and pushes

- **Local on http → the push saves the WordPress Address as `http://`.** The pushed site then loads its Elementor
  style sheets over http on an https page, browsers block them, and every page looks unstyled. **Fix:**
  - turn on HTTPS in Local before the first push; or, after a push
  - set Settings → General to https
  - run Elementor → Regenerate CSS & Data
  - run Replace URL `http://` → `https://`
  - clear the cache
- **A database push replaces everything on the host.** That wipes:
  - payment keys
  - license activations
  - users created on the host
  - the mailer connection
  - pop-ups or settings built directly on the host

  Keep Local as the source of truth, and make host-only changes with a script that also runs on Local.
- **Sandboxes can't take a custom domain** (WP Engine). Plan a conversion to billable/production before launch.
- **The host's page cache holds pages for up to ~10 minutes** (`max-age=600`). After a change, test with a
  cache-busting query (`?nc=123`), or you'll be looking at old pages.
- **WP-CLI over SSH runs without a user.** Elementor refuses to save documents unless you pass `--user=1`.
- **The WordPress admin email and the WooCommerce "From" address come over as Local's dev address**
  (`dev-email@wpengine.local`). Change them after the first push, or alerts go nowhere.

## WooCommerce Coming soon

- **The share link doesn't survive WP Engine's page cache.** The first page (with `?woo-share=`) works; every other
  page is served from cache as Coming soon, because the cache ignores the `woo-share` cookie. Reviewers log in
  instead (Seamless Login or a Shop Manager user), or WP Engine support excludes the cookie from cache.
- **The public Coming soon page loads Elementor's script without its settings,** so it logs one harmless console error
  (`elementorFrontendConfig is not defined`). It goes away at launch.

## Elementor (V4 / atomic)

- **Editing `_elementor_data` directly leaves Elementor's plain-HTML copy (`post_content`) stale.** Search, Rank Math
  share images and feeds then show old content. After direct edits, rebuild it with
  `Plugin::$instance->db->get_plain_text($id)` and save it with `$wpdb` (no revision).
- **Root-relative image links in `post_content` break Rank Math share images** (og:image must be a full URL). Add a
  filter on `rank_math/opengraph/facebook/image` and `…/twitter/image` to make them absolute, or set a share image per
  page.
- **Pages without a featured image share their first picture, often an icon.** Set a social image on Home and key
  pages (Rank Math → Social), using the hero photo.
- **Kit CSS can lose to Elementor's base rules on specificity.** Prefix with `.elementor` / `.e-con`, and write kit CSS
  with a helper script, never through a tool that replaces all the kit settings at once.
- **V4 data formats:**
  - heading text: `{"$$type":"escaped-html","value":"..."}`
  - links: `{"$$type":"link","value":{"destination":{"$$type":"url","value":"..."},"isTargetBlank":{"$$type":"boolean","value":true}}}`
  - form email: `email.value.to` is a string-array; `cc`/`bcc` are strings

  Copy formats from an existing element rather than guessing.
- **Matching a reference "exactly"** usually comes down to line heights (normal vs 1.2–1.65), margins under lists,
  badge line heights and indent widths. Measure both and compare section heights; aim for within 1px.

## Elementor Pro pop-ups

- **There's no "OR" between triggers and rules.** "After 45 s OR on the 3rd page view" needs a small script: open the
  pop-up with `doc.showModal(true)` on the 3rd view, but only if `doc.triggers` exists (Elementor's own rules allow it).
- **"Show up to X times" with Count = On Open counts as soon as the pop-up is eligible on page load,** before it shows,
  so it blocks itself. Use **On Close**, and record the showing yourself on `elementor/popup/show` (key
  `popup-{id}-impressions-count`, lifetime 2628288 s for "Month").
- **A fixed-position pop-up has `offsetParent === null`.** Test visibility with `doc.getModal().isVisible()`.
- **The built-in × is 16px.** Give it a 44px tap area in the pop-up's Custom CSS.

## Payments (card gateways)

- **Live mode needs HTTPS,** so do the key entry and $1 test on the host.
- **Saving or activating keys can reset other gateway settings** (the payment page mode, method names and
  descriptions). Re-check the whole screen after every save.
- **Payment method order lives in `woocommerce_gateway_order`.** Check the real checkout, not just the settings list.
- **Logged-in tests fill checkout with that user's details,** so the confirmation email goes to them. Warn the owner,
  or test with your own details.

## Email

- **Check MX and SPF before choosing a mailer.** `v=spf1 include:spf.protection.outlook.com -all` = Microsoft 365
  only; mail from the web host will fail.
- **Adding the business inbox as a WordPress user doesn't let the site send as it.** That needs the mailbox owner or
  the mail admin (Microsoft/Google), or a sending service verified in DNS.
- **WooCommerce store emails take a comma list in "Recipient".** For a real **Cc**, add it with
  `woocommerce_email_headers`; YITH quote emails use the same filter.
- **V4 Elementor forms have their own To / Cc / Bcc fields.**
- **Changing `admin_email` triggers WordPress's "admin email changed" notice** to the old address.

## Licenses

- **Elementor Pro "Mismatch" after a domain change:** Disconnect, then Connect & Activate on the new site.
  Disconnect only sends the current site's address, so other sites stay connected.
- **Single-site premium licenses** (e.g. YITH) need moving: deactivate on the old site, then activate on the new one.

## Turnstile

- **Every hostname (Local, sandbox, live) must be in the widget's Hostname Management.** Otherwise the widget shows
  "Unable to connect to website" and checkout, login and forms can't be submitted.

## DNS and domain moves

- **A public DNS lookup can't list every record.** Export the zone file from the DNS host as well as running
  `tools/dns-snapshot.sh`.
- **Moving DNS without copying the email records stops company email.** Copy MX, SPF, DKIM, DMARC, autodiscover and
  verification TXT records first.

## Testing habits that paid off

- **Crawl every published URL:** status, PHP output, old local addresses, `http://` links.
- **Headless Chrome checks** at 1440 and 375px: console errors, failed requests, sideways scroll, tap-target sizes.
- **Side-by-side screenshots against the reference,** cropped per section, plus measured section heights.
- **Emails checked by intercepting `pre_wp_mail`,** so nothing is sent.
- **Real-behavior tests for timed things** (pop-ups): fresh browser profile per scenario, with each step's
  expectation written down.
# Responsive Prompt — Phones & Tablets

Reusable prompt for making any site responsive without changing the desktop design. Built from lessons on a live client build. Paste the section below into Claude Code (or any builder).

---

**Make this site fully responsive for phones and tablets without changing the desktop design.**

## Breakpoints
- Desktop: 1280px and up. Keep it exactly as it is now.
- Small laptop: 1025–1279px. Same layout, slightly tighter spacing. Switch the menu to a hamburger if the nav items would wrap or crowd.
- Tablet: 768–1024px.
- Phone: 767px and below.

## Rules
1. **Desktop stays untouched.** Put every change inside a media query for tablet or phone. Don't edit desktop styles.
2. **No horizontal scrolling,** ever. Nothing wider than the screen. Remove fixed pixel widths on containers and use max-width plus percentages, or grid with `minmax(0,1fr)`.
3. **Navigation:** at 1024px and below, the full menu becomes a hamburger. The mobile menu must have the same items, order and dropdown groups as desktop, with dropdowns as tap-to-open sections. Every item must be reachable by tap, never by hover only.
4. **Side-by-side cards** (product rows, galleries, benefit cards, logos):
   - Phone: a carousel showing 1 card plus a small peek of the next, with ‹ › arrow buttons and a "1 of X" counter. Arrows sit below the cards, never covering images or text. Swipe also works.
   - Tablet: 2 cards per view, same arrows.
   - No plain sideways-scroll strips with hidden scrollbars. If items sit side by side on mobile, they get arrows.
5. **Keep stacked (no carousel):** forms, FAQ accordions, long text, spec tables, checkout, cart, footer.
6. **Long sections on phones** (FAQs, resource lists, document lists): collapse into tap-to-open accordions, closed by default, with the item count in the header.
7. **Text:** phone body text at least 16px. Headings scale down with `clamp()` so nothing overflows. Long words and URLs wrap.
8. **Tap targets:** buttons, links, chips and form fields at least 44px tall, with at least 8px between them. Inputs at least 16px font so phones don't zoom in.
9. **Images:** `max-width:100%`, height auto, `object-fit:cover` in fixed-ratio boxes. Never stretched or squashed. Hero images crop sensibly on phones.
10. **Multi-column layouts:** 3–4 columns → 2 on tablet → 1 on phone. Odd counts must not leave one orphaned card with empty space beside it.
11. **Spacing:** reduce section padding on phones (roughly 40px top and bottom, 18–20px sides) so it doesn't feel empty or cramped.
12. **Header and footer** must be responsive too: footer columns stack, contact info stays readable, the logo scales.
13. **Pop-ups and fixed bars:** bottom bar on phones, never blocking the whole screen. Easy-to-tap close button.

## Testing (before saying it's done)
- Check at **375px, 390px, 768px, 1024px, 1280px and 1440px**.
- At each size, confirm: no sideways scroll, nothing overlapping or cut off, every menu item reachable, all carousels have working arrows, all buttons tappable.
- Send screenshots of every page at 375px and 768px, and one at 1440px to prove desktop is unchanged.
- List anything you couldn't fix, and why.

## Process
- Make a backup before changing anything.
- Work page by page or in small batches, and wait for approval between batches.

---

*Adjust the breakpoints or carousel rules if a project's design calls for it. The rest applies to almost any site.*
# WordPress site kit (Elementor + Local + WP Engine)

A reusable starting point for building a client's WordPress site with Claude Code, based on a live
client build (Oct 2026). It holds the instructions, process and checklists. You supply each site's design and content.

## What's in the kit

| File | What it's for |
|---|---|
| `CLAUDE.md` | Instructions Claude reads first: site rules (fill in), quality rules, standing rules, build order, mobile rule. |
| `BUILD-LOG.md` | Template for the running record of what's built, decisions, custom code and what's left. |
| `LAUNCH-PLAYBOOK.md` | Checklists: before the first push, after every push, reviewer access, payment test, email, licenses, domain move, go-live, SSH work. |
| `LESSONS.md` | Gotchas from earlier builds, with the fix for each. |
| `page-spec.md` | Template: header, footer and every page, section by section, with links. |
| `design-tokens.md` | Template: colors, fonts, type scale, spacing, radii and buttons for Elementor globals. |
| `tools/dns-snapshot.sh` | Saves a domain's public DNS (and flags the mail provider and a strict SPF record) before a move. |
| `tools/email-recipients-check.php` | Lists who every store, customer and form email goes to. Read-only by default. |

## Starting a new site

0. **Design the site in Claude Design** and get the owner's approval on every page. Export the approved design as
   HTML for `reference/`.
1. **Create the site in Local** with HTTPS on. Note the PHP version.
2. **Make a `handoff/` folder** in the Local site folder (next to `app/`) and copy this kit into it.
3. **Add the site's inputs** to `handoff/`:
   - `reference/`: the approved design (HTML file, or screenshots/PDFs), with each page labeled by its URL
   - `page-spec.md` and `design-tokens.md`: fill in the templates
   - `data/`: CSVs for products, locations, FAQs and documents, plus an `image-map.csv` (file → new filename + alt text)
   - `images/`: every image the design uses
   - `mobile-guide/` (optional): phone/tablet previews
4. **Fill in `CLAUDE.md`:** the site name, the plugins in use, and the **Site rules** (pricing, contact details, names
   never to use, colors, what's bought vs quoted, shipping, tax, payments, business details, social links, email
   routing). Delete the optional parts that don't apply.
5. **Copy `BUILD-LOG.md`** in as the empty log.
6. **Open Claude Code in the Local site folder** and start with:
   > Read `handoff/CLAUDE.md` and follow it. Start with step 1 of the build order and check with me before moving to
   > the next step.
7. **Before building pages,** connect Claude to Elementor (WP Admin → Elementor → MCP / AI connection) if you use it.

## Things to have ready (they don't block starting)

- **Licenses:** Elementor Pro and any premium plugins. Check how many sites each license covers (Local, staging and
  live may each need one).
- **Payment gateway:** test keys if the gateway has a sandbox; otherwise plan one $1 live test on the host.
- **Hosting account access:** WP Engine. Remember that sandboxes can't take a custom domain.
- **Domain/DNS access:** registrar and DNS host. Find out who manages the business email (Microsoft 365 / Google).
- **Cloudflare account** for Turnstile, the owner's email for notifications, and the business inbox for Cc.

## Using it on non-WooCommerce or non-WP Engine sites

- **No shop:** delete the WooCommerce, quote, shipping, tax and payment parts from `CLAUDE.md`, and sections 5–6 of
  the playbook.
- **Another host:** the playbook's order still holds (push → post-push fixes → reviewer access → email → licenses →
  domain → go-live). Swap in that host's push tool, cache-clearing and SSH steps.
#!/bin/zsh
# Saves a public snapshot of a domain's DNS before a move (website, email and common Microsoft 365 / Google records).
# usage: ./dns-snapshot.sh example.com [output-file]
# A public lookup can't list every record: also export the full zone file from the DNS host.

D="${1:?usage: dns-snapshot.sh example.com [output-file]}"
OUT="${2:-dns-snapshot-$D-$(date +%Y-%m-%d).txt}"

{
  echo "DNS snapshot for $D, taken $(date '+%b %d, %Y %H:%M') (public lookup)."
  echo "Copy every email record to the new DNS host BEFORE switching nameservers, or company email stops working."
  echo
  for q in "NS $D" "A $D" "AAAA $D" "MX $D" "TXT $D" "CNAME www.$D" "A www.$D" "TXT _dmarc.$D" \
           "CNAME autodiscover.$D" "CNAME selector1._domainkey.$D" "CNAME selector2._domainkey.$D" \
           "TXT google._domainkey.$D" "CNAME lyncdiscover.$D" "CNAME sip.$D" "CNAME enterpriseregistration.$D" \
           "CNAME enterpriseenrollment.$D" "SRV _sip._tls.$D" "SRV _sipfederationtls._tcp.$D" "CAA $D"; do
    set -- ${=q}
    r=$(dig +short "$1" "$2" | tr '\n' ' ')
    printf '%-6s %-44s %s\n' "$1" "$2" "${r:-(none)}"
  done
  echo
  ns=$(dig +short NS "$D" | head -1)
  mx=$(dig +short MX "$D" | head -1)
  echo "DNS host: ${ns:-unknown} | Mail: ${mx:-none}"
  case "$mx" in
    *outlook.com*) echo "Mail provider looks like Microsoft 365." ;;
    *google.com*|*googlemail.com*) echo "Mail provider looks like Google Workspace." ;;
  esac
  spf=$(dig +short TXT "$D" | grep -i 'v=spf1')
  [[ "$spf" == *"-all"* ]] && echo "SPF ends in -all: the website must send through the listed providers or a service added to SPF/DNS."
} > "$OUT"

cat "$OUT"
echo
echo "Saved to $OUT"
<?php
/**
 * Shows who every site email goes to: WooCommerce/plugin store emails (To and Cc), customer emails, Elementor form
 * notifications (classic and V4 forms) and the WordPress admin email.
 *
 * Safe by default: it only reads settings and headers, and sends nothing.
 *   wp eval-file email-recipients-check.php --user=1
 * Optional, on Local only: TRIGGER=1 also runs the order emails against the newest order, stopping every message at
 * pre_wp_mail so nothing is actually sent.
 *   TRIGGER=1 wp eval-file email-recipients-check.php --user=1
 */

$cc_of = function ( $headers ) {
	$h = is_array( $headers ) ? implode( "\n", $headers ) : (string) $headers;
	preg_match_all( '/^Cc:\s*(.+)$/mi', $h, $m );
	return $m[1] ? implode( ', ', array_map( 'trim', $m[1] ) ) : '(none)';
};

echo 'Site: ', home_url(), "\n";
echo 'WordPress admin email: ', get_option( 'admin_email' ), "\n";

if ( function_exists( 'WC' ) ) {
	echo 'WooCommerce "From": ', get_option( 'woocommerce_email_from_name' ), ' <', get_option( 'woocommerce_email_from_address' ), ">\n";
	echo "\nStore and plugin emails (headers read only):\n";
	foreach ( WC()->mailer()->get_emails() as $cls => $e ) {
		if ( ! $e->is_enabled() ) { continue; }
		if ( $e->is_customer_email() ) {
			printf( "  %-52s To: the customer\n", $e->get_title() );
			continue;
		}
		$headers = @$e->get_headers(); // Some plugin emails expect an object; the Cc line is still read correctly.
		printf( "  %-52s To: %s | Cc: %s\n", $e->get_title() . " [{$e->id}]", $e->get_recipient(), $cc_of( $headers ) );
	}

	if ( getenv( 'TRIGGER' ) ) {
		$caught = array();
		add_filter( 'pre_wp_mail', function ( $return, $atts ) use ( &$caught, $cc_of ) {
			$caught[] = 'To: ' . ( is_array( $atts['to'] ) ? implode( ', ', $atts['to'] ) : $atts['to'] ) . ' | Cc: ' . $cc_of( $atts['headers'] ) . ' | "' . $atts['subject'] . '"';
			return true; // Stop: nothing is sent.
		}, 10, 2 );
		$ids = wc_get_orders( array( 'limit' => 1, 'orderby' => 'date', 'order' => 'DESC', 'return' => 'ids' ) );
		if ( $ids ) {
			echo "\nOrder emails run against order #{$ids[0]} (intercepted, nothing sent):\n";
			$mailer = WC()->mailer()->get_emails();
			foreach ( array( 'WC_Email_New_Order', 'WC_Email_Cancelled_Order', 'WC_Email_Failed_Order', 'WC_Email_Customer_Processing_Order' ) as $cls ) {
				$caught = array();
				$mailer[ $cls ]->trigger( $ids[0] );
				printf( "  %-36s %s\n", $mailer[ $cls ]->get_title(), $caught ? $caught[0] : 'not sent (disabled)' );
			}
		}
	}
}

global $wpdb;
echo "\nElementor form notifications:\n";
$rows = $wpdb->get_results( "SELECT p.ID, p.post_title, pm.meta_value FROM {$wpdb->postmeta} pm JOIN {$wpdb->posts} p ON p.ID = pm.post_id WHERE pm.meta_key = '_elementor_data' AND p.post_type <> 'revision' AND p.post_status <> 'trash' AND ( pm.meta_value LIKE '%\"elType\":\"e-form\"%' OR pm.meta_value LIKE '%\"widgetType\":\"form\"%' )" );
foreach ( $rows as $r ) {
	$walk = function ( $els ) use ( &$walk, $r ) {
		foreach ( $els as $el ) {
			if ( 'e-form' === ( $el['elType'] ?? '' ) && isset( $el['settings']['email']['value'] ) ) {
				$em = $el['settings']['email']['value'];
				$to = isset( $em['to']['value'] ) ? implode( ', ', wp_list_pluck( (array) $em['to']['value'], 'value' ) ) : '(default)';
				printf( "  #%d %-28s V4 form \"%s\": To: %s | Cc: %s | Bcc: %s\n", $r->ID, $r->post_title, $el['settings']['form-name']['value'] ?? '', $to, $em['cc']['value'] ?? '(none)', $em['bcc']['value'] ?? '(none)' );
			} elseif ( 'form' === ( $el['widgetType'] ?? '' ) ) {
				$s = $el['settings'];
				printf( "  #%d %-28s form \"%s\": To: %s | Cc: %s | Bcc: %s\n", $r->ID, $r->post_title, $s['form_name'] ?? '', $s['email_to'] ?? '(admin email)', $s['email_to_cc'] ?? '(none)', $s['email_to_bcc'] ?? '(none)' );
			}
			if ( ! empty( $el['elements'] ) ) { $walk( $el['elements'] ); }
		}
	};
	$walk( json_decode( $r->meta_value, true ) ?: array() );
}
if ( ! $rows ) { echo "  (no Elementor forms found)\n"; }

$mx = function_exists( 'dns_get_record' ) ? @dns_get_record( wp_parse_url( home_url(), PHP_URL_HOST ), DNS_MX ) : array();
if ( $mx ) { echo "\nThis host name's MX: ", implode( ', ', wp_list_pluck( $mx, 'target' ) ), "\n"; }

Tooling glossary

Elementor

The page builder. Stores layout as JSON in _elementor_data postmeta and auto-generates per-page CSS from it.

The active theme

Supplies the base HTML skeleton and its own compiled CSS — often carrying global defaults that quietly compete with custom styles.

Local

Desktop app that runs WordPress on your computer. The source of truth for the build; pushes go from here to the host.

WP Engine

Managed WordPress host the launch playbook is written for. Sandboxes can’t take a custom domain; its page cache holds pages about 10 minutes.

Hello Elementor

Elementor’s near-empty theme, so the kit’s global styles aren’t fighting a theme’s CSS.

WooCommerce Coming soon

Hides the shop from the public while logged-in reviewers see everything. Its share link doesn’t survive a page cache.

Cloudflare Turnstile

Spam check on forms, login and checkout. Every hostname (Local, sandbox, live) must be allowed.

FluentSMTP

Connects site email to Microsoft 365, Google Workspace or a sending service, so store and form emails actually arrive.

Must-use plugin

A file in mu-plugins/ that always loads. The kit keeps every piece of custom code in one, numbered and explained.

WP-CLI

WordPress from the command line, used over SSH for host-only changes. Pass --user=1 or Elementor won’t save.

Rank Math

SEO plugin: titles, descriptions, sitemap and share images. Needs full image URLs.

Elementor MCP

Elementor’s official connector (4.3+) that lets Claude Code, Codex or Cursor build and edit pages as native, editable Elementor structure. Free to connect; saves everything as a draft.

Atomic editor (V4)

Elementor’s newer element system built on classes and variables. The MCP works with atomic structures, so it must be enabled.

Angie

Elementor’s AI agent plugin. Alongside the MCP it handles WordPress-level work: media library, WooCommerce, ACF, PHP and code snippets.

WordPress Abilities API

How plugins expose actions to AI tools. With Angie installed, the MCP can reach every registered ability, including WooCommerce’s.

ACF (Advanced Custom Fields)

Adds structured custom fields to posts and products. The free version has no repeater fields, so lists need a workaround.

CLAUDE.md

A rules file in the project folder that every new Claude session reads first, so project rules survive between sessions.