Skip to content

Product gallery and variants — 2026-09-26

The implementation described here shipped in the September 27 redesign. This page retains the detailed UI/data contract; start with the redesign story for context. Commands below use the generalized post-redesign tooling.

Follow-up to the layout review and Steps 5–6, before production cutover.

Product detail layout

The previous gallery rendered every photo in its own fixed 4:5 portrait frame. A landscape photo therefore had empty bands above and below it, and additional photos would have made the page progressively longer. The new gallery has:

  • One main photo at a time, using its natural loaded proportions without cropping. Portrait height is capped at the smaller of 68% of the viewport and 42rem; on phones it is capped at 60% and 32rem. Descriptions and expanded details still grow naturally.
  • A horizontal, scrollable row of 80px thumbnail buttons below the main photo, with a selected border, previous/next controls, and a localized photo counter. The strip and controls are omitted for a single photo. Many photos do not increase the page height by stacking full-size frames.
  • Native keyboard-operable buttons and a politely announced photo position; there is no automatic slideshow. Variant image-set changes reset the gallery to its first photo. Missing media has a localized empty state.
  • Desktop outer spacing of 32–48px above/below the product content, rather than the previous 80px top/bottom plus 64px related-section margin. The information-to-buy-panel gap is also tighter. Phones keep normal scrolling.
  • Following the 2026-09-27 review, the public product section fills at least the viewport below the header. Related pieces begin immediately afterward, outside that initial screen. This is a minimum height, not a fixed frame: when the photo/thumbnails, longer descriptions, options or expanded facts need more room, the section grows only as its content and padding require. Resizing vertically removes surplus space until it reaches those margins; content is never clipped or squeezed to force a fit. Phones retain the stacked layout and normal scrolling. The responsive public-nav minimum-height token is shared with the section calculation, including the header's 1px border.

Medusa's image records currently provide IDs, URLs, rank and metadata, not standard pixel dimensions. next/image uses 1200×900 presentation fallback attributes, not invented product dimensions; CSS switches to the loaded image's natural proportions. A portrait can change the initial layout as it loads. Recording verified pixel dimensions at upload time would improve that later.

How the data reaches the UI

flowchart LR
  A[Medusa Admin: media, options and variants] --> B[Medusa product records in PostgreSQL]
  A --> C[Photo files in Garage]
  B --> D[Medusa Store API]
  D --> E[Next.js server: listProducts and product route]
  E --> F[Variant image selection]
  F --> G[Gallery: main photo and thumbnails]
  C --> G
  E --> H[Option buttons, Medusa price and availability]
  H --> I[Selected variant ID in v_id query parameter]
  I --> F
  H --> J[Server cart action: variant ID and quantity]
  J --> D
  1. Andrea uploads all angles/detail photos in a product's Media section. Medusa's configured S3 provider stores the files in Garage; PostgreSQL stores their IDs, URLs and associations. Product media can be associated with specific variants in Admin. Upload a photo to the product first, then assign it to the applicable variant(s). See the official product media guide and variant media guide. This is supported by our installed Medusa 2.19 stack; no schema migration or separate frontend catalog is needed.
  2. src/lib/data/products.ts requests /store/products through the Medusa SDK on the server, with the Colombia region. The response includes product images/options and requested variants.images, variants.options, variants.calculated_price and inventory quantities. It uses no-store; published catalog edits are fetched on the next render, not baked into a new frontend release. Already open pages are not live-pushed updates.
  3. The Guanza product route reads the product handle and optional v_id. getImagesForVariant selects that variant's associated product images, preserving the order supplied by Medusa. No variant photos (or stale associations) falls back to the product's photo set rather than a blank gallery. Associate every relevant angle with each variant if it should remain visible when that variant is selected.
  4. image-gallery/index.tsx prepares only image IDs and public URLs for the small client-side ProductGallery. Thumbnail/arrow clicks change local photo state, without re-querying Medusa or adding anything to the cart. The browser fetches the photo from Garage's public media endpoint. In the local ngrok preview, the optional media proxy translates local-only image URLs; Medusa's stored records remain unchanged.

