wc-pdp-correctness
WooCommerce product-detail-page and add-to-cart correctness for the skyyrose-flagship theme — AJAX contract, variation forms, stock states, cart fragments. Use when building, changing, or reviewing si
SKILL.md.claude/skills/wc-pdp-correctness/SKILL.md
GitHubWooCommerce PDP Correctness
Core-contribution style guides don't cover this. This checklist is for THEME add-to-cart surfaces — where a broken button means zero revenue and no error log. Nothing fatals; the button just does nothing.
When to use
Fires on an observable event:
- You edited or are reviewing anything under
wordpress-theme/skyyrose-flagship/woocommerce/(single-product.php,content-product.php,archive-product.php,cart/,checkout/). - You edited a product card that renders an add-to-cart control —
e.g.
template-parts/product-card-v7-lookbook.php. - You changed cart-related enqueues in
inc/enqueue.php, or any JS that listens foradded_to_cart/wc_fragments_refreshed. - A report arrives that "add to cart does nothing", the cart badge is stale, or a variation picker is dead.
Not for: WooCommerce plugin/core PHP (use woocommerce-backend-dev), webhook receivers
(woocommerce-webhooks), bulk product writes over REST (wordpress-woocommerce-automation), or
pure CSS/visual polish (theme-min-build + design-qc).
Inputs
Required before you start. If any is absent, stop and get it — do not proceed on assumption:
| Input | How to confirm | If absent |
|---|---|---|
| Theme working tree | ls wordpress-theme/skyyrose-flagship/woocommerce/ |
STOP — you are in the wrong checkout |
| Theme composer tooling | ls wordpress-theme/skyyrose-flagship/vendor/bin/phpcs |
STOP — run composer install in the theme dir; do not skip phpcs |
| The specific template(s) you changed | git diff --stat -- wordpress-theme/skyyrose-flagship |
STOP — an unscoped "PDP review" cannot be verified |
| A product to test against | catalog CSV data/skyyrose-catalog.csv (SOT) |
STOP — never invent a SKU or product id |
| Browser access, for the interactive battery | Playwright / Chrome DevTools available | Do NOT mark those steps passed. They become a SKIP with a named owner (the caller runs them) |
Kids Capsule renders 0 cards by design (launch mode). That is not an input failure and is not a bug to fix.
Procedure
- Read the current template before editing it. Quote
file:linefor anything you claim about it. - Simple-product AJAX contract. The button needs BOTH the class
ajax_add_to_cartANDdata-product_id="<id>"—wc-add-to-cart.jsbinds on the class and reads the data attribute. Missing either is a silent no-op. Also required:data-quantity,rel="nofollow"on anchors, and anaria-labelnaming the product. - Never emit a bare
?add-to-cart=GET href as the primary control. Crawlers follow it and edge caches poison it. The href stays the permalink; the data attributes drive the add. - Enqueues. Depend on the registered
wc-add-to-carthandle rather than re-implementing it —WC_Frontend_Scripts::register_scripts()registers it andwc_add_to_cart_paramsis auto-localized bylocalize_printed_scripts(). - Fragments. Custom mini-cart or count badges must listen for
added_to_cartANDwc_fragments_refreshed, or counts go stale after the first add. - Variable products. They cannot use the simple-product AJAX path: the form posts
variation_idplus every attribute field. A themed quick-add must open the variation form, or pass ALL required attributes — a partial attribute set produces a WC error notice, not an add. Keepdata-product_variationsJSON and thewoocommerce_variation_add_to_carthooks intact when restyling; stripping them kills the picker silently. - Out-of-stock variations. The form still renders; the button must disable on
found_variation/hide_variation. Test one OOS combination explicitly. - Purchasability and quantity. Respect
$product->is_purchasable()andis_in_stock(); pre-order and launch-mode products are non-purchasable on purpose. Take min/max/step fromwoocommerce_quantity_input_args— a custom stepper that ignoresmaxoversells. - Button states. Style all of
default → .loading → .added → View cartlink. WooCommerce toggles those classes; if custom CSS styles only the default state, the button looks dead for the whole round-trip. - Run the Verification block below. Any CSS/JS you touched also needs
npm run buildinwordpress-theme/— production serves.min, so a source-only edit ships nothing. - Record anything surprising in
.wolf/buglog.jsonwith a fresh id frompython scripts/wolf_bug_id.py.
Verification
Run every CLI check. Each states its command, its pass condition, and the evidence tag it earns.
cd /Users/theceo/DevSkyy/.claude/worktrees/glimmering-crafting-shannon/wordpress-theme/skyyrose-flagship
php -l woocommerce/single-product.php ### PASS: "No syntax errors detected" [repro]
vendor/bin/phpcs --standard=.phpcs.xml woocommerce/single-product.php ### PASS: exit 0, "1 / 1 (100%)", 0 errors [repro]
grep -n 'ajax_add_to_cart' template-parts/product-card-v7-lookbook.php | grep -c 'data-product_id' ### PASS: prints 1 (N == number of add buttons) [repo]
cd .. && npm run verify:theme -- --only wc-support ### PASS: aspect OK, exit 0 [test]
npm run build && npm run verify:theme -- --only min-sync ### PASS: no stale .min, exit 0 [test]
curl -sI "https://skyyrose.co/?cb=$(date +%s)" | head -1 ### PASS: HTTP/2 200 [live]
What each one buys you: php -l — a fatal in a PDP template takes the whole page down.
phpcs — catches unescaped output on data attributes. The grep pair — the add-to-cart contract
asserted on the markup itself; class and data-product_id must sit on the SAME element.
wc-support — gallery zoom/lightbox/slider plus declared theme support. min-sync — shipped bytes
match source, because production serves .min. The curl — cache-busted, because Batcache serves
stale and WebFetch strips <script>.
Browser battery — these cannot run headless from this skill. A SKIP is not a PASS: report each as SKIP and name the caller as owner, or run them under Playwright/Chrome DevTools yourself:
- Simple product: click add → network shows
?wc-ajax=add_to_cart200 → cart count increments with no page reload.[live] - Variable product: choose a full attribute set → add → cart line shows the correct variation meta.
- An OOS variation combination → button disabled, no add possible.
- Quantity > 1 is respected on the cart line.
- Logged-out clean incognito session — the first-ever add works (nonce/session edge).
- Fragments refresh: badge updates on add AND after browser-back.
Prove the check can fail before you trust it (rule 3): delete data-product_id from the button,
re-run check 3, confirm it prints 0, then restore. A gate you have never seen go red is a guess
with a citation. A gate that dies is not a gate that passed (rule 1) — if verify:theme errors
or times out, its silence is an artifact; re-run it by hand.
Attribution (rule 4): before claiming a phpcs/phpstan finding is caused by your change, run the
same gate on a pristine tree —
git archive HEAD wordpress-theme/skyyrose-flagship | tar -x -C "$SCRATCH" — and diff the check's
contents, not its pass/fail state. Never git stash; the stash stack is shared across worktrees.
Worked example
Reviewing the lookbook card's quick-add control, 2026-07-29, in this worktree.
$ sed -n '148,153p' wordpress-theme/skyyrose-flagship/template-parts/product-card-v7-lookbook.php
printf(
'<a href="%s" data-quantity="1" data-product_id="%d" data-magnetic class="v7card__quickadd button add_to_cart_button ajax_add_to_cart" rel="nofollow">%s</a>',
esc_url( $v7_permalink ),
absint( $v7_product->get_id() ),
esc_html__( 'Quick Add', 'skyyrose' )
);
Contract satisfied on one element: class ajax_add_to_cart, data-product_id (via absint()),
data-quantity="1", rel="nofollow", esc_url() on the href. The comment immediately above at
:140-142 states the rule this template follows — the href is the permalink, never a
?add-to-cart= GET, because crawlers would fire cart-adds and poison the cache. [repo]
Gates run the same session:
$ php -l woocommerce/single-product.php
No syntax errors detected in woocommerce/single-product.php
$ vendor/bin/phpcs --standard=.phpcs.xml woocommerce/single-product.php
. 1 / 1 (100%)
Time: 679ms; Memory: 14MB
$ echo $?
0
$ curl -sI "https://skyyrose.co/?cb=$(date +%s)" | head -1
HTTP/2 200
[repro] for the two local gates, [live] for the storefront probe. The live theme asset version
observed the same session was ver=1.12.9, matching functions.php:21
(define( 'SKYYROSE_VERSION', '1.12.9' );) — the deployed build is current, not stale.
Failure modes
| Symptom | Cause | Evidence needed |
|---|---|---|
| Button click navigates instead of adding | class present, data-product_id missing (or vice-versa) |
grep check 3 above [repo], then a network trace [live] |
| Cart badge stale after first add | custom JS listens for added_to_cart only, not wc_fragments_refreshed |
console + network on a real add [live] |
| Variation picker dead, no console error | template stripped data-product_variations or the woocommerce_variation_add_to_cart hooks |
diff the template against git archive HEAD [repo] |
| "Add" succeeds but wrong/no variation meta | partial attribute set submitted | inspect the POST body [live] |
| Oversell past stock | custom stepper ignores max from woocommerce_quantity_input_args |
order record vs stock [live] |
| Add-to-cart 403 in production | plugin interference corrupting CommerceKit nonces — fixed by the MU-plugin option_active_plugins guard. Check plugin interference before blaming the theme |
prod network trace [live] — bug-096-adjacent; the recorded incident is the homepage plugin guard, PR #596 |
| CSS change looks right locally, unchanged live | .min not rebuilt; production serves .min |
verify:theme --only min-sync [test] |
| Kids Capsule shows 0 cards | launch mode, by design | do not "fix" it |
REST probe returns 401 on /wp-json/ |
WP.com Atomic — use ?rest_route= form |
[live] |
Two standing traps, both recorded as recurring:
- Fail-open gates (bug-230, ×6). A verification step whose input is missing must block, never
pass. If
verify:themecannot find the theme, that is a FAIL, not a skip. - Scope-jumping severity (bug-287, ×2). A finding read from the working tree is
[repo]. Calling it a "production bug" requires a[live]probe of skyyrose.co in the same session. State scope before severity.