Shopify themes, liquid, logos, and UX
The issue: My product page doesn't assign the correct images to the color swatches/variants.
This is the code:
{% assign product_has_only_default_variant = product.has_only_default_variant %} {% assign variant_images = product.images | where: 'attached_to_variant?', true | map: 'src' %} {% assign first_3d_model = product.media | where: 'media_type', 'model' | first %} <div class="product-detail-section" data-product-id="{{ product.id }}" data-section-id="{{ section.id }}"> <div class="product-detail-wrapper"> <div class="product-media-column"> <div class="product-media-gallery"> {% if product.media.size > 0 %} <div class="product-images product-main-images"> {% for media in product.media %} {% case media.media_type %} {% when 'image' %} {% assign associated_variant = false %} {% for variant in product.variants %} {% if variant.featured_image and variant.featured_image.id == media.id %} {% assign associated_variant = variant %} {% break %} {% endif %} {% endfor %} <img src="{{ media | img_url: '800x800', crop: 'center' }}" alt="{{ media.alt | escape }}" class="product-image {% if forloop.first %}active{% endif %}" data-index="{{ forloop.index0 }}" data-media-id="{{ media.id }}" {% if associated_variant %}data-variant-id="{{ associated_variant.id }}"{% endif %} loading="{% if forloop.first %}eager{% else %}lazy{% endif %}"> {% when 'model' %} <div class="product-model {% if forloop.first %}active{% endif %}" data-index="{{ forloop.index0 }}" data-media-id="{{ media.id }}"> {{ media | model_viewer_tag }} </div> {% when 'video' %} <div class="product-video {% if forloop.first %}active{% endif %}" data-index="{{ forloop.index0 }}" data-media-id="{{ media.id }}"> {{ media | video_tag: controls: true, autoplay: false }} </div> {% endcase %} {% endfor %} </div> {% if product.media.size > 1 %} <div class="product-thumbnails"> {% for media in product.media %} {% assign associated_variant = false %} {% for variant in product.variants %} {% if media.variant_ids contains variant.id %} {% assign associated_variant = variant %} {% break %} {% endif %} {% endfor %} <button class="thumbnail-button {% if forloop.first %}active{% endif %}" data-index="{{ forloop.index0 }}" data-media-id="{{ media.id }}" {% if associated_variant %}data-variant-id="{{ associated_variant.id }}"{% endif %} aria-label="Go to image {{ forloop.index }}" > <img src="{{ media | img_url: '100x100', crop: 'center' }}" alt="{{ media.alt | escape }}" loading="lazy" > </button> {% endfor %} </div> {% endif %} {% else %} <div class="product-no-image"> <div class="no-image-placeholder">NO IMAGE</div> </div> {% endif %} </div> </div> <div class="product-info-column"> <div class="product-info-wrapper"> {% if product.tags contains 'Best Seller' %} <div class="product-badge">Best Seller</div> {% endif %} <h1 class="product-title">{{ product.title }}</h1> <div class="product-price"> {%- render 'price', product: product, use_variant: true, show_badges: true -%} </div> {% if product.metafields.reviews.rating %} <div class="product-reviews"> <div class="star-rating"> {% assign rating = product.metafields.reviews.rating.value | round %} {% for i in (1..5) %} {% if i <= rating %} <span class="star filled">★</span> {% else %} <span class="star">★</span> {% endif %} {% endfor %} </div> <a href="#reviews" class="review-count">{{ product.metafields.reviews.count }}</a> </div> {% endif %} {% unless product_has_only_default_variant %} <div class="product-variants"> {% for option in product.options_with_values %} {% if option.name == 'Color' or option.name == 'Colour' %} <div class="variant-option"> <h3 class="option-name"> {{ option.name }}: <span class="selected-color-name"></span> </h3> <div class="color-options"> {% assign unique_values = option.values | uniq %} {% for value in unique_values %} {% assign color_code = value | handleize %} {% assign has_image = false %} {% assign variant_id = "" %} {% assign media_id = "" %} {% for variant in product.variants %} {% if variant.options[forloop.parentloop.index0] == value %} {% assign variant_id = variant.id %} {% for media in product.media %} {% if media.variant_ids contains variant.id %} {% assign has_image = true %} {% assign media_id = media.id %} {% break %} {% endif %} {% endfor %} {% break %} {% endif %} {% endfor %} <button class="color-option {% if forloop.first %}active{% endif %}" data-color="{{ color_code }}" data-option-position="{{ option.position }}" data-value="{{ value | escape }}" data-variant-id="{{ variant_id }}" {% if has_image %}data-media-id="{{ media_id }}"{% endif %} style="background-color: {{ value | downcase | replace: ' ', '' | split: '/' | last | default: color_code }};" title="{{ value }}" {% if forloop.first %}aria-pressed="true"{% else %}aria-pressed="false"{% endif %}> </button> {% endfor %} </div> </div> {% else %} <div class="variant-option"> <h3 class="option-name">{{ option.name }}</h3> <div class="size-options"> {% for value in option.values %} <button class="size-option {% if forloop.first %}active{% endif %}" data-option-position="{{ option.position }}" data-value="{{ value | escape }}" {% if forloop.first %}aria-pressed="true"{% else %}aria-pressed="false"{% endif %} > {{ value }} </button> {% endfor %} </div> </div> {% endif %} {% endfor %} </div> {% endunless %} <div class="product-actions"> <button type="button" class="add-to-cart-button" data-product-id="{{ product.id }}" data-variant-id="{{ product.selected_or_first_available_variant.id }}" {% if product.available == false %}disabled{% endif %} > {% if product.available %} ADD TO BAG {% else %} SOLD OUT {% endif %} </button> </div> <div class="product-accordions"> <details class="product-accordion"> <summary> <h3>DESCRIPTION</h3> <span class="accordion-icon">+</span> </summary> <div class="accordion-content"> {{ product.description }} </div> </details> <details class="product-accordion"> <summary> <h3>FREE SHIPPING & RETURNS</h3> <span class="accordion-icon">+</span> </summary> <div class="accordion-content"> <p>Free standard shipping on all orders.</p> <p>Free returns within 30 days. See our <a href="/pages/returns">return policy</a> for more information.</p> </div> </details> </div> </div> </div> </div> </div> <script> document.addEventListener('DOMContentLoaded', function() { const productSection = document.querySelector('.product-detail-section'); if (!productSection) return; const productId = productSection.dataset.productId; const mainImages = Array.from(productSection.querySelectorAll('.product-main-images .product-image, .product-main-images .product-video, .product-main-images .product-model')); const thumbnailButtons = Array.from(productSection.querySelectorAll('.thumbnail-button')); const colorOptions = Array.from(productSection.querySelectorAll('.color-option')); const sizeOptions = Array.from(productSection.querySelectorAll('.size-option')); const addToCartButton = productSection.querySelector('.add-to-cart-button'); const selectedColorNameSpan = productSection.querySelector('.selected-color-name'); // Added selector const colorInfo = colorOptions.map((option, index) => { return { element: option, value: option.getAttribute('data-value') || option.getAttribute('title') || `Color ${index + 1}`, variantId: option.getAttribute('data-variant-id') || null, mediaId: option.getAttribute('data-media-id') || null, index: index }; }); let allVariantData = []; let currentIndex = 0; const getProductHandle = () => { const path = window.location.pathname; const pathParts = path.split('/'); let handle = ''; for (let i = 0; i < pathParts.length; i++) { if (pathParts[i] === 'products' && i + 1 < pathParts.length) { handle = pathParts[i + 1].split('?')[0]; break; } } return handle; }; const fetchProductData = async () => { const handle = getProductHandle(); if (!handle) return null; try { const response = await fetch(`/products/${handle}.js`); if (!response.ok) throw new Error(`HTTP Error: ${response.status}`); return await response.json(); } catch (error) { console.error('Error getting product data:', error); return null; } }; const buildVariantDataList = (productData) => { if (!productData || !productData.variants || productData.variants.length === 0) return []; let colorOptionPosition = -1; if (productData.options && Array.isArray(productData.options)) { colorOptionPosition = productData.options.findIndex(opt => opt.name.toLowerCase() === 'color' || opt.name.toLowerCase() === 'colour' ); } const useDirectMapping = productData.variants.length === colorOptions.length; const variantMediaMap = {}; if (productData.media && Array.isArray(productData.media)) { productData.media.forEach(media => { if (media.id && media.variant_ids && Array.isArray(media.variant_ids)) { media.variant_ids.forEach(variantId => { variantMediaMap[variantId] = String(media.id); }); } }); } const variants = []; productData.variants.forEach((variant, variantIndex) => { let colorValue = ''; if (colorOptionPosition !== -1) { const optionKey = `option${colorOptionPosition + 1}`; colorValue = variant[optionKey] || ''; } else { const colorData = useDirectMapping && variantIndex < colorInfo.length ? colorInfo[variantIndex] : null; colorValue = colorData ? colorData.value : ''; } let variantImage = variant.featured_image || null; let mediaId = variantImage ? String(variantImage.id) : variantMediaMap[variant.id] || null; if (!mediaId && useDirectMapping && variantIndex < colorInfo.length && colorInfo[variantIndex].mediaId) { mediaId = colorInfo[variantIndex].mediaId; } let imageElement = null; if (mediaId) { imageElement = mainImages.find(img => img.getAttribute('data-media-id') === mediaId); } if (!imageElement && useDirectMapping && variantIndex < mainImages.length) { imageElement = mainImages[variantIndex]; } let colorElement = null; for (const color of colorOptions) { if (color.getAttribute('data-variant-id') === String(variant.id)) { colorElement = color; break; } } if (!colorElement && useDirectMapping && variantIndex < colorOptions.length) { colorElement = colorOptions[variantIndex]; } variants.push({ id: String(variant.id), colorValue: colorValue, colorElement: colorElement, mediaId: mediaId, imageElement: imageElement, imageIndex: imageElement ? mainImages.indexOf(imageElement) : -1, price: variant.price, available: variant.available, variantIndex: variantIndex }); if (colorElement) { colorElement.setAttribute('data-variant-id', String(variant.id)); if (mediaId) { colorElement.setAttribute('data-media-id', mediaId); } // Ensure title attribute reflects the actual color value from variant if (colorValue) { colorElement.setAttribute('title', colorValue); if (!colorElement.hasAttribute('data-value')) { colorElement.setAttribute('data-value', colorValue); } } } }); return variants; }; function updateDisplay(variantIndex) { if (variantIndex < 0 || variantIndex >= allVariantData.length) return; currentIndex = variantIndex; const variant = allVariantData[currentIndex]; colorOptions.forEach(btn => { btn.classList.remove('active'); btn.setAttribute('aria-pressed', 'false'); }); if (variant.colorElement) { variant.colorElement.classList.add('active'); variant.colorElement.setAttribute('aria-pressed', 'true'); } mainImages.forEach(img => img.classList.remove('active')); let imageIndex = -1; if (variant.imageElement) { variant.imageElement.classList.add('active'); imageIndex = mainImages.indexOf(variant.imageElement); } else if (variant.mediaId) { for (let i = 0; i < mainImages.length; i++) { if (mainImages[i].getAttribute('data-media-id') === variant.mediaId) { mainImages[i].classList.add('active'); imageIndex = i; break; } } } if (imageIndex === -1 && variant.variantIndex < mainImages.length) { imageIndex = variant.variantIndex; // Fallback to index if lengths match } if (imageIndex === -1 && mainImages.length > 0) { imageIndex = 0; // Fallback to first image } if (imageIndex !== -1 && imageIndex < mainImages.length) { mainImages[imageIndex].classList.add('active'); } thumbnailButtons.forEach(btn => btn.classList.remove('active')); if (imageIndex >= 0 && imageIndex < thumbnailButtons.length) { thumbnailButtons[imageIndex].classList.add('active'); } // Update selected color name display if (selectedColorNameSpan) { selectedColorNameSpan.textContent = variant.colorValue || ''; } if (addToCartButton) { addToCartButton.setAttribute('data-variant-id', variant.id); addToCartButton.disabled = !variant.available; addToCartButton.textContent = variant.available ? 'ADD TO BAG' : 'SOLD OUT'; } // Trigger price update (assumes a 'price' snippet handles this via events or direct update) const priceElement = productSection.querySelector('.product-price'); if (priceElement) { const event = new CustomEvent('variant:changed', { bubbles: true, detail: { variant: variant } }); priceElement.dispatchEvent(event); // If the price snippet doesn't listen for events, you might need direct manipulation here // e.g., update price based on variant.price } } function updateCartState() { fetch('/cart.js') .then(response => response.json()) .then(cart => { const event = new CustomEvent('cart:updated', { bubbles: true, detail: cart }); document.dispatchEvent(event); const cartTotalElements = document.querySelectorAll('#cart-total'); cartTotalElements.forEach(element => { element.textContent = `$ ${(cart.total_price / 100).toFixed(2)}`; }); const cartCountElements = document.querySelectorAll('.cart-count, .cart-quantity, .cart-item-count, .js-cart-count'); cartCountElements.forEach(element => { element.textContent = cart.item_count; if (cart.item_count > 0 && element.classList.contains('hidden')) { element.classList.remove('hidden'); } else if (cart.item_count === 0 && !element.classList.contains('hidden')) { element.classList.add('hidden'); // Hide if empty } }); // Update section rendering for cart total if needed fetch('/?sections=cart-total') .then(response => response.text()) .then(sectionsText => { try { const sectionsJson = JSON.parse(sectionsText); const cartTotalHtml = sectionsJson['cart-total']; if (cartTotalHtml) { const parser = new DOMParser(); const doc = parser.parseFromString(cartTotalHtml, 'text/html'); const newCartTotalContent = doc.querySelector('#cart-total')?.innerHTML; if (newCartTotalContent) { document.querySelectorAll('#cart-total').forEach(el => { el.innerHTML = newCartTotalContent; }); } } } catch(e) { console.warn("Could not parse cart-total section response"); } }) .catch(error => console.error('Error updating cart total section:', error)); }) .catch(error => console.error('Error updating cart information:', error)); } function setupColorButtons() { colorOptions.forEach((btn, index) => { btn.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation(); const variantId = btn.getAttribute('data-variant-id'); const colorValue = btn.getAttribute('data-value') || btn.getAttribute('title'); let targetIndex = -1; if (variantId) { targetIndex = allVariantData.findIndex(v => v.id === variantId); } if (targetIndex === -1) { targetIndex = allVariantData.findIndex(v => v.colorElement === btn); } if (targetIndex === -1 && allVariantData.length === colorOptions.length) { targetIndex = index; } if (targetIndex !== -1) { updateDisplay(targetIndex); } }); }); } function setupThumbnails() { thumbnailButtons.forEach((btn, index) => { btn.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation(); if (index >= mainImages.length) return; const mediaId = mainImages[index].getAttribute('data-media-id'); let targetIndex = -1; if (mediaId) { targetIndex = allVariantData.findIndex(v => v.mediaId === mediaId); } if (targetIndex === -1) { targetIndex = allVariantData.findIndex(v => v.imageElement === mainImages[index]); } if (targetIndex === -1 && allVariantData.length === mainImages.length) { targetIndex = index; } if (targetIndex !== -1) { updateDisplay(targetIndex); } else { mainImages.forEach(img => img.classList.remove('active')); mainImages[index].classList.add('active'); thumbnailButtons.forEach(button => button.classList.remove('active')); btn.classList.add('active'); } }); }); } function setupAddToCartButton() { if (!addToCartButton) return; addToCartButton.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation(); if (this.disabled || this.classList.contains('adding')) return; const variantId = this.getAttribute('data-variant-id'); if (!variantId) return; this.classList.add('adding'); const originalText = this.textContent; this.textContent = 'Adding...'; const formData = { 'items': [{ 'id': variantId, 'quantity': 1 }] }; fetch('/cart/add.js', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(formData) }) .then(response => { if (!response.ok) { return response.json().then(data => { throw { status: response.status, message: data.description || 'Error' }; }); } return response.json(); }) .then(data => { this.textContent = 'Added!'; updateCartState(); setTimeout(() => { this.classList.remove('adding'); this.textContent = originalText; }, 1500); }) .catch(error => { this.classList.remove('adding'); this.textContent = error.message || 'Error'; setTimeout(() => { this.textContent = originalText; // Re-evaluate button state based on current variant availability const currentVariant = allVariantData[currentIndex]; if(currentVariant) { this.disabled = !currentVariant.available; this.textContent = currentVariant.available ? 'ADD TO BAG' : 'SOLD OUT'; } else { this.textContent = 'ADD TO BAG'; // Default fallback this.disabled = false; } }, 2000); }); }); } async function init() { try { const productData = await fetchProductData(); if (!productData) return; allVariantData = buildVariantDataList(productData); if (allVariantData.length === 0) return; setupColorButtons(); setupThumbnails(); setupAddToCartButton(); // Initialize display with the first available variant or just the first one let initialIndex = allVariantData.findIndex(v => v.available); if (initialIndex === -1) initialIndex = 0; // Fallback to the first variant if none are available if(allVariantData.length > 0) { updateDisplay(initialIndex); } } catch (error) { console.error('Error during initialization:', error); } } init(); const accordions = productSection.querySelectorAll('.product-accordion'); accordions.forEach(accordion => { const summary = accordion.querySelector('summary'); if (summary) { summary.addEventListener('click', (e) => { // Only prevent default if we're managing the open state manually // e.preventDefault(); // Might not be needed with <details> default behavior const currentlyOpen = accordion.hasAttribute('open'); // Close others only if we are opening this one if (!currentlyOpen) { accordions.forEach(acc => { if (acc !== accordion && acc.hasAttribute('open')) { acc.removeAttribute('open'); // Update icon if needed const otherIcon = acc.querySelector('.accordion-icon'); if (otherIcon) otherIcon.textContent = '+'; } }); } // Toggle current accordion (browser might do this automatically, check if needed) // accordion.toggleAttribute('open'); // Only if preventing default // Update icon for the clicked accordion const icon = summary.querySelector('.accordion-icon'); if (icon) { // Check state *after* potential toggle icon.textContent = accordion.hasAttribute('open') ? '-' : '+'; } }); // Set initial icon state for pre-opened accordions const icon = summary.querySelector('.accordion-icon'); if (icon) { icon.textContent = accordion.hasAttribute('open') ? '-' : '+'; } } }); }); </script> <style> .product-price, .sold-out-badge, .product-badge, .option-name, .accordion-content { font-family: "NHaasGroteskTXPro"; } .product-accordion summary h3{ font-family: "SweetSansProBold"; font-size: 12px; } .product-detail-section { max-width: 1350px; margin: 0 auto 50px; padding: 0 20px; } .product-detail-wrapper { display: grid; grid-template-columns: repeat(12, 1fr); gap: 40px; align-items: start; } /* Media Column Styles */ .product-media-column { position: relative; grid-column: 1 / 7; } /* Info Column Styles */ .product-info-column { padding-top: 20px; grid-column: 8 / 13; /* Usando 13 para asegurar que llegue hasta el final (12 inclusive) */ } .product-images { position: relative; padding-bottom: 100%; overflow: hidden; background: #f7f7f7; } .product-image, .product-video, .product-model { position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover; opacity: 0; transition: opacity 0.3s; pointer-events: none; } .product-image.active, .product-video.active, .product-model.active { opacity: 1; pointer-events: auto; } .no-image-placeholder { position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; background: #f8f8f8; color: #999; font-size: 12px; font-weight: bold; } .product-thumbnails { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 15px; } .thumbnail-button { width: 70px; height: 70px; padding: 0; border: 1px solid #e8e8e8; background: none; cursor: pointer; overflow: hidden; } .thumbnail-button.active { border-color: #000; } .thumbnail-button img { width: 100%; height: 100%; object-fit: cover; } .product-badge { font-size: 12px; margin-bottom: 10px; color: #616161; } .product-title { font-size: 24px; font-weight: 400; margin-bottom: 0px; line-height: 1.4; letter-spacing: 0.75px; text-transform: uppercase; font-family: "SweetSansProRegular"; } .product-price { font-size: 16px; color: #000; margin-bottom: 15px; letter-spacing: 0.75px; } /* Reviews */ .product-reviews { display: flex; align-items: center; margin-bottom: 20px; } .star-rating { display: flex; } .star { color: #d8d8d8; font-size: 16px; } .star.filled { color: #000; } .review-count { margin-left: 5px; color: #616161; text-decoration: underline; } /* Variant Selectors */ .product-variants { margin-bottom: 20px; } .variant-option { margin-bottom: 20px; display: flex; gap: 5rem; } .option-name { font-size: 12px; margin-bottom: 10px; font-weight: 400; min-width: 50px; } .color-options { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; } .color-option { width: 24px; height: 24px; border-radius: 50%; border: 2px solid #8d8d8d; cursor: pointer; transition: transform 0.2s, box-shadow 0.2s; padding: 0; background-clip: content-box; } .color-option[style*="background-color: white"] { border-color: #ccc; } .color-option:hover { transform: scale(1.1); } .color-option.active { box-shadow: 0 0 0 1px white, 0 0 0 2px #000; } .size-options { display: flex; flex-wrap: wrap; gap: 10px; } .size-option { min-width: 40px; height: 40px; padding: 0 15px; display: flex; align-items: center; justify-content: center; border: 1px solid #e8e8e8; background: white; cursor: pointer; font-size: 12px; transition: all 0.2s; } .size-option:hover { border-color: #999; } .size-option.active { border-color: #000; background: #000; color: white; } /* Add to Cart */ .product-actions { margin-bottom: 30px; } .add-to-cart-button { width: 100%; padding: 12px; cursor: pointer; transition: all 0.3s; font-weight: 400; text-align: center; background-color: #000; color: #fff; border: 1px solid #000; font-family: "SweetSansProBold"; } .add-to-cart-button:hover:not([disabled]) { background-color: #333; } .add-to-cart-button[disabled] { background-color: #f8f8f8; border-color: #e8e8e8; color: #999; cursor: not-allowed; } /* Accordions */ .product-accordions { border-top: 1px solid #e8e8e8; } .product-accordion { border-bottom: 1px solid #e8e8e8; } .product-accordion summary { display: flex; justify-content: space-between; align-items: center; padding: 15px 0; cursor: pointer; list-style: none; } .product-accordion summary h3 { font-size: 12px; font-weight: 400; margin: 0; } .product-accordion .accordion-icon { font-size: 16px; transition: transform 0.3s; } .product-accordion[open] .accordion-icon { transform: rotate(45deg); } .accordion-content { padding: 0 0 20px 0; font-size: 12px; line-height: 1.6; } /* Responsive */ @media (max-width: 991px) { .product-detail-wrapper { gap: 30px; } .product-media-column { grid-column: 1 / 7; } .product-info-column { grid-column: 7 / 13; } } @media (max-width: 767px) { .product-detail-wrapper { grid-template-columns: 1fr; } .product-media-column, .product-info-column { grid-column: 1 / -1; } .product-media-column { margin-bottom: 20px; } } </style>
It's tough to just see the snippet without actually seeing it in action. Collaboration would be a better option.
You could may be double check if you have the right images on your variants in the admin backend
To start with, I see the problem with this pat of the code:
{% for option in product.options_with_values %}
{% if option.name == 'Color' or option.name == 'Colour' %}
<div class="variant-option">
<h3 class="option-name">
{{ option.name }}: <span class="selected-color-name"></span>
</h3>
<div class="color-options">
{% assign unique_values = option.values | uniq %}
{% for value in unique_values %}
{% assign color_code = value | handleize %}
{% assign has_image = false %}
{% assign variant_id = "" %}
{% assign media_id = "" %}
{% for variant in product.variants %}
{% if variant.options[forloop.parentloop.index0] == value %}
We have 3 nested loops:
Then, a condition is checked:
{% if variant.options[forloop.parentloop.index0] == value %}
The forloop.parentloop.index0 refers to the index in the second loop, but it should refer to the index in the first loop!
So the code should be modified like this:
{% for option in product.options_with_values %}
{% if option.name == 'Color' or option.name == 'Colour' %}
{% assign option_index = forloop.index0 %}
<div class="variant-option">
<h3 class="option-name">
{{ option.name }}: <span class="selected-color-name"></span>
</h3>
<div class="color-options">
{% assign unique_values = option.values | uniq %}
{% for value in unique_values %}
{% assign color_code = value | handleize %}
{% assign has_image = false %}
{% assign variant_id = "" %}
{% assign media_id = "" %}
{% for variant in product.variants %}
{% if variant.options[option_index] == value %}
I've added a variable option_index and use this variable instead.
There may be other problems...
June brought summer energy to our community. Members jumped in with solutions, clicked ...
By JasonH Jun 5, 2025Learn how to build powerful custom workflows in Shopify Flow with expert guidance from ...
By Jacqui May 7, 2025Did You Know? May is named after Maia, the Roman goddess of growth and flourishing! ...
By JasonH May 2, 2025