Description
- hide sold out products
- get new item and fill in related products
(Before – Related with 4 Sold out products)

(After using code)

Install Code
Use code to Store Page Header Injection
<!-- @tuanphan - Related Products - Sold Out -->
<script>
(function() {
const cache = new Map();
const imageCache = new Map();
async function fetchJSON(url) {
try {
const res = await fetch(url);
return res.ok ? await res.json() : null;
} catch (e) {
console.error('Fetch error:', url, e);
return null;
}
}
async function getCategoryProducts(categoryUrl) {
if (cache.has(categoryUrl)) {
console.log('Using cached products for:', categoryUrl);
return cache.get(categoryUrl);
}
console.log('Fetching products from:', categoryUrl);
const data = await fetchJSON(categoryUrl + '?format=json');
const products = data?.items || [];
console.log('Found products:', products.length);
cache.set(categoryUrl, products);
return products;
}
async function getProductImage(product) {
// Check cache first
if (imageCache.has(product.id)) {
return imageCache.get(product.id);
}
// Try assetUrl first
let img = product.assetUrl;
// If assetUrl is invalid (ends with just a number/), fetch individual product
if (!img || /\/\d+\/?$/.test(img)) {
console.log('Invalid assetUrl for', product.title, ', fetching product detail...');
const productData = await fetchJSON(product.fullUrl + '?format=json');
img = productData?.item?.items?.[0]?.assetUrl ||
productData?.item?.items?.[0]?.mediaFocalPoint?.sourceUrl ||
'';
}
imageCache.set(product.id, img);
return img;
}
function isSoldOut(el) {
const status = el.querySelector('.product-list-item-status');
return status && /sold.?out/i.test(status.textContent);
}
function isAvailable(product) {
const variants = product.structuredContent?.variants;
if (!variants) {
console.log('Product has no variants, treating as available:', product.id);
return true;
}
const available = variants.some(v =>
v.unlimited || v.qtyInStock === undefined || v.qtyInStock > 0
);
console.log(`Product ${product.id} availability:`, available,
'variants:', variants.map(v => ({unlimited: v.unlimited, qty: v.qtyInStock})));
return available;
}
function getProductId(el) {
return el.getAttribute('data-product-id') ||
el.querySelector('.product-list-item-link')?.href?.match(/\/p\/([^\/\?]+)/)?.[1];
}
async function createProductHTML(product) {
const url = product.fullUrl;
const title = product.title || '';
const img = await getProductImage(product);
console.log(`Image for "${title}":`, img || 'NO IMAGE');
const variant = product.structuredContent?.variants?.[0];
const price = variant?.priceMoney ? `$${(variant.priceMoney.value / 100).toFixed(2)}` : '';
return `
<div class="product-list-item is-loaded" data-product-id="${product.id}">
<a class="product-list-item-link" href="${url}" aria-label="${title}">
<div class="product-list-image-wrapper">
<figure class="product-list-item-image" data-animation-role="image">
<div class="grid-image-wrapper">
${img ? `<img src="${img}" alt="${title}" class="grid-item-image grid-image-cover loaded" style="object-position: 50% 50%; display: block;" loading="lazy">` : ''}
</div>
</figure>
</div>
<section class="product-list-item-meta" data-animation-role="content">
<div class="product-list-title-price">
<div class="product-list-item-title">${title}</div>
${price ? `<div class="product-list-item-price">${price}</div>` : ''}
</div>
<div class="product-list-item-status"></div>
</section>
</a>
</div>
`;
}
async function replace() {
console.log('=== STARTING REPLACE FUNCTION ===');
const container = document.querySelector('.product-related-products .product-list-container');
if (!container) {
console.log('No related products container found');
return;
}
console.log('Found related products container');
const data = await fetchJSON(location.pathname + '?format=json');
if (!data?.item) {
console.log('No product data found');
return;
}
console.log('Current product ID:', data.item.id);
const categories = data.nestedCategories?.itemCategories || [];
console.log('Product categories:', categories.map(c => c.displayName).join(', '));
if (!categories.length) {
console.log('No categories found');
return;
}
const items = Array.from(container.querySelectorAll('.product-list-item'));
console.log('Total related products:', items.length);
// Build image cache from existing items
items.forEach(el => {
const id = getProductId(el);
const img = el.querySelector('img')?.src;
if (id && img) {
imageCache.set(id, img);
}
});
const soldOut = items.filter(isSoldOut);
console.log('Sold out products:', soldOut.length);
soldOut.forEach((el, i) => {
const id = getProductId(el);
const title = el.querySelector('.product-list-item-title')?.textContent;
console.log(` ${i+1}. Sold out: ${title} (ID: ${id})`);
});
if (!soldOut.length) {
console.log('No sold out products to replace');
return;
}
const excludeIds = new Set([
data.item.id,
...items.map(getProductId).filter(Boolean)
]);
console.log('Excluded IDs:', Array.from(excludeIds));
const available = [];
for (const cat of categories) {
console.log(`\nSearching category: ${cat.displayName} (${cat.fullUrl})`);
const products = await getCategoryProducts(cat.fullUrl);
for (const p of products) {
if (excludeIds.has(p.id)) {
console.log(` Skipping (already excluded): ${p.id}`);
continue;
}
if (!isAvailable(p)) {
console.log(` Skipping (not available): ${p.id}`);
continue;
}
console.log(` ✓ Adding available product: ${p.id} - ${p.title}`);
excludeIds.add(p.id);
available.push(p);
if (available.length >= soldOut.length) {
console.log(` Reached target (${soldOut.length} products needed)`);
break;
}
}
if (available.length >= soldOut.length) break;
}
console.log('\n=== REPLACEMENT SUMMARY ===');
console.log('Sold out items to replace:', soldOut.length);
console.log('Available products found:', available.length);
for (let i = 0; i < soldOut.length; i++) {
const el = soldOut[i];
if (available[i]) {
const oldTitle = el.querySelector('.product-list-item-title')?.textContent;
const newTitle = available[i].title;
console.log(`Replacing: "${oldTitle}" → "${newTitle}"`);
const newHTML = await createProductHTML(available[i]);
console.log('Generated HTML length:', newHTML.length);
const temp = document.createElement('div');
temp.innerHTML = newHTML.trim();
const newElement = temp.firstElementChild;
if (newElement) {
el.replaceWith(newElement);
console.log('✓ Successfully replaced element');
} else {
console.error('Failed to create new element');
}
} else {
console.log(`No replacement available for position ${i+1}`);
}
}
console.log('=== REPLACE FUNCTION COMPLETE ===\n');
}
function init() {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', replace);
} else {
replace();
}
if (window.Squarespace?.onInitialize) {
window.Squarespace.onInitialize(Y, () => Y.on('mercury:load', replace));
}
}
init();
})();
</script>
