Per-Variant Media Gallery
For merchants: when the supplier provides multiple photos for a color, PromoSync attaches them all to the matching variant (front, back, side, lifestyle), so shoppers see every angle of the color they picked without leaving the variant. Coverage depends on what the supplier publishes; some products and some colors only ship with a single image. Available on the Trial, Business, and Enterprise plans. The rest of this page is for Shopify theme developers who will render the gallery on the storefront.
Overview
The per-variant media gallery is a curated, ordered list of
color-matched images attached to each Shopify product variant. It lives
in a single metafield (psrestful.gallery) and contains every angle
(front, back, side, lifestyle) for the variant’s specific color. The
first item in the list is the variant’s featured image; the rest are
additional views of the same color.
The feature exists because Shopify’s native data model only allows one image per variant. Merchants who sell apparel and other color-keyed products want shoppers to see every angle of the selected color, not just a single hero shot. By writing a list of file references at the variant level, themes can render a complete, color-aware gallery that swaps in lockstep with the variant picker.
The images come from the supplier’s PromoStandards media feed, surfaced through the PSRESTful API. At import time, PromoSync picks the color-matched images for each variant using supplier-specific association strategies (a color cascade for SanMar and S&S Activewear, partId-only matching for HIT, and a default cascade for other suppliers). The matched files are then uploaded to Shopify, and their GIDs are attached to the variant.
Plan eligibility
The psrestful.gallery variant metafield is populated only on the
following PSRESTful plans:
| Plan | Variant gallery populated? | Native variant.featured_image |
|---|---|---|
| Trial | Yes | Yes |
| Business | Yes | Yes |
| Enterprise | Yes | Yes |
| Free | No | Yes |
| Standard | No | Yes |
| Premium | No | Yes |
| Supplier | No | Yes |
| Small | No | Yes |
| Medium | No | Yes |
| Large | No | Yes |
On non-eligible plans, variants still receive a single featured image
through Shopify’s native variant.featured_image, so storefronts see
no regression; they just don’t get the multi-image gallery. Themes
that follow the fallback pattern
below work correctly on every plan.
To upgrade and unlock the gallery, see {{upgrade_url}}.
Data model
The gallery is stored as a single metafield on the variant:
| Property | Value |
|---|---|
| Owner | PRODUCTVARIANT |
| Namespace | psrestful |
| Key | gallery |
| Type | list.file_reference |
| Value | Ordered list of Shopify File references (MediaImage, since the gallery only contains images) |
Each item in the list is a Shopify File reference. PromoSync only
writes image files, so in practice every entry dereferences to a
MediaImage. The first item is always the variant’s featured
image, and subsequent items are additional views for the same color,
in the canonical PromoStandards order (primary → front → rear →
left/right → inside/outside → other).
The featured image is duplicated into the gallery on purpose. It lets
themes render a single ordered gallery without having to special-case
the featured image (no need to prepend variant.featured_image
separately, no risk of rendering it twice).
Related: product-level media ordering
On products that have a per-variant gallery, product-level media in the
Shopify Admin is also color-grouped, with every photo of one color
sitting together (Black, Black, Black, Blue, Blue, Blue, …). Within
each color, images are sorted primary → front → rear → left/right →
inside/outside → other. PromoSync enforces this ordering with a
productReorderMedia mutation that runs immediately after the initial
productSet.
Liquid integration
The gallery metafield is list.file_reference, so each item in
metafield.value is a File object, not a MediaImage directly.
To render an image, read the file’s preview_image property.
Available per-file properties:
| Property | Description |
|---|---|
.preview_image | The underlying image (use image_url) |
.alt | Alt text |
.media_type | image, video, model, etc. |
Basic gallery render with fallback
This is the recommended baseline. It reads psrestful.gallery, falls
back to variant.featured_image on plans where the metafield is empty,
and uses Shopify’s image filters with responsive widths and lazy
loading.
{% raw %}{% assign variant = product.selected_or_first_available_variant %}
{% assign gallery = variant.metafields.psrestful.gallery.value %}
<div class="variant-gallery" data-variant-id="{{ variant.id }}">
{% if gallery and gallery.size > 0 %}
{% for file in gallery %}
{% assign image = file.preview_image %}
<figure class="variant-gallery__item" data-media-type="{{ file.media_type }}">
{{ image | image_url: width: 1200 | image_tag:
widths: '300, 600, 900, 1200',
sizes: '(min-width: 750px) 50vw, 100vw',
alt: file.alt,
loading: 'lazy' }}
</figure>
{% endfor %}
{% elsif variant.featured_image %}
{# Fallback for shops without the gallery metafield (Free / Standard / Premium / etc.) #}
<figure class="variant-gallery__item">
{{ variant.featured_image | image_url: width: 1200 | image_tag:
widths: '300, 600, 900, 1200',
sizes: '(min-width: 750px) 50vw, 100vw',
alt: variant.featured_image.alt,
loading: 'lazy' }}
</figure>
{% endif %}
</div>{% endraw %}Fallback to variant.featured_image
The fallback in the example above is the only thing required to keep
free-tier and other non-eligible shops working. Because PromoSync still
sets variant.image on every plan, the single-image experience is
unchanged for those stores; your theme just renders one figure instead
of many.
Swapping the gallery on variant change
When a shopper picks a new variant, the gallery has to swap to the new variant’s color. There are two common patterns.
(a) Section Rendering API
This is the most robust option and requires no metafield-aware JavaScript. Shopify re-renders the section server-side with the new variant selected.
// Fired by Dawn-style themes when the picker changes the variant.
document.addEventListener('variant:change', async (event) => {
const variantId = event.detail.variant.id;
const sectionId = 'main-product'; // your section's id
const url = `${window.location.pathname}?variant=${variantId}§ion_id=${sectionId}`;
const html = await fetch(url).then(r => r.text());
const fresh = new DOMParser()
.parseFromString(html, 'text/html')
.querySelector('.variant-gallery');
document.querySelector('.variant-gallery').replaceWith(fresh);
});Because the request includes ?variant=ID,
product.selected_or_first_available_variant resolves to the new
variant on the server, and the Liquid above renders that variant’s
gallery directly.
(b) Client-side JS using preloaded metafields
If you want to avoid the network round-trip, expose the metafield to
JavaScript and swap the DOM in place. You’ll need to make the metafield
available in the page, either by serializing it into a <script type="application/json"> tag or by enabling Storefront API access on
the metafield definition.
{% raw %}<script type="application/json" id="variant-galleries">
{
{% for variant in product.variants %}
"{{ variant.id }}": [
{% for file in variant.metafields.psrestful.gallery.value %}
{
"src": {{ file.preview_image | image_url: width: 1200 | json }},
"alt": {{ file.alt | json }}
}{% unless forloop.last %},{% endunless %}
{% endfor %}
]{% unless forloop.last %},{% endunless %}
{% endfor %}
}
</script>{% endraw %}const galleries = JSON.parse(
document.getElementById('variant-galleries').textContent
);
document.addEventListener('variant:change', (event) => {
const items = galleries[event.detail.variant.id] || [];
const container = document.querySelector('.variant-gallery');
container.innerHTML = items.map(item => `
<figure class="variant-gallery__item">
<img src="${item.src}" alt="${item.alt || ''}" loading="lazy" />
</figure>
`).join('');
});The Section Rendering approach is easier to maintain (one source of truth in Liquid). The client-side approach is faster on swap but duplicates rendering logic between Liquid and JS.
Defining the metafield in Theme Customizer
For the metafield to be usable as metafield.value (auto-dereferenced
to file objects), it must be registered as a metafield definition
in Shopify Admin under Settings → Custom data → Variants.
Without a definition, .value returns the raw JSON of GIDs and
.preview_image won’t work.
PromoSync registers this definition automatically on app install via
bin/create_metafields.py. Merchants can verify it exists by visiting:
Settings → Custom data → Variants → “Variant Gallery”
The expected definition shape:
| Field | Value |
|---|---|
| Name | Variant Gallery |
| Namespace | psrestful |
| Key | gallery |
| Type | List of file references |
| Validation | Image files only |
If the definition is missing (most often because PromoSync was
installed before this feature shipped and the merchant hasn’t
re-triggered the metafield setup), the metafield values may still be
written, but .value won’t dereference to file objects in Liquid. Have
the merchant re-run the metafield setup from the PromoSync app or
contact support at {{support_url}}.
Behavior details worth knowing
A few non-obvious rules are applied at import time. Knowing them will save you from chasing “missing image” reports that aren’t actually bugs.
- Swatches are filtered out. PromoStandards class type
1004(color swatches) never appear in the gallery. Use the variant’scolor_arraymetafield if you need to render a swatch UI. - Low-resolution images are filtered out. Any image below the shop’s configured Minimum Image Resolution (set in PromoSync shop settings) is dropped at the source. This includes SanMar’s 300×450 “shared primary” filler image, which would otherwise pollute every variant gallery.
by_colorshops skip the gallery. Shops configured withvariant_grouping_strategy='by_color'create one Shopify product per color (e.g. Hit-a-Double-style stores). The metafield is not written on these shops because the product itself is already color-curated. Use Shopify’s standardproduct.mediainstead.- “Import first image only” disables extras. When the merchant enables Import first image only in import settings, the gallery collapses to just the featured image. The metafield is still written (so your theme code keeps working), but it will contain a single item.
Troubleshooting
Gallery is empty in the theme but I’m on Business or Enterprise
- Verify the metafield definition exists at Settings → Custom data →
Variants → “Variant Gallery”. Without a definition,
.valuereturns raw JSON instead of file objects, and the{% raw %}{% if gallery and gallery.size > 0 %}{% endraw %}guard will treat it as falsy. - Re-import the affected product. The gallery metafield is populated at import time only; it is not retroactively backfilled on variants that were imported before the feature was enabled or before the plan was upgraded.
Gallery only shows one image
Either:
- The shop’s plan does not include the gallery feature (see Plan
eligibility). The theme is correctly falling
back to
variant.featured_image. Upgrade at{{upgrade_url}}. - The supplier’s media feed had no color-matched extras for that variant (common for accessory items, single-angle products, or smaller suppliers).
- The shop has Import first image only turned on in import settings.
Images out of order in Shopify Admin but correct on the storefront
The productReorderMedia mutation that color-groups product-level
media runs asynchronously after import. The Admin Media tab can take up
to a minute to settle. The variant gallery itself (psrestful.gallery)
is written in order in the same mutation that creates the metafield, so
the storefront order is correct immediately.
If the Admin order is still wrong after a few minutes, contact support
at {{support_url}} with the product handle.
Gallery and featured image are both empty
If variant.featured_image is nil and the gallery metafield is also
empty, the supplier’s media feed simply didn’t publish any image for
that color of that product. This is unrelated to the plan or the
metafield definition — PromoSync only attaches images the supplier
actually provides. It shows up most often on niche, discontinued, or
brand-new SKUs.
Verify by opening the product on PSRESTful and checking the Media Content response for that part. If the supplier later adds images, re-importing the product will pick them up.