01
Context
A WooCommerce store on Divi selling magnetic therapy devices that are listed as regulated medical devices, with more than 100 published products and well over a thousand orders. Keap is the CRM of record, connected through the InfusedWoo plugin, and it also carries invoicing, referral partners and practitioner nurture. The store sold SKU codes rather than solutions: no guided entry, no "help me choose" path, and trust signals parked in the footer. The goal was a guided, outcome-led store that stays on WooCommerce, keeps Keap authoritative and leaves room for a recommendation quiz later.
The engagement runs in fixed milestones: an audit plus a merchandising foundation, then Sets, trust and product cards, then product pages, Keap fixes and the customer journey, with an optional guided quiz. The client had no staging site and edited production directly, so every build ran on an isolated clone or a staging copy first. Product copy goes through a regulatory reviewer, which rules out benefit or outcome language anywhere on the site.
02
The Challenge
- An order-count gap between the HPOS and legacy order tables framed the project as a data-loss risk before anyone had checked it.
- Keap is retiring its XML-RPC API on 31 December 2026, and the connector was mid-migration to REST, so it was unclear which sync paths would stop working.
- Set contents lived as free text in the short description, with inconsistent formats and several links that resolved to a duplicate product or redirected to themselves.
- The theme has no child theme, the site's own custom CSS already used the obvious class names, and two people were editing live product data during the build.
- Every title, badge and trust line had to survive regulatory review: device, dimensions and SKU only, no therapeutic claims.
03
Scope and Approach
Audit on an isolated clone
Five areas audited: WordPress and WooCommerce health with a full plugin inventory, catalogue architecture, the Keap integration in code and data, keep versus replace, and conversion UX. The clone ran with outbound HTTP and mail blocked, and live Keap was read through its UI only. Each finding cites a query, a file and line, a log entry or a screen, and anything unseen is labelled to confirm.
Merchandising foundation
Three custom taxonomies (pain area as multi-select, format, depth) with 14 terms, classifying more than 90 products on staging. Product titles moved to the client's own compliance naming, with factual detail held in a dedicated subtitle field. Every write went through the WooCommerce REST API from scripts with apply, verify and revert modes and a JSON snapshot taken first.
Set cards as a plugin
A self-contained plugin renders each Set as a landscape card: badge, title, subtitle, an Included and Bonus split, bonus value, a fixed purchase column and a trust row. Sets sit in three ordered groups set from a Merchandising tab, saved behind a nonce and a per-product edit capability. The same card appears on the category, the Products page and search results.
Fitting into the live theme
The class prefix was renamed after the site's custom CSS turned out to own it, including rules on the card class itself. Sets sort last in related products, which needed three hooks because WooCommerce shuffles after its own filter. Cards stack below 768 pixels with images contained, not cropped, and were checked at desktop, tablet and mobile widths with no sideways scrolling.
Live-site support and scope ledgers
A Widgets screen that refused to save was traced to its real cause. A practitioner discount was configured with a role-only condition, auto-apply and core product exclusions on the coupon licence already installed. For the remaining milestones, a scope ledger classes every item as literal contract text, plan addition or out of scope, with the quote it came from.
04
Architecture flow
05
Technical Deep Dive
The JSON error that never reached PHP
The block Widgets editor saves through the REST batch endpoint. That one path returned HTTP 418 for GET, POST and OPTIONS alike, with no X-Powered-By header, so PHP never ran; the same route requested through the query-string form returned 200. A firewall rule was matching the literal URL. One filter switches the screen to the classic editor, which saves another way. On staging all 12 sidebars and 24 widgets stayed intact.
Which Keap calls stop on 31 December
The connector routes each service through REST when a REST branch exists. Its contact wrappers are complete, but several modules call the low-level datastore update for contacts directly, and that path has no REST branch. Classic checkout is cushioned because it updates the contact over REST first; address edits, admin re-syncs and express checkouts are not. The same audit found orders locked out of every future sync.
Links that cannot move a bonus item
Contents items became links again without reintroducing a bug where bonus items drifted into the Included list. The parser now returns plain text and HTML as parallel arrays at the same index, and classification runs on the text only. Markup renders through an allow-list of one tag, the anchor, checked by 20 assertions against WordPress's own kses: script, event handlers and javascript URLs are all neutralised.
Collapsible groups inside a product loop
A product list only accepts list items, so each group heading is a list item holding a native details element. A generated :has() rule hides the member cards of a closed group. Cards stay in the DOM, the first group opens by default, counts sit in the heading, and the whole mechanism works with JavaScript switched off.
06
Engineering Decisions
A plugin, not theme edits
With no child theme, any change to the theme directory is wiped on the next update. The cards live in one hook-based plugin that takes over the loop only for Sets and restores exactly the callbacks it removed. It declares HPOS compatibility, touches no prices, cart, checkout or Keap, and deactivating it returns every page to its previous state.
Keep Keap, fix the connector
Keap here is CRM, invoicing ledger, referral system and practitioner nurture at once, so replacing it means rebuilding several tools, not swapping one. The recommendation is to keep it, fix the integration, and get the vendor's written commitment to finish the REST migration, with the conditions that would flip the decision written down in advance.
Regulated copy stays factual
Titles carry device, dimensions and SKU only; factual detail goes to the subtitle field. An earlier design draft supplied the card's structure and rhythm, but none of its outcome copy. The trust row reads from a filterable list of regulatory entries, so wording changed after regulatory review without touching the template.
07
Results
- A five-area audit with every finding traced to source: the HPOS order gap fully explained by refund records, a recurring cart-page fatal traced to the Keap connector, and the XML-RPC exposure narrowed to named sync paths.
- On staging, three custom taxonomies with 14 terms classify more than 90 products, and titles and subtitles follow the client's compliance naming, every write snapshotted and revertable.
- On staging, every Set renders through one card plugin in three groups, with every contents item linked; 171 fix assertions and 20 sanitisation assertions pass.
- The Widgets save failure traced to a host firewall rule rather than WordPress, with a one-line fix verified on staging.
- A role-restricted practitioner discount, auto-applied at cart with product exclusions, live on production.
08
Planned next
- Production rollout of the Set cards plugin once signed off, with staging refreshed from production first.
- A structured component record per Set replacing parsed free text, with parser fallback and colour-coded size markers, as a proposed add-on.
- Product page polish, the Keap fixes from the audit (cart fatal patch, stuck syncs, a sync-failure alert), an end-to-end journey test with conversion tracking, and a country-aware data model.
- An optional guided pain-area quiz that captures every participant into Keap, tagged by pain pattern.