Shopify Builds>Nudestix>Product card with award badges, per-variant content payload and per-card structured data

Product Card With Award Badges, Per-Variant Content Payload and Per-Card Structured Data

One snippet renders the whole Nudestix grid card: tag and award badges, quick view, wishlist, comparison, an in-grid add to bag, and a per-variant data payload the shade switcher reads without a fetch.

Dawn's product card shows an image, a title and a price. A color-cosmetics grid needs shade-level content, award art, reviews, a wishlist and an add to bag that never leaves the page. We extended Dawn's card-product.liquid into a card that serializes every variant's copy into the DOM at render time, so the shade switcher and quick view repaint instantly with no Storefront API token.

Fact Strip

  • Client: Nudestix — color cosmetics DTC, nudestix.com
  • Surfaces: Product listing pages, search, home
  • Templates served: Three — collection, search and index, plus bundle-builder and mega-menu call sites
  • Complexity: High — roughly 850 lines in a single snippet
  • Attribution — Deploi-modified vendor: the storefront runs Dawn 6.0.2, and card-product.liquid is Dawn's own card snippet. The card's skeleton, its image conventions and its product-form scaffolding are Dawn's. The badge engine, the variant payload, the comparison gate, the quick-view path and the structured data are our modification.
  • Status: Live — verified 2026-09-06
  • Evidence: Moderate confidence — documented from the code
  • Platform primitives: 5

The Problem

Dawn's product card is deliberately generic: an image, a title, a price, a badge for sale or sold out. For a theme that has to work for every catalog, that's the right default. For color cosmetics it's the wrong card. On a shade-driven grid the card carries most of the merchandising — which shade is showing, whether the product won an award, whether it's trending, the subtitle under the name, the review stars, a wishlist action, and an add to bag that works without leaving the grid. The shade-level content a quick view needs is spread across variant metafields, so the card can't show any of it without first going and getting it.

The Constraint

A snippet has no schema of its own. Sections get a {% schema %} block and merchandiser-editable settings; a snippet gets whatever its caller passes. So every behavior in this card has to be driven by render arguments, product and variant metafields, shop metaobjects, or global theme settings. There's no fourth option.

The same card renders on collection, search, bundle and mega-menu surfaces, and sometimes twice for one product on a page — once in the grid, once in a promo rail. Every id it emits has to be collision-proof, because two forms sharing an id break add to cart quietly rather than visibly. You find that out from the revenue, not the console.

And variant-level content can't be fetched client-side without a Storefront API token. For the card to have shade copy on hand the instant someone clicks a swatch, that copy has to already be in the DOM.

What We Built

card-product.liquid is Dawn's card snippet, extended into roughly 850 lines.

Badges. A marketing badge derived from tag_-prefixed product tags, with BEST SELLER normalized to BESTSELLER so two spellings in the admin don't yield two badges — and the tag badge suppressed entirely when an award badge is present, because the award outranks the marketing. Two award slots sit alongside it: a theme-asset image driven by the variant metafield custom.allure_badge, and an image-metafield-driven second slot at custom.allure_badge_2. That second slot ships a transparent GIF as its placeholder, so JavaScript can swap the source on a shade change without the card reflowing.

Naming and copy. custom.custom_display_name overrides the product title where merchandising wants a shorter name in the grid. The subtitle comes from my_fields.subtext_lines. The PowerReviews listing-page rating snippet renders inline, and a Wishlist Hero button sits in the card corner.

Actions. Quick view is a modal-opener paired with a <quick-add-modal> that fetches the product with ?view=quickview, or ?view=quickviewbundle inside the bundle builder. Add to bag is a real product-form. Three alternative button labels — grid, cart page, PDP — are all emitted and toggled by CSS per surface, alongside an ADD TO PALETTE mode for the palette builder.

Comparison. The comparison checkbox appears only on collection templates, and only where a limit is configured: the card reads collection.metafields.custom.comparison first and falls back to shop.metaobjects.comparison.default. No configured limit, no checkbox.

The payload. This is the part that makes the card unusual. A hidden input per variant carries data-meta-title, data-meta-description, data-meta-ingredient, data-meta-how-to, data-meta-details, data-title, data-orgmeta-description and both award flags. The add-to-cart form's own hidden input carries varTitle, varDesc, varName, varSku, varPrice and varFeatureImg for the analytics and cross-sell layers. The shade switcher and the quick view read those attributes and repaint card and modal with no network call.

Structured data. Each card can emit its own Product JSON-LD with one Offer per variant, and a gtin12, gtin13 or gtin14 chosen by the length of the barcode rather than assumed from the field it sits in.

Why This Way

