Why an empty variable product costs you sales
WooCommerce derives everything a shopper needs from the variations, not from the parent. The price range, the stock status, the option dropdowns and the add-to-cart button all come from the published variations underneath. Take them away and the parent has nothing to show — but it is still published, still indexed, still linked from category pages and feeds.
- The page cannot convert. The storefront shows the title, the description and a notice that the product is unavailable. Whatever brought the shopper here — a search result, a category grid, an ad — ends on a dead page.
- Category and search listings look broken. A variable product without variations has no price to display, so it sits in the grid as an item with a blank where every neighbour shows a number, and sorting by price pushes it to the wrong end.
- Feeds and rich results reject it. Product feeds and structured data need a price and an availability; a parent with no variations supplies neither, so the item is dropped or flagged in tools that read your catalog.
- It usually means an import went wrong. A CSV import that created the parents but not their children, a variation set that was trashed during a cleanup, or a product switched to variable before anyone built the options — all leave the same shell behind, and nobody notices until an order does not arrive.
How to find variable products without variations manually
Variations are child posts of type product_variation
whose post_parent points at the variable product. With
WP-CLI, one query lists every published variable product with no
published child:
wp db query "SELECT p.ID, p.post_title FROM wp_posts p JOIN wp_term_relationships tr ON tr.object_id = p.ID JOIN wp_term_taxonomy tt ON tt.term_taxonomy_id = tr.term_taxonomy_id AND tt.taxonomy = 'product_type' JOIN wp_terms t ON t.term_id = tt.term_id AND t.slug = 'variable' LEFT JOIN wp_posts v ON v.post_parent = p.ID AND v.post_type = 'product_variation' AND v.post_status = 'publish' WHERE p.post_type = 'product' AND p.post_status = 'publish' GROUP BY p.ID, p.post_title HAVING COUNT(v.ID) = 0 ORDER BY p.ID;"
Adjust the table prefix if yours isn't wp_. The
product_type taxonomy is what marks a product as
variable, so the join through the term tables is required.
The query is accurate on the day you run it, and that is its limit. It gives you IDs, so each product still has to be opened by hand to see why it is empty — no children at all, or children that were left as drafts. And it does not stay on watch: the next CSV import or the next bulk trash can recreate the same shells a week later, and nothing tells you until a shopper does.
How CatalogLift finds it
Every scan reads the synced catalog and picks out published products of the variable type that have no published variation attached — a variation that is a draft, trashed, or no longer present in the store does not count. Each affected product becomes one finding, raised at critical severity because it blocks the sale outright, with the product itself as evidence and the reason spelled out: a published variable product with no published variations.
This is a detection-and-evidence check. Variations are catalog structure, and structure is built in your store, so CatalogLift never creates them and never writes to WooCommerce for this finding. Instead it hands you the exact path: open the product, tick "Used for variations" on the attributes shoppers choose from, generate the variations under the Variations tab, give each one a price, and update. When you are done, the next scan confirms the finding is gone — and because CatalogLift keeps watching, an import that empties a variable product again shows up as a fresh finding rather than a silent regression. The scan is free, and nothing in your store changes unless you change it.