Shopify Builds>Nudestix>A second product template whose add-to-bag switches between one cart line and several

A second product template whose add-to-bag switches between one cart line and several

main-product-bundle.liquid is a fork of the theme's product section in which one Liquid conditional wraps the buy form in either a standard <product-form> or a <bundle-product-form>, depending on which bundle metafield the product carries.

Fixed kits add one line to the cart. Build-your-own sets add several at once. Rather than branch the highest-traffic template on the site, we forked it: a second product template whose add-to-bag wrapper is chosen by a single Liquid conditional, driven by which of two metafields a merchandiser has filled in. The cost is a second copy of the product page.

Fact Strip

  • Client: Nudestix
  • Surfaces: Product detail page and cart
  • Templates served: 1
  • Complexity: High
  • Attribution: Deploi-authored, as a fork. main-product-bundle.liquid began as a copy of the theme's own main-product.liquid in a theme based on Dawn 6.0.2. The bundle behavior is ours; the surrounding product-page logic is largely inherited.
  • Status: Live, verified 2026-09-06
  • Evidence: One section file plus two client task-register records — confidence: moderate

The Problem

Nudestix sells two shapes of set, and they diverge at the only moment that matters. A fixed kit is one purchase: the shopper buys a bundle SKU and the cart gets one line. A build-your-own set is several: the shopper picks a shade per component, and the add-to-bag submits several lines at once.

Neither fits a standard product template, because that template has one buy form and assumes one product. If you sell both shapes, you have this problem. And the two differ enough that expressing both as conditionals inside the main product section would have loaded the site's busiest template with branches that fire for a minority of products.

The Constraint

There is no native bundle product on this plan and theme version, so the composition of a set has to live in metafields, and a multi-line add has to be assembled client-side out of what those metafields describe.

The harder constraint is everything a bundle page still has to be. It needs the same shade swatches, per-variant SEO, accordions, review surface and offer banner as any other product page — a shopper does not accept a downgraded page because the product is a set. Writing one from scratch would have meant reimplementing all of that, then maintaining a second, worse version of it.

And the section being modified is the primary product template: the highest-traffic template on the storefront and the riskiest file in the theme to touch.

What We Built

main-product-bundle.liquid, a fork of main-product.liquid, driven by two metafields.

custom.pdp_bundle_kits sets is_bundle_kits. That path loads bundle-kits.css, renders bundle-kits.liquid as a "Save with Sets" module in the information column, and keeps the standard <product-form> add-to-bag — one product, one line, nothing unusual reaching the cart.

custom.pdp_bundle_products takes the other path. It renders bundle-products.liquid above the quantity selector — the component picker, passed the main product id and the metaobject list that defines the set — and wraps the buy button in <bundle-product-form>, the element that owns the multi-line submit. The set's contents are not a variant and not an app record; they are a metaobject list on the product, read at render time and turned into as many cart lines as the shopper has configured.

The switch itself is deliberately small: one Liquid conditional around the form's opening tag and a matching one around its closing tag. Everything between them is shared. That is the whole mechanism, and keeping it that size is the point — the two paths differ in their wrapper and in one rendered snippet, not in their content logic.

Everything else in the fork is layout. The product parent is container-width rather than full-bleed. Sticky badges move into the first gallery slide instead of the wrapper. The mobile title and review block is hoisted above the gallery. The swiper controls get their own positioning. And the buy button's price renders from selected_or_first_available_variant rather than the product, because on a set the two are not the same number.

product.bundle.json wires the section in as main, with the customizable product-banner section disabled and a live product_banner_offer block.

Why This Way

Forking the section rather than branching inside it is the whole decision here, and it is a risk trade, not a taste one. The arithmetic is the transferable part.

Branch inside the primary product section and every bundle conditional you add evaluates on almost all of the storefront's product traffic. Get one wrong and you have not broken bundles — you have broken the product page. Fork it, and a bundle mistake can only reach one template. You buy blast radius and you pay in duplication: two copies of the product page's content, SEO and gallery behavior, every fix landing twice, and the two already drifting in small ways, because copies always do.

So the question is not which is cleaner. It is how often the shared half changes, against how bad a mistake on the primary template would be. Here the product section is the busiest file in the theme and bundle campaigns turn over faster than the PDP does, so the fork wins — for now. If the shared half starts changing weekly it stops winning, and the answer is extracting the duplicated loops into snippets one at a time, not merging the files back together.