Serializing per-variant content into hidden inputs is what makes shade switching and quick view instant. No Storefront API token, no fetch on click, no loading state to design — the card is a small data document as well as markup. Ids are salted with a randomNumber derived from 'now' | date: '%N', because the same product legitimately appears more than once on a page. Award badges are always present and display-toggled rather than built on demand, so the switcher flips a style instead of constructing DOM.

Two of the costs are ordinary. The card carries data it never displays, so every new piece of shade-level content is another attribute here and another metafield to keep in sync. And because a snippet has no schema, the contract between card and call site is convention rather than something the theme editor enforces — a missing argument fails quietly.

The third is the one this page owes you plainly, because it is a page about structured data. A full Product graph per card means an Offer per variant per card, so a two-dozen-card grid of twelve-shade products ships several hundred offers on one page. Repetition is handled — call sites that render a card twice pass show_seo: false — but volume is a different question from duplication, and we have not had a second opinion on it against Google's guidance for repeated Product markup in listings. We think the per-card graph is right for a shade-led catalog. We would not tell you it is settled.

Implementation Notes

  • section_id is accepted as a render argument and falls back to section.id, so the card still works when it's called from a snippet that has no section context at all.
  • The card image prefers the selected variant's featured image over the product's, so a collection that links to a specific shade opens showing that shade rather than the default.
  • disable_comparison is read from a product metaobject reference at custom.product.value.disable_comparison, which lets a single product opt out of comparison without touching the collection's limit.
  • A <noscript> variant <select> is retained, so the card degrades to a plain, submittable form when scripts don't run.
  • Sold-out cards hide the quick-view button entirely and render a disabled sold-out submit instead, rather than opening a modal that can't complete.
  • A roughly 200-line in-card swatch rail — settings-driven hex color list, +N MORE overflow link and all — sits commented out in the snippet. The swatch picker now lives elsewhere on the storefront.
  • The empty and placeholder branch of the snippet still emits the analytics data attributes, so onboarding and skeleton cards don't throw when the analytics layer reads them.

Edge Cases

  • Products with only a default variant skip the option select entirely and fall back to the product title for varName, so single-variant products don't emit an empty option.
  • The sale badge renders only when compare_at_price > price and the product is available; the sold-out badge takes precedence over it in every case.
  • The secondary hover image is emitted only when media[1] actually exists and show_secondary_image was passed, so single-image products don't get a hover flicker.
  • Comparison UI is suppressed entirely when no comparison metaobject limit is configured — the feature disappears rather than rendering a control that can't work.
  • show_seo can be passed false to suppress the per-card JSON-LD wherever a card is deliberately repeated on a page.
  • Bundle mode replaces the card link with a non-navigating wrapper, so clicking a card adds it to the palette instead of leaving the builder mid-build.

Platform Primitives Used

  • Metafields — product and variant fields supply the display-name override, the subtitle, both award badges and the per-variant payload.
  • Metaobjects — a comparison metaobject holds the compare limit, read from the collection first and the shop default second.
  • Cart AJAX API — the in-grid product-form posts to the cart without a page load. That is the whole of "add to bag from the grid."
  • Liquid section schema and settings — global theme settings supply the toggles a schemaless snippet can't own itself.
  • Structured data — a per-card Product graph with an Offer per variant, opt-out per call site.

Integrations in Play

  • PowerReviews — the card mounts the vendor's listing-page rating snippet inline, in the card's own layout rather than beside it.
  • Wishlist Hero — the wishlist control coexists with the card's own actions; we mount the vendor button and style it into the card, and the vendor owns the list behind it.

Where It Runs

Three templates mount the card: collection, search and index. It's also called from the bundle builder and the mega menu, so the bundle and palette modes live inside a snippet that is nominally a grid card. On a busy collection page the same snippet renders many times over, and occasionally twice for one product.

What This Demonstrates

Two capabilities meet in this file. It's filed primarily under structured data for AI answers, because the card emits an entity graph per product rather than leaving product markup to the PDP. It's also a variant-aware content and media build: the payload exists so a shade change is a repaint, not a request.

How We Know

One snippet, roughly 850 lines, read directly from the live theme, along with the metafield and metaobject references it depends on. Documented from the code; no client-side record of the decision survives. There's no working-session record behind this build — no brief, no note explaining why the payload was chosen over a fetch. What we can say is what the file does. So this page describes mechanisms and declines to describe intent it can't evidence.

Related Builds

The Buy-vs-Build Question

Every SEO app will add Product markup to a product page. Few will put a per-variant entity graph on a grid card that has to survive being rendered twice, and none of them know your barcode length decides whether it's a gtin12 or a gtin13. We've written up where that line sits in Structured data and schema markup: buy or build?

Ready to Make Your Grid Do More?

The product card is the most-rendered piece of markup on most storefronts and usually the least-considered one. If yours is still showing an image, a title and a price, there's room in it. Contact us today to talk about what yours could carry.

More builds