Appearance
Product page support
Diagnostic and troubleshooting reference for the product detail page (PDP) — gallery + zoom, variant picker, sticky add-to-cart, product recommendations, back-in-stock signup, selling-plan subscriptions, combined-listing parent-child, breadcrumb JSON-LD, and per-preset template ordering.
FAQ
The gallery zoom is not working on desktop.
Hover-zoom on desktop requires a viewport width of ≥ 1024 px and a pointing device that supports hover (mouse, trackpad). If you are on a touch-first device (iPad Pro, Surface Go with keyboard-only), the theme detects pointer: coarse and swaps hover-zoom for click-zoom (full-screen viewer). This is intentional — hover-zoom on touch triggers during scrolling and is bad UX.
To verify the expected mode, run the diagnostic snippet below and inspect gallery.zoomMode. Expected values:
hover— ≥ 1024 px desktop with hover-capable pointerclick— < 1024 px or touch-first devicedisabled—prefers-reduced-motion: reduceis set by OS, or a merchant-added CSS override disabled zoom
If your zoomMode is hover but zoom is not firing, confirm product-gallery custom element is present and registered: document.querySelector('product-gallery').constructor.name should return ProductGallery.
Pinch-to-zoom does not work inside the full-screen viewer on mobile.
Pinch-to-zoom is implemented as two-finger gesture detection inside the <gallery-viewer> overlay. Requirements:
- Touch device with
pointer: coarse(phones, tablets) - The viewer is open (tap an image to open it)
- No competing gesture listeners — some PWA shell wrappers or custom theme additions bind their own
touchstarthandlers that preventDefault early, swallowing the gesture
Verify via the diagnostic snippet — gallery.viewer.present === true should be true when the viewer is open. If pinch still fails, try disabling browser extensions and test in an incognito window.
Gallery deep-links (URL with #media-<n>) do not open the Nth image.
The deep-link handler reads location.hash on page load, parses media-<n> where n is 1-based, and calls gallery.selectMedia(n). If it's not working:
- Confirm the hash is exactly
#media-<n>(no typo, correct casing). - Confirm the product has at least
nmedia items — if you deep-link to#media-5but the product has 3 images, the handler clamps to the last image and logs a warning to the console. - Confirm the page loaded the product template — deep-links do not work on non-product templates.
The variant picker does not show the combined-listing chip.
The combined-listing chip only renders when:
- The current product has a
combined_listing.parentrelationship set (Shopify Admin → Products → Organization → Combined listings) - The
combined_listingblock is added to the product section (Theme editor → Product section → Add block → Combined listing) - At least one sibling product exists in the combined-listing group (the chip is hidden if the current product is the only child)
Run the diagnostic snippet — inspect variantPicker.combinedListing. Expected:
json
{
"parentHandle": "commercial-fit-out",
"siblingCount": 2,
"chipVisible": true
}If parentHandle is null, the product is not part of a combined listing. Configure via Shopify Admin or run the demo-data script: node scripts/populate-combined-listing-demo.mjs --preset <preset>.
Sticky add-to-cart bar does not appear on scroll.
The sticky bar uses an IntersectionObserver on the in-page buy-button. When the buy-button scrolls out of viewport, the sticky bar is shown; when it scrolls back in, the sticky bar hides (avoiding double-render).
If the sticky bar never shows:
- Confirm the
sticky-add-to-cartcustom element is mounted:document.querySelector('sticky-add-to-cart')should return the element (notnull). - Confirm the page is long enough to scroll — on very short product descriptions (no additional blocks below buy-button), the sticky bar never triggers because the buy-button stays visible.
- Confirm no custom CSS has
overflow: hiddenon a parent element that would breakIntersectionObserverviewport detection. - Confirm
sticky-add-to-cart.cssis loaded — check the Network tab; the asset should return 200.
Some merchants hide the sticky bar intentionally for minimalist PDPs. Custom CSS override:
css
sticky-add-to-cart {
display: none !important;
}The back-in-stock form is not submitting to Klaviyo / my CRM.
Submissions only forward to a merchant webhook when back_in_stock_webhook_url is populated on the block. Verify:
- Theme editor → Product section → Back-in-stock block → Back-in-stock webhook URL — confirm the URL is pasted.
- The URL is reachable — try a
curl -X POST <your-url>with a sample payload. If you get 401/403/404, the CRM endpoint is rejecting — check their docs for the expected headers and format. - Open DevTools → Network tab on a sold-out PDP, submit the form, and confirm a
POSTto your webhook URL appears with 200 response. If you seePOST /contactinstead, the webhook URL field is empty or malformed.
The theme posts application/x-www-form-urlencoded with these fields:
| Field | Description |
|---|---|
email | Submitter email address |
product_id | Shopify product numeric ID |
variant_id | Shopify variant numeric ID (the specific sold-out variant) |
handle | Product handle |
locale | Current storefront locale code |
restock_date | ISO date from the custom.restock_date metafield if populated; empty otherwise |
If your CRM expects JSON, run a proxy at Cloudflare Workers / Vercel that transforms the form POST to JSON and forwards. The theme intentionally submits form-encoded for compatibility with Shopify's native contact form handler fallback.
The save badge is not showing on my subscription plan.
The badge renders only when:
show_save_badgeis enabled on the selling-plan block (default: on)- The selling plan has a non-zero discount configured in Shopify Admin → Subscriptions
If your plan is priced equal to one-time purchase (0% save), the badge intentionally does not render — a "Save 0%" badge would be misleading UX.
Verify via diagnostic snippet — inspect sellingPlan.plans[].savePercent. Expected: non-zero for discounted plans, null or 0 for non-discounted.
Recommendations strip is showing sold-out products.
The exclude_sold_out setting filters client-side. Verify:
- Theme editor → Product recommendations section → Hide sold-out products — confirm enabled.
- Run the diagnostic snippet — inspect
recommendations.cards[].available. All cards should betruewhenexclude_sold_outis on. - If the setting is on but sold-out products still appear, push the latest theme — the client-side filter logic lives in
assets/product-recommendations.jswhich must be at the current commit.
Check the deployed theme asset version via: curl -s <your-store>/products/<any-product>.js | head — confirm the HTML references product-recommendations.js with a current timestamp parameter.
The OOS cross-sell strip does not render on a sold-out PDP.
The cross-sell replaces the buy-button area only when every variant is sold out. If one variant is still available, the buy-button stays and the cross-sell is not triggered (the user can still add-to-cart via the available variant).
If every variant is sold out but the strip still does not render:
- Confirm
show_oos_recommendationsis enabled on the product section (default: on). - Confirm
product-recommendations.jsis loaded — check the Network tab. - Confirm the recommendations API endpoint returns at least one related product:
curl -s <your-store>/recommendations/products.json?product_id=<id>&limit=4should return a non-emptyproductsarray.
If the recommendations API returns empty (Shopify platform requires the store to have at least ~30 days of traffic data for the algorithm to train), the strip does not render and a fallback back-in-stock form is shown. This is intentional — empty cross-sell cards are worse than a signup form.
Restock-date banner shows but the countdown is wrong.
The day-count is computed server-side in Liquid from product.metafields.custom.restock_date vs 'now' | date: '%s'. Both are UTC. If the countdown is off by one day in your timezone, that is correct server behavior — the display localizes the date string, not the day-count math.
If the countdown is wildly wrong (e.g., negative, or year-off):
- Confirm the metafield value is a valid ISO date (YYYY-MM-DD or YYYY-MM-DDTHH:MM:SSZ).
- Confirm the metafield namespace.key matches the
oos_restock_date_metafieldsection setting (defaultcustom.restock_date). - The theme falls back to the variant-level metafield if product-level is empty. If both are empty, no banner renders.
Breadcrumbs do not emit JSON-LD.
The breadcrumb JSON-LD emits inline in snippets/breadcrumb.liquid when rendered. Verify:
bash
curl -s <your-store>/products/<any-product> | grep -A 2 'BreadcrumbList'Expected: one JSON-LD script block containing "@type": "BreadcrumbList" with itemListElement entries using absolute URLs (starts with https://).
If the block is missing, the breadcrumb snippet is not rendered. The product.<preset>.json templates include the breadcrumb by default — check you did not accidentally remove it via theme editor.
The gallery shows the wrong image after swatch hover.
Swatch hover updates the gallery image to match the hovered variant's featured_image. If the variant does not have a featured_image assigned in Shopify Admin, the gallery does not update (hover is a no-op). To fix:
- Shopify Admin → Products →
<product>→ Media → drag each image onto the matching variant. - The theme reads
product.media_index_by_variant_idto map variants to images. Admin-driven, no theme-editor configuration.
The PDP is slow to load on mobile.
Mobile Lighthouse performance below 0.6 on PDPs is typically caused by large product images, video heroes, or embedded iframes. Common causes:
- Unoptimized product images (>1MB each) — Shopify auto-resizes for responsive loading but source file size matters for the first request
- Video-as-hero products — consider an image placeholder with a play-button overlay
- Large descriptions with embedded YouTube/Vimeo — lazy-load via native
loading="lazy"on the iframe wrapper
Run Lighthouse per-page to identify the blocker — .lighthouserc.cjs config ships with the theme and targets mobile + desktop.
Decision tree: "Why is X not working?"
text
Symptom: gallery zoom not firing on desktop
-> Is viewport >= 1024 px? (DevTools → width indicator)
-> no: hover-zoom disabled by design; click-zoom should work
-> yes: continue
-> Is pointer hover-capable? (console: matchMedia('(hover: hover)').matches)
-> false: touch-first device; click-zoom should work
-> true: continue
-> Is prefers-reduced-motion set? (console: matchMedia('(prefers-reduced-motion: reduce)').matches)
-> true: zoom disabled by a11y preference; respect it
-> false: confirm <product-gallery> mounted
-> null: asset failed to load; check network tab for product-gallery.js
-> present: check for CSS override `pointer-events: none` on gallery
Symptom: sticky ATC bar never appears on scroll
-> Is <sticky-add-to-cart> in DOM? (console: document.querySelector('sticky-add-to-cart'))
-> null: section removed or asset failed; re-add section + check Network
-> present: continue
-> Is the product page long enough to scroll past the buy-button?
-> no: sticky bar stays hidden by design
-> yes: continue
-> IntersectionObserver support? (console: 'IntersectionObserver' in window)
-> false: very old browser; polyfill not shipped in 06.4 (browser-matrix is modern evergreens)
-> true: check for custom CSS `position: fixed` overrides on parent elements
Symptom: back-in-stock submission does not reach Klaviyo/CRM
-> Is webhook URL set? (Theme editor → block setting)
-> empty: submits to Shopify admin inbox; add URL to forward to CRM
-> populated: continue
-> Does `curl -X POST <url>` with sample payload return 2xx?
-> 4xx/5xx: CRM endpoint rejecting — check docs for expected format
-> 2xx: continue
-> DevTools → Network → submit form; what is the POST target?
-> <your-url>: success; check CRM inbox
-> /contact: webhook URL field is empty or malformed; re-paste
Symptom: combined-listing chip missing
-> Does the product have combined_listing.parent set? (Shopify Admin → Organization)
-> no: configure via Admin or run populate-combined-listing-demo.mjs
-> yes: continue
-> Is the combined-listing block added to the section? (Theme editor)
-> no: add it (Theme editor → Product → Add block → Combined listing)
-> yes: continue
-> Is there at least 1 sibling product in the group? (Shopify Admin)
-> no: chip correctly hidden (the current product is the only child)
-> yes: check Network tab; `combined_listing_json` fetch should return 200
Symptom: recommendations strip showing sold-out products
-> Is `exclude_sold_out` enabled on the section? (Theme editor)
-> off: enable it
-> on: continue
-> Push latest theme — filter logic lives in assets/product-recommendations.js
-> Diagnostic snippet → check `recommendations.cards[].available` (all should be true)
Symptom: OOS cross-sell not rendering on sold-out PDP
-> Is every variant sold out? (some variants still available triggers buy-button, not cross-sell)
-> no: design-correct; fix sold-out status or expect buy-button
-> yes: continue
-> Is `show_oos_recommendations` enabled? (Theme editor → Product section)
-> off: enable it
-> on: continue
-> Does `/recommendations/products.json?product_id=<id>&limit=4` return non-empty?
-> empty: Shopify algorithm needs traffic history; fallback to back-in-stock form only
-> non-empty: check product-recommendations.js is loaded + check console for JS errors
Symptom: breadcrumb JSON-LD missing
-> curl the PDP: grep 'BreadcrumbList' — present?
-> no: breadcrumb snippet not rendered; check template includes it
-> yes: continue
-> Google Rich Results Test (paste HTML) — BreadcrumbList enhancement green?
-> red: URLs relative instead of absolute — push latest theme (fixed in 06.4)
-> green: correctly configured
Symptom: PDP slow on mobile (Lighthouse perf < 0.6)
-> Identify LCP element (DevTools → Performance → LCP flame)
-> Hero image: optimize source size; Shopify auto-resizes but source matters
-> Video: swap to placeholder image with play-button overlay
-> Description block: check for embedded iframes; add loading="lazy"
-> Are fonts preloaded? (head → `<link rel=preload as=font>`)
-> no: enable via theme settings → Performance → enable_lcp_preload
-> yes: continue checkingValidator interpretation
| Validator | Output message | Meaning | Fix |
|---|---|---|---|
| Lighthouse | "CLS > 0.1 on PDP" | Gallery image loading caused layout shift | Check gallery images have width + height attributes; theme emits them but custom edits may strip them |
| Lighthouse | "Perf < 0.6 on mobile PDP" | LCP element > 2.5s mobile | See decision tree "PDP slow on mobile"; check hero image size, fonts preload, video heroes |
| Lighthouse | "TBT > 300 ms mobile" | Total Blocking Time exceeded mobile gate | Shopify platform baseline (4× CPU throttle lab artifact). Not a theme defect. See Phase 06.2 baseline |
| Lighthouse | "BP = 0.78" | Best Practices score capped at 0.78 | Shopify platform cap (portable-wallets.en.js, bugsnag telemetry). Not a theme defect |
| Lighthouse | "Perf < 0.6 on mobile PDP" | LCP element > 2.5s mobile on a product with large media | Re-export hero at ≤ 600 KB; Shopify CDN auto-resizes responsive variants but source size dominates first render |
| axe | "color-contrast on .product__pill--selected" | Variant pill selected state contrast ratio fails WCAG AA | 06.4 fix shipped but cached CSS may lag — clear CDN + re-push theme; if still failing, escalate (contrast math changed) |
| axe | "aria-expanded invalid on pickup-availability" | Drawer aria-expanded value not syncing on open/close | 06.4 fix shipped via capture-phase listener (F38 in audit). Clear CDN + re-push. If still failing, check for custom JS overriding handler |
| GRRT | "Product.offers.itemCondition missing" | product:condition hardcoded "new" in JSON-LD, no merchant setting | Deferred to Phase 7 |
| GRRT | "BreadcrumbList.itemListElement.item must be a URL" | Breadcrumb emitted relative URLs | Fixed in 06.4 (absolute URLs). Push latest theme |
| Meta Sharing Debugger | "URL cannot be scraped" | Dev-store password protection blocks URL-only scrapers | Platform constraint — dev stores cannot disable password. Reachable post-Theme-Store-transfer. Use HTML-paste validators meanwhile |
| LinkedIn Post Inspector | "Unable to retrieve preview" | Same as Meta (dev-store password) | Same as above |
| Shopify Theme Check | "@app block dispatch requires content_for 'blocks'" | PDP sections use legacy {% for block in section.blocks %} instead of content_for 'blocks' | Deferred to Phase 7 |
| Shopify Theme Check | "AssetCount > 60" | Theme file count exceeds D-22 cap | Asset count (~106) exceeds the original 60-file cap; Phase 7 renegotiation in progress |
Diagnostic snippet
Paste into your browser DevTools Console on any product detail page of your live store. Produces a single JSON blob describing current PDP state — safe, read-only, and adds zero theme code. The snippet runs one read-only traversal of the PDP DOM and writes nothing.
javascript
(() => {
const state = {
pageUrl: location.href,
viewport: { w: innerWidth, h: innerHeight },
theme: document.documentElement.getAttribute('data-theme') || 'light',
dir: document.documentElement.getAttribute('dir') || 'ltr',
lang: document.documentElement.getAttribute('lang'),
template: document.body.getAttribute('data-template') || 'unknown',
preferences: {
hover: matchMedia('(hover: hover)').matches,
pointerCoarse: matchMedia('(pointer: coarse)').matches,
reducedMotion: matchMedia('(prefers-reduced-motion: reduce)').matches,
},
gallery: {
present: !!document.querySelector('product-gallery'),
activeIndex: (() => {
const el = document.querySelector('product-gallery [data-active]');
return el ? Number(el.dataset.active) : null;
})(),
mediaCount: document.querySelectorAll('product-gallery [data-media]').length,
zoomMode: (() => {
const el = document.querySelector('product-gallery');
return el ? el.getAttribute('data-zoom-mode') : null;
})(),
deepLink: location.hash.match(/^#media-(\d+)$/)?.[1] || null,
viewer: {
present: !!document.querySelector('gallery-viewer'),
open: !!document.querySelector('gallery-viewer[open]'),
},
},
variantPicker: {
present: !!document.querySelector('variant-picker'),
activeVariantId: (() => {
const el = document.querySelector('variant-picker [data-active-variant]');
return el ? el.dataset.activeVariant : null;
})(),
variantsAvailable: document.querySelectorAll('variant-picker [data-variant]:not([data-sold-out])').length,
variantsSoldOut: document.querySelectorAll('variant-picker [data-variant][data-sold-out]').length,
combinedListing: (() => {
const el = document.querySelector('combined-listing-chip');
if (!el) return { parentHandle: null, siblingCount: 0, chipVisible: false };
return {
parentHandle: el.getAttribute('data-parent-handle'),
siblingCount: Number(el.getAttribute('data-sibling-count') || 0),
chipVisible: getComputedStyle(el).display !== 'none',
};
})(),
},
buyButton: {
present: !!document.querySelector('[data-product-form]'),
enabled: (() => {
const btn = document.querySelector('[data-product-form] button[type="submit"]');
return btn ? !btn.disabled : null;
})(),
unitPriceVisible: !!document.querySelector('[data-unit-price]'),
},
stickyAtc: {
present: !!document.querySelector('sticky-add-to-cart'),
visible: (() => {
const el = document.querySelector('sticky-add-to-cart');
return el ? getComputedStyle(el).display !== 'none' && getComputedStyle(el).visibility !== 'hidden' : false;
})(),
observing: !!document.querySelector('sticky-add-to-cart[data-observing]'),
},
backInStock: {
formPresent: !!document.querySelector('.product__back-in-stock-form'),
webhookConfigured: (() => {
const form = document.querySelector('.product__back-in-stock-form');
return form ? !!form.dataset.webhookUrl : false;
})(),
successShown: !!document.querySelector('.product__back-in-stock-form.product__back-in-stock-form--success'),
},
recommendations: {
present: !!document.querySelector('product-recommendations'),
excludeOos: (() => {
const el = document.querySelector('product-recommendations');
return el ? el.dataset.excludeOos === 'true' : null;
})(),
collectionScope: (() => {
const el = document.querySelector('product-recommendations');
return el ? el.dataset.collectionScope || null : null;
})(),
cardsCount: document.querySelectorAll('product-recommendations .product-card').length,
cardsAvailableCount: document.querySelectorAll('product-recommendations .product-card:not([data-sold-out])')
.length,
},
oosCrossSell: {
present: !!document.querySelector('.product__oos-cross-sell'),
restockBanner: !!document.querySelector('.product__restock-banner'),
restockDays: (() => {
const el = document.querySelector('.product__restock-banner [data-restock-days]');
return el ? Number(el.dataset.restockDays) : null;
})(),
},
sellingPlan: {
present: !!document.querySelector('[data-selling-plan-block]'),
planCount: document.querySelectorAll('[data-selling-plan-option]').length,
saveBadgesVisible: document.querySelectorAll('[data-selling-plan-save-badge]:not([hidden])').length,
costPerDeliveryVisible: document.querySelectorAll('[data-selling-plan-cost-per-delivery]:not([hidden])').length,
plans: Array.from(document.querySelectorAll('[data-selling-plan-option]')).map((el) => ({
id: el.dataset.sellingPlanId,
savePercent: Number(el.dataset.savePercent || 0),
costPerDelivery: el.dataset.costPerDelivery || null,
})),
},
breadcrumb: {
present: !!document.querySelector('[data-breadcrumb]'),
jsonLdPresent: !!document.querySelector('script[type="application/ld+json"][data-breadcrumb-jsonld]'),
jsonLdAbsoluteUrls: (() => {
const el = document.querySelector('script[type="application/ld+json"][data-breadcrumb-jsonld]');
if (!el) return null;
try {
const parsed = JSON.parse(el.textContent);
const items = parsed.itemListElement || [];
return items.every((i) => typeof i.item === 'string' && i.item.startsWith('https://'));
} catch (e) {
return false;
}
})(),
},
productJsonLd: {
present: !!document.querySelector('script[type="application/ld+json"][data-product-jsonld]'),
valid: (() => {
const el = document.querySelector('script[type="application/ld+json"][data-product-jsonld]');
if (!el) return null;
try {
const parsed = JSON.parse(el.textContent);
return parsed['@type'] === 'Product' && !!parsed.offers;
} catch (e) {
return false;
}
})(),
},
shopperState: {
available: typeof window.UisceShopperState === 'object',
recentlyViewedCount: (() => {
try {
return JSON.parse(localStorage.getItem('uisce.shopper.recentlyViewed') || '[]').length;
} catch (e) {
return null;
}
})(),
},
};
console.log('Uisce Theme — PDP diagnostic report');
console.log(JSON.stringify(state, null, 2));
return state;
})();What to look for in the output:
gallery.present === trueandgallery.mediaCount >= 1— gallery is mounted with content.gallery.zoomModematches the expected mode for your device (hover / click / disabled).variantPicker.present === true— picker mounted;combinedListing.chipVisibleistrueonly on combined-listing children.buyButton.enabled === trueon an in-stock product,falseon a sold-out product.buyButton.unitPriceVisible === trueon variants withunit_price_measurementset.stickyAtc.present === trueon every PDP;stickyAtc.visibledepends on scroll position.backInStock.formPresent === trueon sold-out variants;backInStock.webhookConfigured === trueonly if merchant pasted a webhook URL.recommendations.present === true+cardsAvailableCount === cardsCountwhenexcludeOos === true.oosCrossSell.present === trueon fully-sold-out PDPs (every variant OOS);oosCrossSell.restockDaysis an integer if the restock-date metafield is populated.sellingPlan.plans[].savePercentis non-zero for discounted plans, zero for non-discounted (badge correctly hidden).breadcrumb.jsonLdAbsoluteUrls === true— breadcrumb itemListElement.item values all start with https://.productJsonLd.valid === true— Product JSON-LD is emitted with a validoffersblock.shopperState.available === true— shopper-state primitive loaded (drives recently-viewed, back-in-stock success memory, etc).
Curl cheat-sheet
Use these one-liners to verify your live store's PDP. Replace YOURSTORE with your domain and <product-handle> with an actual product. If your store is password-protected (dev store), add -H 'Cookie: storefront_digest=<hash>' — see your dev-store password storage in Shopify admin. For live production stores, no cookie is needed.
1. Verify the Product JSON-LD is emitted and valid.
bash
curl -s https://YOURSTORE/products/<product-handle> | grep -A 20 'data-product-jsonld'Expected: a JSON-LD <script> block containing "@type": "Product", "offers": {...}, and "name": "...".
2. Verify the BreadcrumbList JSON-LD uses absolute URLs.
bash
curl -s https://YOURSTORE/products/<product-handle> | grep -oE '"item":"https://[^"]+"'Expected: at least 2 lines, each starting with "item":"https://. Any result showing "item":"/ (relative) indicates a stale deployment.
3. Verify hreflang alternates in the head (regression check from Phase 06.2).
bash
curl -s https://YOURSTORE/products/<product-handle> | grep -c '<link rel=.alternate. hreflang='Expected: 1 or more if you have published locales.
4. Verify product-gallery custom element is present.
bash
curl -s https://YOURSTORE/products/<product-handle> | grep -c '<product-gallery'Expected: 1.
5. Verify variant-picker custom element is present and has variants.
bash
curl -s https://YOURSTORE/products/<product-handle> | grep -oE '<variant-picker[^>]*>' | head -1Expected: one line containing <variant-picker.
6. Verify sticky-add-to-cart is mounted.
bash
curl -s https://YOURSTORE/products/<product-handle> | grep -c '<sticky-add-to-cart'Expected: 1.
7. Verify product-recommendations.js is loaded with defer.
bash
curl -s https://YOURSTORE/products/<product-handle> | grep -oE '<script[^>]*product-recommendations[^>]*>'Expected: one line containing defer.
8. Verify the back-in-stock block is present on a sold-out product.
bash
curl -s https://YOURSTORE/products/<sold-out-product-handle> | grep -c 'product__back-in-stock-form'Expected: 1 when the product has at least one variant sold out.
9. Verify commerce OG tags for social sharing.
bash
curl -s https://YOURSTORE/products/<product-handle> | grep -oE '<meta[^>]*property="og:(type|price:amount|price:currency|availability)"[^>]*>'Expected: 4 lines (og:type=product, og:price:amount, og:price:currency, og:availability).
10. Verify the template name is product (not a custom template) — regression check for per-preset template gating.
bash
curl -s https://YOURSTORE/products/<product-handle> | grep -oE 'data-template="[^"]+"' | head -1Expected: data-template="product" or data-template="product.<preset>" (e.g., product.tine).
11. Verify the combined-listing chip renders on a parent-linked child product.
bash
curl -s https://YOURSTORE/products/<combined-listing-child-handle> | grep -c 'combined-listing-chip'Expected: 1 on a child product in a group.
12. Verify selling-plan save badge renders on a discounted subscription.
bash
curl -s https://YOURSTORE/products/<subscription-product-handle> | grep -oE 'data-selling-plan-save-badge[^>]*>'Expected: one or more lines if the product has selling plans with discount.
13. Verify the OOS restock banner renders when metafield is populated.
bash
curl -s https://YOURSTORE/products/<oos-product-handle> | grep -c 'product__restock-banner'Expected: 1 when the product is fully sold out AND has custom.restock_date populated.
Deferred items (v1.1 / Phase 06.5 / Phase 07)
These items were surfaced during the Phase 06.4 audit but explicitly deferred to a later phase. All have seed files tracking the revisit criteria.
| Finding | Rationale | Status |
|---|---|---|
| Rating count CLDR fan-out (F24) | Most locales only have one / other forms; 50-locale CLDR rollover belongs in Phase 06.5 PLP audit where rating is more prominent on collection cards | Deferred to Phase 06.5 |
| Asset count cap D-22 breach (F41) | Pre-existing breach (99→106 files vs cap of 60); Phase 7 renegotiates with fresh evidence (HTTP/2, Shopify CDN amortization, Lighthouse before/after) | Deferred to Phase 7 renegotiation |
@app block dispatch via content_for (F33) | Retrofitting PDP sections from legacy {% for block in section.blocks %} to content_for 'blocks' is a Theme Store review gate; migrate before submission | Deferred to Phase 7 |
product:condition merchant setting | Hardcoded "new" in JSON-LD; merchants selling refurbished / used need a per-product override setting | Deferred to Phase 7 |
| Embedded PDP diagnostic tool (same as 06.3) | Console snippet provides ~80% of the value at ~5% of the cost; v1 ships without an embedded UI | Deferred to Milestone 2 |
Related docs
- Product page guide — settings reference and configuration
- Theme settings
- Support form — submit a ticket if the above does not resolve your issue