The switch itself is which metafield is populated, not a template assignment or a tag. A merchandiser turns a product into a kit page or a build-your-own page by filling in a field — no developer, and no documentation beyond the field names.

Why Not an App

Bundle apps hold the bundle definition inside the app. Why that was the deciding fact — and what it costs a merchant the day the subscription lapses — is argued in full on the palette builder's page, where the same call was made first. We are not going to make it twice.

What is specific to this template is the switch itself. A product becomes a kit page or a build-your-own page because a merchandiser filled in one of two metafields — not because anyone configured a widget, applied a tag or reassigned a template. One file serves both shapes, the composition of a set is a metaobject list Shopify owns, and the multi-line add goes out through the standard /cart/add.js route like any other cart write. An app would express the same thing as two widget setups on two page configurations, maintained apart from each other, and still could not tell you which shape a given product is without opening the app.

Implementation Notes

  • is_bundle_kits is derived from product.metafields.custom.pdp_bundle_kits being present and having a non-blank .value, so an empty metafield does not put the product on the kits path.
  • The add-to-bag wrapper is chosen by a Liquid conditional around both the opening and the closing tag: <product-form> for kits, <bundle-product-form data-bundle-product-form="{{ product.id }}"> for build-your-own.
  • bundle-products.liquid receives main_product_id and the pdp_bundle_products metaobject list; bundle-kits.liquid receives the product. Both are shared with the module that renders kits on component pages.
  • The bundle button renders "Add to bag | $price" from selected_or_first_available_variant.price, split on the decimal, where the main product template reads product.price.
  • The shade swatch registry from settings.swatch_colors is re-split inside the bundle-products render call, so component pickers get the same colors as the main variant picker.
  • The mobile title, subtitle and wishlist block is duplicated above the gallery on the same variant_title / product_h1 fallback chain as the main template, so per-variant SEO stays consistent.

Edge Cases

  • A product carrying neither bundle metafield falls through to ordinary product-page behavior here, rather than rendering a broken set.
  • One legacy bundle product is special-cased by id onto the kits branch, so it keeps the simple single-line form.
  • Sticky badges render only on the first gallery slide on this template, so they are not duplicated across the swiper's slides.
  • The price block is disabled in product.bundle.json, because on this template the price is rendered inside the buy button instead.
  • Reviews on this template are served by PowerReviews; the alternate review app block that ships elsewhere in the theme is present but disabled here.

Platform Primitives Used

  • Metafields — the two switch fields, plus the copy and SEO fallbacks inherited from the product template.
  • Metaobjectspdp_bundle_products and pdp_bundle_kits define what a set contains.
  • Cart AJAX API/cart/add.js, once for a kit and repeatedly for a build-your-own set.
  • Liquid section schema and settings — the forked section's schema, including the offer banner block.
  • Custom elements<product-form> and <bundle-product-form>, the two wrappers the conditional picks between.
  • App blocks — third-party blocks mount here alongside the bundle logic.

Integrations in Play

  • PowerReviews — serves the review surface on this template.
  • Tolstoy — coexists on the page as video content.
  • Wishlist Hero — its control sits in the mobile title block this template hoists above the gallery.

Where It Runs

One template: product.bundle. That is deliberate — the template is the switch. A product becomes a bundle page by being assigned this template and carrying one of two metafields, and how many products are in that state changes with the merchandising calendar, not the code.

What This Demonstrates

How We Know

One section file was read directly, alongside the two snippets it renders and the JSON template that wires it in. There are also two client task-register records, but they are honest about what they are: the one document trace that touches this template records an operational follow-up on it, not the decision to build it. Confidence is moderate for exactly that reason. Documented from the code; no client-side record of the decision survives.

Related Builds

The Buy-vs-Build Question

The deciding question is not what the bundle looks like — it's where its definition lives. If it can live in an app, buy. If it has to live in Shopify, in metaobjects the merchant owns, that is a build, and a forked template's maintenance cost is the price. Our reasoning is on the fixed bundles and multipacks decision page.

Ready to Decide Where Your Bundle Definitions Should Live?

That one question settles most bundle architecture, and it is worth answering before the first install rather than after the second migration. Contact us today and we'll work through it with your catalog.

More builds