Options and variants

Options are dimensions of choice: Color, Size, Texture, Style, and so on. A variant is a sellable combination, such as Natural / Small, with its own ID/SKU, price and inventory rules. Multiple angles of the same object are media, not extra variants. A non-selectable material description is a product fact, not necessarily a buying option. Andrea owns these catalog choices; this change does not invent option values, prices or availability.

The existing ProductActions / OptionSelect components render text buttons from product.options[].values. Selecting a complete combination resolves a real returned variant. The UI then updates its price/stock display, writes its ID to ?v_id=... without jumping the scroll position, and the server route supplies its associated photos. A valid shared variant link now initializes the option buttons correctly instead of dropping the selection on mount. Unknown IDs do not select another product's variant; incomplete/impossible combinations cannot be added to the cart. Out-of-stock selection follows the existing Medusa inventory/backorder rules, not guessed availability.

The cart action sends variant ID + quantity, not the displayed text or a browser-decided price, to Medusa. The current guest workflow remains an inquiry, not a payment or stock reservation. Language switching preserves the existing variant query; UI control labels are localized. Untranslated merchant option names/values remain merchant content, consistent with the current disclosure.

Deliberate next steps, not implemented here

  • A color-swatch/texture-chip presentation can replace selected option text buttons once explicit approved colors or texture thumbnails are available. Do not infer hex colors from arbitrary Spanish/English option names.
  • Disable impossible option combinations proactively, rather than only blocking an incomplete/invalid add-to-cart selection; retain clear sold-out indications.
  • Add an enlarged-photo dialog, pinch/zoom or touch swiping if Andrea wants it. Thumbnail clicks and previous/next buttons already expose every associated photo.
  • Improve media delivery: next.config.js currently sets images.unoptimized. An 80px thumbnail display does not automatically generate an 80px download; responsive derivatives and verified source dimensions need a separate media optimization decision. See SEO planning for related image work.

Validation boundaries

Focused checks:

npm --prefix apps/storefront test
npm --prefix apps/storefront run lint
npm --prefix apps/storefront run build
node scripts/build-product-gallery-fixture.mjs
node scripts/check-app-layout.mjs --gallery-fixture

The fixture builder uses the installed storefront tsx dependency's esbuild; run it with the Linux toolchain in WSL. The opt-in browser check requires Playwright on Node's module search path and the local preview at port 8080; PLAYWRIGHT_CHROMIUM_EXECUTABLE can select an existing Chromium installation. The generated fixture bundle lives under ignored work/ and must be rebuilt before reviewing gallery changes. No new application dependencies are added.

Unit tests cover product/variant image selection, stale-association fallback, and valid/invalid shared variant option initialization. Browser review covers the real local product page in both languages, plus a separate component fixture with landscape, portrait, square and many-image cases. That fixture is not a public route or a Medusa database write. No VPS catalog/schema changes or production deployment are part of this iteration.

Validation completed on 2026-09-26: 41 storefront tests passed, storefront lint reported zero warnings, and the production build/type check succeeded. The opt-in browser check passed for both languages, including existing landing and related-card regressions, natural photo sizing, twelve-photo rail overflow, keyboard controls and resetting to a single-photo variant set. Real sample portrait media was also reviewed at desktop and 390px phone widths. Existing seed products do not exercise a real multi-variant purchase flow; image filtering and variant-link initialization have focused unit coverage, while the gallery fixture exercises image-set changes without modifying the catalog.

The 2026-09-27 viewport-focus adjustment also passed the 41 tests, lint, production build and opt-in browser checks. At desktop widths, the browser regression checks landscape and portrait products with details collapsed and expanded at 1000, 900, 700 and 500px heights: the section ends at the viewport boundary when content fits, otherwise immediately after the taller column plus the existing padding. Existing related-card alignment, phone reflow and gallery controls continue to pass. This follow-up changes layout only, not the Medusa catalog or purchasing workflow.