Cards keep their design and wording. Where a card needs a product detail, its text names the product field, and the page fills in that product's value.
In a card's Card Data field, a product field's name between three colons on each side is a placeholder. When the page shows the card, it replaces the placeholder with that product's value for the field. Card Data without placeholders works exactly like the description does today.
Lens curve: :::sportrx.base_curve::: base.
Base curve: 6
Lens curve: 6 base.
The card is written once. Every product that has it shows its own number, and if a product's base curve is corrected, its card updates on its own.
Card wording below is illustrative.
The goggle never shows a half-filled “Lens curve: base.” card, because it was never given the card. Products without a base curve simply aren't assigned it.
It works the same for any field. If activities ever move into a card, Card Data reading “Perfect for: :::product.list_activities:::” would show each product's own list, e.g. “Perfect for: Biking, Running, Fishing”. We tested exactly this on the staging store.
Only the placeholder changes from product to product. So one card works for all base curve products as long as everything else on it is the same: the wording around the placeholder, the image, the pop-up and the colours. If any of that needs to differ for some products, duplicate the card, make the change, and give the copy to just those products.
Both cards still fill in each product's own base curve. The duplicate exists only because its wording and image are different. Make as few copies as the differences require, since each copy is one more card to keep up to date. Card names and wording here are illustrative.
| Situation | What the shopper sees |
|---|---|
| The card isn't on the product's card list | Not shown The card list always decides first. |
| The card is on the list and the product has the value | Shown With the product's value filled in. |
| The card is on the list, but the product's field is empty | Not shown A backup. Normally this can't happen, because a product without the detail isn't given the card. It covers a value being removed later. |
| The card has no placeholders, e.g. Free Shipping | Shown Whenever it's on the list, exactly as written. |
A question for you: when a card is on a product but that product's field is empty, should the card show general default wording instead of hiding? We've planned for it to hide, because these cards exist to give details specific to that product, and a general card without the product's own detail says little. If there are cards where general wording would still help, tell us which.
Together we define the set of cards used across many products, with their wording, images and placeholders.
Rules by product type, brand or a product detail. For example: every sunglass and eyeglass gets the base curve card, and only products with a base curve.
The import adds the right cards to every product at once, so nobody assigns cards by hand for the existing catalog.
After launch, the team adds or removes cards on any product freely, and can create one-off cards for unique situations alongside the standard set.
Edit the product's field, e.g. its base curve. Every card on that product that uses it updates.
Edit the card once. Every product that has it changes.
Create the card, with placeholders for any product details, then add it to the products it belongs on. That can be done in bulk from a spreadsheet.
Fill in its fields and give it its cards, typically the same standard cards as similar products.
Create a one-off card and add it to just the products that need it.
The Card Data field will carry short instructions right below it in the admin, on how to write a placeholder, with an example. Placeholders work the same in the card's pop-up.
What this decision commits us to build or change. The one item still marked “confirm” needs checking with the Shopify toolkit during the build.
:::namespace.key::: inside the Card Data field (key description) and the pop-up content (item 7). The heading stays plain text. Split the text on :::: odd-indexed parts are tokens, even-indexed parts are literal text. Split each token on . into namespace and key.
(text.size − (text | remove: ':::').size) ÷ 3. An odd count means an unclosed token, so treat the text as malformed. Otherwise Advanced :::sportrx.base_curve Base Lens would read the rest of the sentence as one token.., no spaces, no :. That catches stray extra colons (::::a:::: splits into :a and :).product already exists (product.activities, product.list_activities). A token like product.list_activities means namespace product, key list_activities, read from the context object.:::) are treated as unresolved.product (whose fields placeholders read) and card_list (which cards show). If they point at different things, cards get filled with the wrong product's values, and Liquid can't detect that. Resolve one subject per block and use it for both.
page setting (new), then closest.page, then page.product setting, then closest.product, then product.closest, so a static pick wins everywhere, including inside product grids. Say so in the setting's help text.card_list if set, otherwise the subject's pdp_extended_content.info_cards.product set on a product page, cards come from the current product and values from the chosen one.| Setup | Cards | Values | Right? |
|---|---|---|---|
| Product page, nothing set | Current product's list | Current product | Yes |
Product page, card_list picked by hand (e.g. service cards) | The picked cards | Current product | Yes. The cards are templates the product fills in |
product set to a related product (e.g. dynamic source “the frame for this lens”) | That product's list | That product | Yes, because both use the subject |
card_list dynamic-sourced from another object's field, product blank | That object's cards | Current product | Possibly wrong. Liquid can't see where a dynamic source came from |
Page template, card_list from a page field | Page's cards | Page | Yes, with the new page setting |
| Page template, a card whose placeholder reads a product field | Page's cards | Page, which lacks that field | Card hides, with an editor note |
| Repeater inside a product grid or recommendations | Each item's list | Each item | Yes, via closest.product, unless a static product pick overrides it |
card_list is dynamic-sourced from an object's field, set product or page to that same object.card_list saying so.handle” under the block, so anyone can see which object placeholders read. Shoppers never see it.closest.collection or closest.metaobject extend the same chain later; request.page_type picks the chain.{% liquid
if request.page_type == 'page'
assign subject = block.settings.page | default: closest.page | default: page
else
assign subject = block.settings.product | default: closest.product | default: product
endif
assign info_cards = block.settings.card_list | default: subject.metafields.pdp_extended_content.info_cards.value
assign owner = subject
assign parts = text | split: ':::'
assign out = ''
assign unresolved = false
for part in parts
assign odd = forloop.index0 | modulo: 2
if odd == 1
assign nk = part | strip | split: '.'
assign field = owner.metafields[nk[0]][nk[1]]
if nk.size != 2 or field == blank
assign unresolved = true
endif
# format field by type (item 3), append to out
else
assign out = out | append: part
endif
endfor
%}
| round) before inserting. Any other wording goes around the placeholder in Card Data. This covers sportrx.base_curve, a number_decimal field whose catalog values are all whole numbers (2, 4, 6, 8). It's the starting rule; revisit if a field ever needs decimals.label), as with filters.frame_materials.metafield_tag before inserting.theme/blocks/info-card-repeater.liquid, 95 lines today)
description, rich text: render to HTML, then replace) and the pop-up content. The heading is output as is.card_list setting (set directly or by dynamic source; page templates such as track-order, prescription-replacement-lenses and Test Run already use it), otherwise the subject's pdp_extended_content.info_cards (item 2). Same order as today, but read from the shared subject.request.design_mode is true (theme editor only; confirmed in Shopify's theme editor docs), print a small note under any card with a malformed token, or a token that resolved to nothing on the product being previewed.
?preview_theme_id=) isn't design mode.description field's display name to Card Data, which is true whether or not it uses placeholders. Keep the key description, so the repeater, existing cards and imports keep working.:::namespace.key::: format, the base curve example, numbers shown as whole numbers, and that an empty value hides the card.modal_topic (item 7).popup-example, and no modal with that topic exists (repo sections/modals.json and preview theme 166153421036), so the + button opens nothing. Plan: one site-wide modal that every card's pop-up is sent to, built only from existing Neptune pieces.
sections/modals.json), e.g. topic info-card. Turn on its existing Use X-HTML setting (remote_frame, from the RemoteFrame schema in components/@schemas/remote-frame.js). That wraps the modal's content in an <x-html> frame (id="remote-frame-{section.id}"). Set its Document events (remote_document_events) to a dedicated event, e.g. info-card:popup.<div hidden id="info-card-popup-{{ block.id }}-{{ entry.system.handle }}">.document.dispatchEvent(new CustomEvent('info-card:popup', { detail: { sourceSelector: '#info-card-popup-…' } })), then Modal.open('info-card').
detail straight into its refresh. With a sourceSelector, x-html runs in proxy mode (renderFromProxySource in assets/x-html.js): it renders that on-page element into the modal, with no network request.x-html:error, and the frame refuses to point at itself or anything containing it.Modal.open already handles history, focus return, Escape and click-outside.src plus sectionId in the event detail, and it loads one section of that URL through Shopify's section rendering, as remote-product-items and recommendations already do with <x-html shopify section-id=…>.hidden div's children as expected. Avoid <template>, whose content isn't in the live DOM.x-html:load rather than immediately, so the shopper never sees the previous card's content flash.modal_topic: the shared modal is the only pop-up path, so one way to make a pop-up avoids confusion.
modal_topic = popup-example, a topic that doesn't exist, so nothing visible is lost.replacement-lens-form block has its own modal_topic block setting. That's not this field, and it stays.base_curve value; skip products without one.pdp_extended_content.info_cards column replaces the whole list, so plan from a fresh export and merge any hand-placed cards.altera ref metafield-definitions products, plus the admin API for storefront access, on demand or on a schedule.spike-liquid-heading, activity-biking and spike-list-activities-source.product.list_activities definition and its value on 100-blake-test-logan, plus that product's 7th card.