Listing Image Scrubbing¶
Lets shoppers preview a product's other gallery images without leaving the collection/listing/category page — a best-in-class "scrub through the photos" interaction on each product card. Listing pages stay lightweight: the card ships with only its primary image and the rest of the gallery is fetched lazily, on intent, from a small public endpoint.
Nova theme only. Off by default — enable it per site.
What shoppers see¶
| Device | Interaction |
|---|---|
| Desktop (mouse) | Move the cursor across the card image to step through the gallery; a thin segmented bar tracks position. Arrow keys also step when the card is focused. Nothing animates while idle. |
| Touch | Swipe the image to step through, with dot indicators; and when a card dwells centred in the viewport it gently auto-advances until the shopper swipes. |
The experience is progressive: with JavaScript disabled, a single-image product, or the feature
turned off, the card behaves exactly as before. It honours prefers-reduced-motion (instant
swaps, no auto-advance) and Save-Data (skips idle prefetch).
Enabling it¶
In Admin → Site Settings (Collection configuration):
| Setting | Key | Default | Purpose |
|---|---|---|---|
| Product Image Scrubbing | product-image-scrub-enabled |
false |
Master on/off for the feature on listing cards. |
| Gallery Images Per Card | gallery-max-images |
6 |
Caps how many images the public gallery endpoint returns per product. |
How it works¶
- Lean listing HTML. Collection (
/collection/...) and category (/c/...) listing pages render only the primary image per card (as before). When the feature is on and a product has more than one image, the card's image wrapper carriesdata-scrub-*attributes. -
Lazy fetch on intent. On hover-intent (desktop) or dwell/first touch (mobile), the
product-image-scrub.jsmodule fetches the product's gallery from a public, same-origin endpoint:The response is cached per product (and prefetched for cards near the viewport on idle), so repeat hovers are instant. 3. Same-origin via the ingress
/api/publicmount. That endpoint lives in therestmodule, whose public surface (/api/public/**) is mounted under each tenant's own domain by the ingress (Caddy in dev, the Cloudflare tunnel in production) — scoped to/api/publicso the web app keeps serving its own/api/*routes (uploads, inline edit, checkout session). Because the call is same-origin there is no CORS hop, and the tenant is resolved from the requestHost— no API key or login required for this public, read-only endpoint. The dedicatedapi.*host and the JWT-protected/v1API are unchanged. 4. Minimal payload. The endpoint returns an ordered, capped list of image URLs (and alt text) — values that are already public on the CDN. The client builds the resized (fit-in) URLs the same way the sharedresponsive-imagefragment does. 5. Abuse protection at the edge. Rate limiting and bot management are handled by Cloudflare, not in the application.
Accessibility & performance¶
- No added initial weight — the listing fetches no extra images up front; galleries load only on interaction.
- Reduced motion — auto-advance is disabled and image swaps are instant when
prefers-reduced-motion: reduceis set. - Screen readers — the position indicator and overlay frames are decorative (
aria-hidden); the card's link continues to expose the product name.
API¶
The gallery endpoint is part of the OpenAPI specification (operation getProductGallery); see the
API Reference.