Why a shared image holds products back
The featured image is the product wherever the store shows a grid: the shop page, category archives, related products, search results, the cart, and the product feed your ads read from. The gallery is where a shopper goes to be sure. Shoppers scan the pictures, not the titles. When two products carry the same picture, the grid stops telling them anything.
- Listings look like duplicates. The same photo four times in a row reads as a mistake or a lazy import, and shoppers skip past all four rather than open each one to find the difference.
- Feeds and marketplaces notice. Google Merchant Center and marketplace channels compare image links across items; several products pointing at one file get treated as near-identical listings, and the whole feed's quality suffers.
- A borrowed gallery photo misleads just as well. The shopper who opens the gallery to check the fabric is looking at another product's fabric, and the return says the picture was wrong.
- Returns go up. A shopper who bought the blue variant off a photo of the grey one sends it back — and the review says the picture was wrong, not that they clicked the wrong item.
- Placeholders quietly become permanent. A supplier import that sets one stock photo on a whole line looks finished in wp-admin, so nobody goes back to shoot the real products.
How to find shared product images manually
WooCommerce keeps the featured image as an attachment ID under the
_thumbnail_id meta key and the gallery as a comma-separated
list under _product_image_gallery. One query has to read
both and then count the distinct products behind each attachment:
wp db query "SELECT attachment_id, COUNT(DISTINCT product_id) AS products,
GROUP_CONCAT(DISTINCT product_id ORDER BY product_id) AS product_ids
FROM (
SELECT m.post_id AS product_id, m.meta_value AS attachment_id
FROM wp_postmeta m
JOIN wp_posts p ON p.ID = m.post_id
WHERE m.meta_key = '_thumbnail_id' AND m.meta_value <> ''
AND p.post_type = 'product' AND p.post_status = 'publish'
UNION ALL
SELECT m.post_id, TRIM(j.value)
FROM wp_postmeta m
JOIN wp_posts p ON p.ID = m.post_id
JOIN JSON_TABLE(
CONCAT('[\"', REPLACE(m.meta_value, ',', '\",\"'), '\"]'),
'$[*]' COLUMNS (value VARCHAR(20) PATH '$')
) j
WHERE m.meta_key = '_product_image_gallery' AND m.meta_value <> ''
AND p.post_type = 'product' AND p.post_status = 'publish'
) usages
GROUP BY attachment_id
HAVING products > 1
ORDER BY products DESC;"
Adjust the table prefix if yours isn't wp_. The gallery
half needs MySQL 8 for JSON_TABLE; on MariaDB the list has
to be split in application code instead.
This works, with gaps. It hands you attachment numbers, not products you can open, so every row means another lookup. It does not say which product shows the photo where, so it cannot tell you whether you are looking at two listings wearing one face or a gallery that borrowed a picture. It ignores whether the attachment still exists, so a deleted file that a dozen products still point at shows up as a shared image rather than a missing one. And it answers once: the next supplier import can set the same stock photo on thirty new products, and nobody runs the query again.
How CatalogLift finds it
Every scan reads the images of each published product in the synced catalog — the main image and the gallery together — and groups them by the attachment they point at. An attachment more than one distinct published product shows produces a finding on each of those products. A product showing the shared photo as its main image is rated medium; one that only carries it in a gallery is rated low, because a borrowed gallery picture misleads later in the journey than the face of a listing. A product that shows the same photo in both places gets one finding naming both, not two findings — the same image twice inside one product is the repeated gallery image check's business, and it stays there.
Only published products count, on both sides of the collision, so a draft copy that kept the original's images never earns the original a finding. An image the store no longer has as a live attachment is left to the missing-image checks rather than reported here.
Each finding carries evidence: the image itself, where this product shows it, how many products stand behind it, and a sample of the others by name and by the place each one shows it. Because the count is taken across the whole store even on a targeted re-scan, a shared image is never reported as resolved just because its twin fell outside the products being rechecked. And when one product gets a photo of its own, the findings on the products that never moved close as a group collapse — nobody is handed a receipt for work they did not do.
This is a detection-and-evidence check. A photo of the product itself is the one thing CatalogLift cannot write for you, so the change is made in WooCommerce: open the product, go to the box the case names — Product image or Product gallery — put the right photo there, save. This is a hand-off: CatalogLift cannot make this change for you and neither can an agent, so nothing in your store moves until somebody uploads that photo. The next scan confirms the finding is gone. Because CatalogLift keeps watching, an import that reuses one photo across a new line becomes a fresh finding instead of a silent regression.