Skip to content

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

  1. 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 carries data-scrub-* attributes.
  2. Lazy fetch on intent. On hover-intent (desktop) or dwell/first touch (mobile), the product-image-scrub.js module fetches the product's gallery from a public, same-origin endpoint:

    GET /api/public/v1/products/{seoName}/gallery
    

    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/public mount. That endpoint lives in the rest module, 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/public so 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 request Host — no API key or login required for this public, read-only endpoint. The dedicated api.* host and the JWT-protected /v1 API 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 shared responsive-image fragment 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: reduce is 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.