Featured-Collection Product Card With Metafield Hero Media and a Tag-Driven Badge Stack
The card that renders inside the homepage's main merchandising rail: hero and hover art chosen in the Shopify admin, a responsive image ladder built by hand, and a badge stack resolved from tags, price and block settings at render time.
Shopify gives a product one featured image, and it is the same image everywhere the product appears. This card takes its hero and hover layers from two product metafields instead, wraps them in a width-guarded WebP ladder, and resolves one exclusive price badge plus three independent flags from product tags, compare-at price and block settings — all in Liquid, before the page paints.
The Problem
A product has one featured image in Shopify, and every surface that renders a card gets that same image. The catalogue shot that works on a product listing page — pack front, white background, cropped tight — is usually the wrong hero for the homepage, where the same product is being sold as a campaign rather than as a row in a grid.
The homepage rail is also where promotional language lives. Merchandisers wanted a save badge, a free-shipping flag, a new flag, a best-seller flag and an award flag, plus a short line of card copy that is not the first paragraph of the product description. None of that is a field Shopify hands a card renderer. Every one of them has to be derived from something else the store already knows.
The Constraint
There is no per-surface featured image in Shopify. An override has to come from somewhere the merchandising team can already edit, which made it a product metafield holding a file reference — and a file reference resolves to either an image or a video, so the card has to be able to render both from the same setting.
The theme's shared card snippet was the other option, and it was already carrying a lot on the collection page. A second card renderer was cheaper to reason about than another branch inside the shared one, and that decision has a price we pay on every change.
The badge rules mix three sources — product tags, section settings and per-block overrides — and Liquid has to settle all three at render time. There is no client-side pass to fix a badge afterwards, so the precedence between them has to be a rule in the template rather than a convention in someone's head.
What We Built
de_fc_product_card.liquid is the card. It is rendered per slide by de_featured_collection.liquid, the merchandising section documented on its own page; this page is about what happens inside one card.
Media resolution. The primary layer prefers product.metafields.custom.feature_img. The hover layer prefers product.metafields.custom.feature_hover_img and falls back to product.media[1]. The hover art is a second stacked element rather than a src swap on the first, so the transition is a CSS one and nothing is fetched at the moment a shopper's cursor arrives. Where the hover metafield holds a video rather than an image, the card branches to the video path documented on the card hover video page.
Responsive emission. Both layers get an explicit seven-step WebP srcset, and every candidate is guarded by a width check against the source, so a small uploaded image never gets a candidate larger than itself to upscale into. The sizes expression is computed from settings.page_width, so the card's declared width tracks the theme's own layout setting instead of a hard-coded breakpoint table. A lazy_load: false argument flips loading to eager for the first row of cards, so the image most likely to be the largest thing on screen is not queued behind lazy loading.
Badge resolution. This is the part with real rules in it, and it resolves in two independent lanes.
A VALUE tag switches the price badge from a percentage to the money saved, which is the shape a bundle sells better in.
Copy and commerce. Card copy comes from an optional block-level description_override, and otherwise from product.description, stripped of markup and truncated to twelve words. A Yotpo review widget mount sits under the title. The add control is a per-product <product-form> posting the first available variant, and an unavailable product renders a link to the product page instead of a form.
Why This Way
Reading the hero from a metafield keeps a merchandising decision in the admin, where the person making it works. Swapping a homepage hero image becomes a change to one product record rather than a change to a theme file, and the person who does it does not need a deploy or a developer.
Stacking the hover layer instead of swapping src is the cheaper mechanic on the hardware that matters. The second image is already in the document, so the hover is a CSS transition rather than a request that starts when the cursor arrives and finishes after it has left.
Two things got worse in the trade. There is now a second card renderer in the theme, so any change to card behaviour has to be considered twice — once here, once in the shared card. And the badge precedence is invisible where it is used: a merchandiser who types a custom badge label has silently switched off the computed save badge for that block, and nothing in the theme editor says so. The rule is right, and it is only documented in the template that enforces it.
Why Not an App
Product-label and merchandising-card apps do this job, and they work the same way: their script reads the grid after the page has painted and rewrites the cards with their own markup.
Two things follow that did not suit a homepage rail. The card would arrive twice — once as the theme rendered it, once as the app re-rendered it — on the highest-traffic row on the site. And the app's badge sits outside the card's own markup, so it cannot participate in the decisions the card is already making about srcset, layout and eager loading. Server-rendered inside the section, this card is in the initial HTML, which is also what the section-rendering path returns when the rail is re-fetched.
Implementation Notes
- Badge precedence is explicit rather than emergent: a block-level
badge_discount_textsuppresses the computed save badge entirely, so the two can never render together on one card. cheapest_variant, resolved asproduct.variants | sort: 'price' | first, is checked for a real compare-at price before a save badge is allowed to render at all.- The percentage saving applies
times: 100.0beforedivided_by. Liquid divides integers into integers, so without the float first a genuine nineteen-percent discount floors to zero and the badge silently never renders. - Each layer emits a seven-step WebP
srcsetwhose candidates are individually guarded by a width check, so a small source image is never given a larger candidate to upscale into. - The
sizesexpression is derived fromsettings.page_width, so the card's declared width follows the theme's own layout setting instead of a hard-coded breakpoint table that would drift the first time the grid changed. - A
lazy_load: falseargument flipsloadingto eager for the first row of cards, keeping the largest above-the-fold image out of the lazy queue rather than treating every card in the rail identically. - Tag matching is case-normalised for the automatic flags, so
bestseller,best-sellerandbest sellerall resolve to the same badge and a merchandiser's spelling does not decide whether it appears.
Edge Cases
- An unavailable product renders a "Sold out" link to the product page rather than a disabled form, so there is no submit control that cannot succeed.
- A product with no media at all falls back to a placeholder SVG, so a rail with an unphotographed product in it still lays out correctly.
- The hover swap can be turned off for a whole section with
show_image_hover_swap, which is how a rail of packshots avoids a hover state it has no second image for. - Card copy falls back through the block override, then the product description, then nothing — an empty description renders no element rather than an empty line.
Platform Primitives Used
metafields—custom.feature_imgandcustom.feature_hover_imgcarry the per-surface art direction that Shopify's single featured image cannot express.liquid-schema-settings— the badge toggles, the description override and the hover switch are all block and section settings resolved in Liquid.cart-ajax-api— the card's<product-form>posts through the theme's own AJAX cart rather than a second cart implementation.
Integrations in Play
- Yotpo — the card renders the review widget's mount point in the card body and leaves the star rendering to Yotpo's own script. It coexists with the card; nothing about the rating is computed in Liquid.
Where It Runs
The home page today, through the featured-collection section, on one template: index. The card is also registered against the collection listing surface, because the badge and media rules were written to be surface-independent. Every card in the rail renders from this one snippet, so a change to badge behaviour lands on the whole rail at once.
What This Demonstrates
- Art-directed hero and banner system — the primary capability. Art direction at card scale: the image a product shows in a campaign slot is chosen for that slot, not inherited from the catalogue.
- Metafield-driven PDP content blocks — the same storage decision applied to merchandising rather than to product copy: the override lives on the product record, edited beside the product it describes.
- Video and shoppable media modules — a file-reference metafield can resolve to a video, so the card's second layer is a media branch rather than an image slot.
How We Know
One Liquid snippet read from the theme, roughly 325 lines, plus a metafield export that documents the custom.feature_img and custom.feature_hover_img pair, and client working-session records from spring 2026. The records establish the requirement: a single standardised card component, used in both cross-sell placements, carrying title, star rating, pricing, description and view and add controls, with price sitting directly under the title and rating. This card is that component. The badge precedence and the image ladder are code-side decisions, read from the snippet rather than from any document.
Related Builds
- Merchandisable homepage product carousel with a tag-driven badge engine and inline add-to-bag — the section that mounts this card. That page covers the rail, its settings and its script; this one covers what one card does with the product it is handed.
- Product-card hover video and a reusable deferred-media playback refactor — the same storefront's answer to the video branch of the hover metafield, on the shared card rather than this one.
- Product card with award badges, per-variant content payload and per-card structured data — a different storefront solving the same problem with a different storage decision. The cross-client comparison is the useful part.
The Buy-vs-Build Question
A labels app buys the badge vocabulary for a monthly fee and a post-paint repaint. Building it bought badges in the initial HTML and a merchandising override that lives on the product record, and cost a second card renderer to keep in step with the shared one. Where product data should live, and when a metafield is the right home for it rather than an app's database, is the question on the metafields and metaobjects architecture decision page.
Provenance & Evidence
- Client: LUS Brands — loveurcurls.com
- Surfaces: Home page, collection listing
- Templates served: one — the
indextemplate - Complexity: High
- Attribution: Deploi-authored. The card snippet is ours. It runs in a theme built on Dawn 15.4.1, and its add control deliberately posts through the theme's own
<product-form>and AJAX cart rather than a cart of ours. - Status: Live, verified 2026-09-06
- Evidence: One Liquid snippet from the theme, a metafield export, and three client working-session records
- Confidence: Strong — the code and the client record describe the same component
- Primary capability: Art-directed hero and banner system
Ready to Choose the Image Your Homepage Shows?
You dream it. We build it. If your best merchandising slot is stuck with whatever image the catalogue picked, Contact us today and we'll show you where that decision could live instead.