Bundle builder, plan v3
The rail ladder the client chose, on a receipt that leads with what the shopper saves, riding the theme's own cart path. Mobile first, because 78.5% of sessions are.
Decision record
Keep the rail. Keep the money. Borrow the theme's cart. Build the tiles ourselves.
- The rail ladder stays. It is the element the client picked out of draft 1: dots on a track, the bag count under each, the reward under that. The v2 receipt-ladder is dropped.
- The v2 receipt survives in its money form. The savings figure is the biggest number in the panel, the checkout total sits under it, and the gifts are one row with their retail value. The reason is not taste: the rungs are cumulative, so listing every cleared rung reads as though the free bags stack. Only the top one is true.
- Campaigns keep their own identities. Black Friday is black with crimson; Cyber Monday is midnight with the brand's own mint. Same rail, same tiles, different room.
- The theme's bundle engine is not the base. Concept's
product-bundlesection draws one card per product with a picker, and our catalogue is one product with 62 variants per market. A shopper building ten bags would work one dropdown ten times. We keep our tap-to-add tiles and borrow only the parts of the theme that de-risk the cart.
Reuse map: theme vs custom
Verified against the theme files on the Shubham branch on 14 Sep. The stock bundle section is byte-identical to main and has never been published on a page.
| Theme part | What it does | Use it? | Why |
|---|---|---|---|
Cart submit path in ProductBundle | Batched /cart/add.js with items[], cart:bundled-sections, cartUpdate pubsub, sold-out relabel, error line | Yes | Already patched for the fly-to-cart decision in product-bundle.js. One submit path for the whole store. |
quantity-input | Stepper element with min, max, step validation | Yes | Receipt row steppers. Free validation, free translations. |
sticky-element | Sticky sidebar that respects the header offset | Yes | Desktop receipt. Removes our own max() offset arithmetic. |
.button, hover-button | Theme buttons with the fill animation | Yes | CTA looks native on every market. |
theme.Currency.formatMoney | Market-correct money strings | Yes | Already used in draft 1. |
NB_GIFT_RULES.tiersSpec | The live gift ladder per market | Yes | Reward copy derives from it, never re-authored. Kills the drift risk from draft 1. |
product-card-bundle grid | One card per product, variant picker inside | No | Wrong shape for one product with 62 variants. |
ProductBundle slot model as shipped | One slot per add, prevent_duplicate_products | Not as is | One override merges repeat adds of the same variant into one slot; duplicates setting stays off. |
<progress-bar> | One fill toward minimum_quantity | No | No rungs, no labels. The rail replaces it. |
page.bundle.json + market contexts | The page template | Yes | Add nb-bundle-builder as a section; the three stock sections stay in the file untouched. |
| Native automatic discounts | Amount off products, minimum quantity, scheduled | Yes | Ran on this store 2022 to 2024; "best discount applies" resolves the tiers; no app to uninstall or pay for. |
ProductBundle element | Slots, totals, submit, errors, pubsub | Extend | Subclass with four overrides (merge same variant, rail fill, slot template, 422 loop); the rest is inherited and updates with the theme. |
QuickView subclass | Modal that fetches a page and injects a fragment | Phase 2 | The homepage and PDP popup, per the September workflow finding. |
Mobile first: 390px, top to bottom
- Masthead. Campaign banner only when a campaign is on. Headline. At zero bags the offer sentence; from one bag the hero count with its caption. Countdown line reserved so the shelf never shifts.
- The rail. Full width. All rungs as dots with the bag count under each. Six reward labels do not fit 390px, so they collapse to one line beneath the track: 2 more bags for 15% off, plus 3 free bags and 3 carabiners. The shape the client liked survives; the copy stays readable.
- Style index. Sticky, text only, jumps and scroll-spies.
- Shelf. Two full rows per style, then "Show all 12 colours". Tap a tile to add; tap again to increment; a solid stepper bar sits on the photo.
- No tray at zero bags. The rail already shows the whole offer, so a dormant bar eating 68px before the first tap is deleted.
- Docked bar from the first bag. 68px: $12.76 saved and the gap on the left, the CTA on the right. Tap the left half to open the sheet: money hero, gifts row, bag rows, pinned CTA. Escape or the scrim closes it.
Desktop, 992px and up
Same order; the rail spans both columns under the masthead, the shelf takes the left column, the receipt is sticky on the right at 380px. Nothing is centred.
Three states, one section
| Everyday | Black Friday | Cyber Monday | |
|---|---|---|---|
| Ground | paper #f7f4ef | black #0b0b0c | midnight #0f1830 |
| Accent | crimson | crimson | mint #a8e8e2, the theme's own |
| Receipt | ink | white paper | lifted navy |
| Rungs | 1, 3, 4, 6, 8, 10 | 1, 2, 3, 4, 6, 8, 10 | Black Friday plus 15 |
| Extra | none | banner, countdown | banner, countdown, stock-up card |
Data and money
- One source per fact. Gift thresholds from
nb_gift_rules, already on every page. Cash percentages are section blocks (one per rung, three percent columns: everyday, Black Friday, Cyber Monday); the campaign window and the free-shipping rung are section settings. Everything the merchant changes, they change in the theme editor. Thenb_bundle_offersmetaobject from the September plan is dropped; the build spec has the schema. - The discount comes off at checkout through native Shopify automatic discounts, one per rung, scoped to a
bundle-eligiblecollection and scheduled per campaign. No app: the store ran exactly this from 2022 to 2024 and Shopify applies the best qualifying discount when tiers cannot combine (verified 14 Sep). The section previews it and never states a price the cart could contradict; the savings figure is exact because the percentage is deterministic on bag count. - The count is cart-seeded. The ladder reads
/cart.json boot and on everycartUpdate, so it never resets to zero after an add and it agrees with the drawer. - Campaign state is decided in the browser from
starts_atandends_at, because the page is edge-cached and Liquid'snowis frozen at cache time. A merchant who forgets to flip the toggle gets everyday styling the minute the window closes.
Build order, with the gate for each step
- Refresh Shubham. Commit the in-flight work in its groups, then
git merge origin/main.Gate:
templates/index.jsonandtemplates/product.jsonmatch main byte for byte before any bundle edit touches them. - Discount plumbing. Simple Discounts automatic tiers on the eligible collection, one market at a time.
Gate: a real 4-bag cart in US, UK and EU checks out at the ladder's percentage. Screenshots kept.
- Section v3 on the v2 files: rail, tiles, receipt, campaign tokens, theme cart path.
Gate:
shopify theme checkclean; probe script passes every state at 390 and 1440; zero em-dashes; AA on every measured pair. - Data. Rewards derived from
tiersSpec;nb_bundle_offersmetaobject; cart seeding; the market cache guard (one reload, never a loop).Gate:
?country=DE,?country=PLand?country=SGeach render their own prices and translated colour names. - Copy to locale keys, 32 files, plural-safe.
Gate: no "Translation missing" on da or pl.
- Page.
nb-bundle-builderadded topage.bundle.jsonplus 8 market contexts, page created unpublished.Gate: pre-consent EU pageview renders the shelf and the ladder with the script deferred; consent-guard check passes.
- Popup on homepage and PDP via the QuickView subclass.
Gate: modal fetch is under 20KB and the header never appears inside it.
Dates, working back from Black Friday
| When | What |
|---|---|
| 15 to 17 Sep | Shubham refresh. NB09 closes on the 17th; nothing goes near main before that. |
| 18 Sep to 3 Oct | Steps 2 to 5. Discount plumbing first because it is the part nobody can see and the part that costs money if wrong. |
| 4 to 9 Oct | Page on the Shubham theme, client review, per-market QA. |
| 12 to 26 Oct | Rollouts test, everyday ladder, 7 markets, EU out. |
| 27 Oct to 12 Nov | Readout, fixes, popup, campaign dry run with the clock forced. |
| 13 Nov | Code freeze. |
| 27 Nov, 30 Nov | Black Friday, Cyber Monday. The browser flips the state; nobody is awake at midnight for this. |
Thinking ahead: what will bite, and the answer for each
- The cart drawer already contradicts the ladder. Its free-shipping bar reads
USD:40while shipping is weight-gated at 49.9g. Two Daypacks are $51.90 and 40g: the drawer promises free shipping, checkout charges $4.95. The bundle will say "free shipping at 3 bags" beside a drawer that says "$40". Fix the drawer to the weight rule before the builder ships, or the two messages fight in the same viewport. This is a live bug today, independent of the bundle. - The photography. Every tile is a model wearing the bag. At 165px the colourway is hard to judge. The tile design wants bag-on-white; that is a content request, not a code fix, and it has a lead time.
- The 422.
/cart/add.jswithitems[]is all-or-nothing, and inventory in an edge-cached page is stale by definition. One sold-out variant fails a 10-bag add. The submit path drops the offending line, marks the tile sold out in place, retries up to three times, and reports exactly what did not go in. - The control arm freezes. A Rollouts control serves a snapshot from the moment the test starts. Any fix to main during the test reaches only the treatment. Ship fixes before the 12th or restart the test.
- Cookiebot. The section script is marked
data-cookieconsent="ignore"and readstheme.settingsat boot, so it belongs to the boot-chain set. Nothing about script order changes without the whole-set rule and a fresh pre-consent EU pageview. - The privacy banner. Shopify's banner sits at z-index 2000000 and has covered a drawer CTA on this store before. The mobile docked bar is the only path to the cart. The
custom.cssz-index fix is a prerequisite, not a follow-up. - The theme's 25-section homepage cap. The popup trigger goes into the existing
image-with-text-overlayinstance; no new section is added toindex.json. - Mint on the buy button. Cyber Monday's CTA in mint is the strongest "different event" signal and it replaces a crimson button shoppers have learned. Test it; do not decide it.
- After the season. The same section is the everyday bundle page from 1 December. Nothing seasonal is hardcoded; the metaobject window is the only switch.
Test, do not assume
- Rail vs list. The rail is the client's pick; the v2 receipt-ladder scored higher with the CRO judges. One arm each, same everything else. Cheap, and it settles the argument with orders instead of opinions.
- Mint CTA on Cyber Monday vs crimson. Two days of data is thin; run it as a preview on the Shubham theme first, then decide whether it is worth an arm.
- Preset button present vs absent. "Add 2 more Standard" is the one-tap rung closer; it is also a button most builders do not have.
Decisions needed now
- Confirm the read: the design the client liked is draft 1's rail, not Concept's stock section. If it is the stock section, I push it to the Shubham theme on
page.bundleso you can both see it rendered before we go further. - Settled 14 Sep: discounts are native automatic tiers, not the Simple Discounts app. The audit is in the build spec; the app stays installed and unused.
- Bag-on-white photography: who shoots it, and by when.
- The drawer's free-shipping rule: fix it now as its own change, or bundle it into this work.
What the demo is and is not
The v3 page on this hub is a design prototype with the real US catalogue and 62 real product photos. It is not connected to a cart: the count is not seeded from /cart.js, the 422 recovery is not built, the market guard is not built, and the copy is inline English. Everything you can click, toggle and read is the intended design; everything underneath is specified above and unbuilt.