Appearance
Product page support
Diagnostic and troubleshooting reference for the product detail page (PDP) — gallery + lightbox, variant picker, sticky add-to-cart, product recommendations, back-in-stock signup, selling-plan subscriptions, combined-listing parent-child, breadcrumb structured data, and per-preset template ordering.
FAQ
Clicking a gallery image does nothing.
Clicking or tapping a gallery image opens the fullscreen lightbox — a native <dialog> owned by media-gallery itself, with previous/next navigation; Escape or the close button closes it (verified live: 5 triggers on a 5-image product, dialog opens with prev/next, Escape closes). Diagnose in the console on a product page:
js
document.querySelectorAll('[data-lightbox-trigger]').length; // ≥ 1 — the clickable images
!!document.querySelector('[data-lightbox]'); // true — the dialog exists
!!customElements.get('media-gallery') && !!document.querySelector('media-gallery'); // must be trueIf triggers exist but clicks do nothing, check for custom JS intercepting clicks or a CSS override with pointer-events: none on the gallery. (A hover-magnifier zoom mode exists in the codebase but is not wired into the composed page — tracked as issue #234; do not diagnose against it.)
Zooming does not work inside the fullscreen viewer on mobile.
Inside the open lightbox dialog, zooming uses your browser's own pinch gesture on the image — the theme adds no custom gesture handlers, so if pinch fails it is being intercepted before the browser sees it: PWA shell wrappers or custom additions binding their own touchstart with preventDefault, or an embedded webview with gestures disabled. Close the dialog with Escape or its close button.
Gallery deep-links (URL with ?media=<n>) do not open the Nth image.
The gallery parses the media query parameter from the page-load URL — ?media=<n>, 1-based — and activates that image (?media=2 → second image; verified live). Clicking thumbnails keeps the URL updated via replaceState, so copied links reopen the same image. If a deep link is not working:
- Confirm the syntax is the query parameter
?media=<n>— an older hash-based syntax is not read. - Confirm the product has at least
nmedia items — an out-of-rangenis ignored (the first image stays active; nothing is logged). - Confirm the page loaded the product template — deep-links only apply where the gallery renders.
- The parameter is read once, at page load — changing it in the address bar without reloading does nothing.
The variant picker does not show the combined-listing chip.
The combined-listing chip only renders when:
- The product belongs to a combined-listing group (created with Shopify's Combined Listings app — a Shopify Plus feature); the render gate is simply whether Shopify exposes
combined_listingon the product - The
combined_listingblock is added to the product section (Theme editor → Product section → Add block → Combined listing)
What renders is plain navigation: cross-product option pills (each a link to the sibling carrying that value) plus the "You're viewing X — See related options" chip linking to the parent. Nothing expands in place. Check in the console: !!document.querySelector('.product__combined-listing'). If false on a product you believe is grouped, the group or the block composition is missing. On dev stores, seed a demo group with 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).
The surface is dual-track: two <sticky-atc> elements — data-mode='bar' (phones ≤ 749 px, with the optional quantity stepper) and data-mode='dock' (wide desktops ≥ 1400 px, no stepper). Between those widths, desktop relies on the sticky info column (CSS from ≥ 1024 px) and neither element shows — that is by design, not a failure.
If the sticky surface never shows on a width where it should:
- Confirm both elements are mounted and check the one for your viewport:
[...document.querySelectorAll('sticky-atc')].map(e => e.dataset.mode)should return['bar', 'dock']. - 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-atc {
display: none !important;
}Where do back-in-stock submissions go?
The back-in-stock form submits through Shopify's native contact form; Shopify emails each submission to your store's Sender email address (Shopify Admin → Settings → Notifications). The theme does not post to any external endpoint — there is no external-forwarding field of any kind, by design.
Verify:
- Open DevTools → Network tab on a sold-out PDP, submit the form, and confirm a
POST /contactappears with a redirect back to the product page. - Check the inbox for your store's Sender email address (Settings → Notifications). Each submission carries
contact[form_type]=back_in_stockplus the variant id, product handle, variant title, and the shopper's email.
To get these signups into Klaviyo / Omnisend / Postscript, use that provider's own signup form or Shopify app instead of this block, or forward the notification emails from your mail provider — the theme deliberately keeps the shopper's email inside Shopify rather than transmitting it to a merchant-typed URL.
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 (a zero adjustment), the badge intentionally does not render — a "Save 0%" badge would be misleading UX.
Verify in the DOM: the selling-plan block renders inside .product__selling-plan — a discounted plan shows its save badge there; a plan with a zero adjustment shows none. The badge wording follows the plan's price-adjustment type in Shopify Admin, not any theme setting: a percentage adjustment renders "Save X%", a fixed-amount adjustment renders the money-formatted amount (e.g. "Save €5.00").
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.
- Confirm the setting reached the storefront: the
<product-recommendations>element carriesdata-exclude-oos="true"(console:document.querySelector('product-recommendations')?.dataset.excludeOos). - With the filter on, no rendered card should show a Sold out badge. If sold-out cards still appear, push the latest theme — the client-side filter lives in
assets/product-recommendations.js.
Check the deployed script is current: curl -s <your-store>/products/<any-product> | grep -oE '<script[^>]*product-recommendations[^>]*>' — the tag should reference product-recommendations.js with a version query.
The OOS cross-sell strip does not render on a sold-out PDP.
The cross-sell renders after the buy column, only when every variant is sold out — the disabled add-to-cart button stays in place above it. If one variant is still available, the cross-sell is not triggered (the shopper can still add-to-cart via the available variant).
The strip fetches the theme's product-recommendations section file through Shopify's recommendations endpoint and ships its own script and stylesheet tags, so it does not depend on a separate Product recommendations section being composed. The fetch pins the strip's own parameters — related intent, limit 4, sold-out cards always excluded, the fetched section heading hidden in favour of the strip's own — and everything else renders with the section file's schema defaults. A separately-composed Product recommendations section's instance settings (heading, intent, limit, columns, quick-buy, Hide sold-out products, Limit to a collection) apply only to that section, never to this strip.
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 (the strip emits its own<script>tag, so a missing script means a stale deployed theme). - 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's engine can return no results — for example on new or low-data stores), or every returned card is sold out, the recommendations element empties and hides itself rather than showing bare placeholder cards. Nothing is injected in its place. A back-in-stock signup only appears if the Back-in-stock alert block is separately composed on the product — the two are independent.
Restock-date banner shows but the countdown is wrong.
The countdown is computed client-side, in the visitor's browser: the restock banner renders a <countdown-timer> whose remaining time comes from Date.now() against the metafield date, ticking live (compact days : hours : minutes; hides itself when the date passes). Two consequences: a visitor whose device clock is wrong sees a wrong remaining time, and before scripts run the banner shows the localized date itself rather than a countdown.
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 theme's breadcrumb structured data is schema.org microdata inline in the breadcrumb markup (snippets/breadcrumbs.liquid: itemscope itemtype='https://schema.org/BreadcrumbList' with ListItem children). Shopify may additionally emit its own BreadcrumbList JSON-LD script at the platform level (observed live) — either form satisfies Google Rich Results. Verify:
bash
curl -s <your-store>/products/<any-product> | grep -c 'schema.org/BreadcrumbList'Expected: 1 or more. The microdata's itemprop='item' links carry absolute URLs (each href is prefixed with the shop URL).
If the block is missing, the breadcrumb snippet is not rendered. Breadcrumbs render globally from layout/theme.liquid (before the page content) — they are not a product-template section, so there is nothing to remove in the theme editor's product view. If they are missing, diagnose the layout path: confirm layout/theme.liquid still renders the breadcrumbs snippet and that no customization removed that render.
The gallery does not switch images when a variant is selected.
Selecting a variant (click / change — there is no hover-preview path) fires a variant:change event; the gallery activates that variant's assigned featured image. If the variant has no image assigned in Shopify Admin, the gallery deliberately stays where it is. To fix:
- Shopify Admin → Products →
<product>→ Media → drag each image onto the matching variant. - That assignment is the whole mechanism — 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. The shipped .lighthouserc.cjs config runs the desktop preset — for mobile numbers, run Lighthouse from DevTools with mobile emulation.
Decision tree: "Why is X not working?"
text
Symptom: gallery lightbox not opening on click
-> Are the triggers present? (console: document.querySelectorAll('[data-lightbox-trigger]').length)
-> 0: the gallery markup did not render; check media-gallery.js in the Network tab
-> 1+: continue
-> Is the dialog present? (console: !!document.querySelector('[data-lightbox]'))
-> false: stale/customized gallery snippet; restore snippets/product-media-gallery.liquid
-> true: check for custom click handlers or `pointer-events: none` overrides on the gallery
(the pointer-mode magnifier in media-zoom.js is not wired into the composed page — issue #234; do not diagnose against it)
Symptom: sticky ATC never appears on scroll
-> Is the viewport in a sticky-element range? (bar ≤749px, dock ≥1400px; between them the CSS info column is the sticky surface)
-> no: by design — resize or test on a phone/wide desktop
-> yes: continue
-> Are both elements in the DOM? (console: [...document.querySelectorAll('sticky-atc')].map(e => e.dataset.mode))
-> not ['bar','dock']: 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 not appearing
-> DevTools → Network → submit form; is there a POST /contact?
-> no: check the block is enabled + the variant is actually sold out
-> yes: continue
-> Check the inbox for the store's Sender email address (Settings → Notifications)
-> missing: confirm the Sender email (Settings → Notifications) is a monitored address
-> present: working as designed (submissions stay inside Shopify — no external forwarding by design)
Symptom: combined-listing block missing
-> Is the product in a combined-listing group? (Combined Listings app — Shopify Plus; the gate is product.combined_listing existing)
-> no: create the group, or on dev stores 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: console check !!document.querySelector('.product__combined-listing') — false means the block did not render; re-check composition
Symptom: recommendations strip showing sold-out products
-> Is `exclude_sold_out` enabled on the section? (Theme editor → Hide sold-out products)
-> off: enable it
-> on: continue
-> Does the element carry data-exclude-oos="true"? (console: dataset.excludeOos)
-> no: the deployed theme is stale — push latest
-> yes: no rendered card should show a Sold out badge; if one does, push latest (filter: assets/product-recommendations.js)
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: the recommendations element hides itself (no fallback is injected); compose the Back-in-stock alert block if you want a signup there
-> non-empty but every returned product is sold out: same hide-by-design — the strip's always-on sold-out filter leaves zero cards to render
-> non-empty with in-stock products: check product-recommendations.js is loaded + check console for JS errors
Symptom: breadcrumb structured data missing
-> curl the PDP: grep 'schema.org/BreadcrumbList' — present? (microdata in the breadcrumb markup, not a JSON-LD script)
-> no: breadcrumbs render globally from layout/theme.liquid (before page content) — confirm the layout still renders the breadcrumbs snippet; there is no product-template entry to check
-> 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: the theme always emits them (snippets/fonts.liquid) — a missing preload means a customized layout dropped the fonts snippet; restore it
-> 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 | any offense | The repo gate is zero offenses (npm run theme:check) | An offense means a local edit introduced it — the shipped theme passes clean; diff your changes against the theme source |
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'),
presetClass: [...document.body.classList].find((c) => c.startsWith('preset-')) || null,
isProductPage: location.pathname.includes('/products/'),
preferences: {
pointerFine: matchMedia('(pointer: fine)').matches,
reducedMotion: matchMedia('(prefers-reduced-motion: reduce)').matches,
},
gallery: {
present: !!document.querySelector('media-gallery'),
activeIndex: document.querySelector('media-gallery')?._currentIndex ?? null,
mediaCount: document.querySelector('media-gallery')?.items?.length ?? 0,
lightboxTriggers: document.querySelectorAll('[data-lightbox-trigger]').length,
deepLink: new URLSearchParams(location.search).get('media'),
lightbox: {
present: !!document.querySelector('[data-lightbox]'),
open: document.querySelector('[data-lightbox]')?.open ?? false,
},
},
variantPicker: {
present: !!document.querySelector('variant-picker'),
urlVariant: new URLSearchParams(location.search).get('variant'),
pillCount: document.querySelectorAll('variant-picker .product__pill').length,
swatchCount: document.querySelectorAll('variant-picker .product__swatch').length,
dropdownCount: document.querySelectorAll('variant-picker .product__select').length,
},
combinedListing: {
blockRendered: !!document.querySelector('.product__combined-listing'),
},
buyButton: {
present: !!document.querySelector('.product__add-to-cart'),
enabled: (() => {
const btn = document.querySelector('.product__add-to-cart');
return btn ? !btn.disabled : null;
})(),
unitPriceVisible: !!document.querySelector('.product__unit-price'),
},
stickyAtc: {
modes: [...document.querySelectorAll('sticky-atc')].map((e) => e.dataset.mode),
visibleModes: [...document.querySelectorAll('sticky-atc')]
.filter((e) => getComputedStyle(e).display !== 'none' && getComputedStyle(e).visibility !== 'hidden')
.map((e) => e.dataset.mode),
},
backInStock: {
formPresent: !!document.querySelector('.product__back-in-stock-form'),
successShown: (() => {
const el = document.querySelector('.product__back-in-stock-success');
return el ? !el.hidden : false;
})(),
},
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,
},
oosCrossSell: {
present: !!document.querySelector('.product__oos-cross-sell'),
restockBanner: !!document.querySelector('.product__oos-restock-banner'),
countdownPresent: !!document.querySelector('.product__oos-restock-banner countdown-timer'),
},
sellingPlan: {
present: !!document.querySelector('.product__selling-plan'),
},
breadcrumb: {
microdataPresent: !!document.querySelector("[itemtype='https://schema.org/BreadcrumbList']"),
absoluteUrls: (() => {
const links = document.querySelectorAll("[itemtype='https://schema.org/BreadcrumbList'] [itemprop='item']");
if (!links.length) return null;
return [...links].every((a) => (a.href || '').startsWith('https://'));
})(),
},
productJsonLd: (() => {
/* Shopify's structured_data filter emits @type "Product" for
single-variant products and "ProductGroup" for multi-variant ones. */
for (const el of document.querySelectorAll("script[type='application/ld+json']")) {
try {
const parsed = JSON.parse(el.textContent);
if (parsed['@type'] === 'Product' || parsed['@type'] === 'ProductGroup')
return { present: true, type: parsed['@type'] };
} catch (e) {
/* skip malformed blocks */
}
}
return { present: false, type: null };
})(),
shopperState: {
available: typeof window.UisceShopperState === 'object',
stateKeys: Object.keys(localStorage).filter((k) => k.startsWith('uisce.state.v1.')).length,
},
};
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.lightboxTriggers >= 1andgallery.lightbox.present === true— clicking an image opens the fullscreen viewer.variantPicker.present === true— picker mounted; exactly one ofpillCount/swatchCount/dropdownCountis non-zero per option.combinedListing.blockRendered === trueonly on combined-listing children with the block composed.buyButton.enabled === trueon an in-stock product,falseon a sold-out product;unitPriceVisible === trueon variants with a unit-price measurement.stickyAtc.modesis['bar', 'dock']when the section is composed;visibleModesdepends on viewport width (bar ≤ 749 px, dock ≥ 1400 px, neither in between) and scroll position.backInStock.formPresent === trueon sold-out variants; the form submits to Shopify viaPOST /contact(no external endpoint).recommendations.present === true; withexcludeOos === true, no rendered card should show a Sold out badge.oosCrossSell.present === trueon fully-sold-out PDPs;restockBanner+countdownPresentwhen the restock metafield is populated.sellingPlan.present === truewhen the block is composed; a discounted plan renders its Save badge inside it.breadcrumb.microdataPresent === trueandabsoluteUrls === true— the microdata'sitemprop='item'links all start with https://.productJsonLd.present === true— Shopify'sstructured_datafilter emitsProduct(single-variant) orProductGroup(multi-variant); verified live asProductGroupon a multi-variant PDP.shopperState.available === true— shopper-state primitive loaded (its keys are namespaceduisce.state.v1.*).
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), a bare curl gets the password page — authenticate in a browser and reuse its cookies (Shopify's storefront session rides the _shopify_essential cookie), or use a browser-driven check instead. 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 -c '"@type":"Product'Expected: 1 or more — Shopify's structured_data filter emits "Product" for single-variant products and "ProductGroup" for multi-variant ones (the prefix grep matches both; verified live).
2. Verify the BreadcrumbList microdata is present with absolute URLs.
bash
curl -s https://YOURSTORE/products/<product-handle> | grep -c 'schema.org/BreadcrumbList'Expected: 1. The breadcrumb's itemprop='item' links carry absolute URLs (each href is prefixed with the shop URL).
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 the media-gallery custom element is present.
bash
curl -s https://YOURSTORE/products/<product-handle> | grep -c '<media-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 the sticky add-to-cart elements are mounted.
bash
curl -s https://YOURSTORE/products/<product-handle> | grep -c '<sticky-atc'Expected: 2 — the dual-track surface always mounts both the bar (≤ 749 px) and the dock (≥ 1400 px); the viewport decides which of the two (if either) is visible, not which is in the markup.
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 when the CURRENT variant is sold out.
bash
curl -s 'https://YOURSTORE/products/<handle>?variant=<sold-out-variant-id>' | grep -c 'product__back-in-stock-form'Expected: 1. The block renders when the selected variant (or the first available one, when nothing is selected) is unavailable and the block is composed — target an actually-unavailable variant with ?variant=; other variants being sold out is not the trigger.
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 preset cascade reached the page — regression check for per-preset packaging.
bash
curl -s https://YOURSTORE/products/<product-handle> | grep -oE "class='preset-[a-z]+'" | head -1Expected: class='preset-<your-preset>' on the <body> (e.g. preset-tine).
11. Verify the combined-listing block renders on a grouped child product.
bash
curl -s https://YOURSTORE/products/<combined-listing-child-handle> | grep -c 'product__combined-listing'Expected: 1 or more on a child product in a group with the block composed.
12. Verify the selling-plan block renders on a subscription product.
bash
curl -s https://YOURSTORE/products/<subscription-product-handle> | grep -c 'product__selling-plan'Expected: 1 or more when the product has selling plans and the block is composed.
13. Verify the OOS restock banner renders when the metafield is populated.
bash
curl -s https://YOURSTORE/products/<oos-product-handle> | grep -c 'product__oos-restock-banner'Expected: 1 when the product is fully sold out AND has the restock-date metafield 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' was a Theme Store review gate | Shipped — the PDP dispatches via content_for 'blocks'; Theme Check runs clean |
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