Skip to main content
This guide walks through the deep integration path — appropriate when:
  • Your PDP uses a custom variant picker that doesn’t dispatch standard [name="id"] change events, OR
  • You want the upsell section to render INSIDE the variant-specific markup (per-variant anchor), OR
  • You want the upsell picks bundled atomically with the theme’s own Add-to-cart POST instead of fired as a separate /cart/add.js.
The path described here is what most Dawn-forked themes need. If your theme works out of the box with the default block placement, see the zero-code activation guide first — you may not need any of this.

Overview — seven steps

  1. Theme editor — drop the PDP upsell block, set Mode = linked, Render mode = deferred. The block doesn’t paint until your theme code asks it to.
  2. PDP markup — drop an anchor element next to the variant you want the panel under. The variant-id="..." attribute on the anchor is what renderInto targets.
  3. assets/global.js — inside VariantSelects.onVariantChange, call Glood.upsell.renderInto(...) so the panel re-mounts on every variant switch.
  4. assets/product-form.js — inside ProductForm.onSubmitHandler, await Glood.upsell.commitPickSelections({ triggerVariantId }) and commitFreeGiftSelections() BEFORE the cart sections re-render. The picks ride along on the same Add-to-cart click.
  5. snippets/cart-drawer.liquid — drop the data-glood-mini-cart-line marker <div> under each cart item. Required for the carousel / Reselect CTA to render in the mini-cart.
  6. assets/cart-drawer.js — call Glood.upsell.miniCart.hydrate() on every cart-rendered event AND register confirm/swap callbacks that re-fetch the cart sections.
  7. App embed JS — paste a small init script into the app embed’s Custom JS setting. It boots the upsell on initial page load, registers the cart-drawer refresh callbacks, and re-hydrates on every theme cart-update event so the integration “just works” without further theme changes.

Step 1 — Theme editor: PDP block, deferred + linked

  1. In the Shopify admin → Online Store → Themes → Customize, open a product template.
  2. In the Product information section, click Add blockGlood Upsell for PDP.
  3. With the block selected, set:
    • Mode: linked — picks are written to state.selections but NOT auto-committed; the theme’s Add-to-cart commits them atomically.
    • Render mode: deferred — the block renders hidden and waits for Glood.upsell.renderInto(...) to mount it into a theme anchor. Without this the block paints into its default position immediately and renderInto has nothing to relocate.
  4. Save.
Deferred render + linked mode is a one-time configuration per block. You only need to do this once per theme — the settings persist in settings_data.json.

Step 2 — PDP markup: per-variant anchor

Inside the product template (or wherever you want the panel to land per variant), drop an anchor element with variant-id="{{ variant.id }}". The Glood JS uses the selector to mount the deferred block:
You can also use a single anchor and re-target it on every variant change — same code path, simpler markup:
The choice is yours. Per-variant anchors let you style each variant’s container independently; a single anchor is simpler and re-paints on every switch.

Step 3 — assets/global.js: re-anchor on variant change

Inside Dawn’s VariantSelects.onVariantChange callback, call Glood.upsell.renderInto(selector, { variantId }) whenever the customer picks a different variant:

What renderInto does

  • First arg — selector (e.g. [variant-id="48328565752127"]) OR a direct DOM element reference. Both work.
  • variantId — the variant the panel should scope its budget/trigger against. Without it the panel falls back to whatever state.variantId holds (which may lag the customer’s click by one tick on slow variant pickers).
Idempotent — calling renderInto again on the same anchor with the same variant id is a no-op. Calling it with a DIFFERENT anchor moves the mounted node (useful for SPA navigations).

Step 4 — assets/product-form.js: atomic commit on Add-to-cart

Inside ProductForm.onSubmitHandler, after the theme’s /cart/add.js POST resolves, await the upsell commits BEFORE the cart-drawer re-renders. The picks ride along on the same click and the customer sees one atomic update:

Why the order matters

If you fetched the sections BEFORE the upsell commits, the drawer would paint the host line at quantity=N with no gifts attached — and a fraction of a second later the cart would re-shape to N qty=1 bundles. Customer sees a flicker. Always await the commits first. The await Glood.upsell.refresh() is load-bearing — it awaits the auto-attach pipeline so the multi-qty split has fully landed before the next-line code reads the cart. Without it, a fast click-to-checkout could race the split.

Step 5 — snippets/cart-drawer.liquid: marker div per line

For the mini-cart carousel and Reselect CTA to render, drop the data-glood-mini-cart-line marker <div> once per cart line:

Attribute contract

Dawn-style themes (and many others) strip properties whose key starts with _ from item.properties before they reach the rendered DOM. That’s why the storefront also reads pick_gifts JSON from the live /cart.js response (keyed by data-line-key) — the anchor’s data-line-properties-json may be incomplete but the carousel still hydrates correctly.

Step 6 — assets/cart-drawer.js: hydrate + refresh on commit/swap

The mini-cart adapter is theme-driven — Glood doesn’t auto-rehydrate on cart events because re-rendering at the wrong moment can corrupt theme animations. The theme calls hydrate() on every cart-rendered event it knows about.

Add a refresh() method to CartDrawer

For the mini-cart confirm/swap callbacks to re-render the drawer, the drawer needs an idempotent refresh method:

Hydrate on drawer open

Glood’s storefront JS won’t paint the carousel until you call hydrate(). The simplest hook is inside open() — every time the drawer opens (icon click, post-Add-to-cart auto-open, etc.) the carousel re-paints fresh:
Place the hydrate() call AFTER document.body.classList.add('overflow-hidden') (right at the end of open()). Calling it before the drawer is in its final state can leave the carousel measuring widths against the still-animating container — the Swiper picks up the wrong slidesPerView for one frame before settling.

Hydrate after the drawer’s content re-renders

The open() hook handles “drawer opens after being closed.” You ALSO need to hydrate after renderContents() (the method Dawn calls when the cart sections HTML is replaced after Add-to-cart), and after your custom refresh() method (step 6 below). The example refresh() further down already calls hydrate() at the end; for renderContents() add it inline:
Because renderContents() ends by calling open(), the hydrate() inside open() is sufficient — no extra call needed here. If your theme has a renderContents path that does NOT call open() afterward (e.g., a non-drawer cart page render), add window.Glood?.upsell?.miniCart?.hydrate?.() at the end of that path too.

Wire the mini-cart callbacks to refresh()

The mini-cart Confirm and Reselect → Confirm flows mutate the cart via /cart/change.js + /cart/add.js directly. The theme’s drawer doesn’t know about those mutations unless you re-render. Register the callbacks once — typically inside the app embed’s custom JS settings or a small snippet you load on every page:

Hydrate options (slidesPerView, text overrides, classNames)

hydrate() accepts an options object you can use to tweak the Swiper, override mini-cart-specific text, and add CSS classes:
Reserved Swiper keys (modules, navigation) are stripped — the prev/next buttons are wired by the storefront and can’t be overridden. See the JS triggers reference for the full hydrate options contract.

Step 7 — App embed JS: boot, callbacks, and cart-update rehydrate

Steps 1–6 wire the upsell into the theme’s own classes (VariantSelects, ProductForm, CartDrawer). That covers every action the customer takes after the page is interactive. Step 7 boots the upsell on initial page load — the first PDP render before any variant change, the first cart-drawer paint before any open/close cycle — and registers the cross-cutting callbacks (free-gift auto-add, mini-cart confirm, mini-cart swap, theme cart-update event) so the integration stays in sync without you re-wiring them in every class. Paste this into the app embed’s Custom JS setting (Theme Editor → Theme Settings → App embeds → Glood.AI Recommendations → expand the embed → Custom JS field). Saving the theme injects it into every storefront page:

What each block does

The subscribe function is the Dawn theme’s own pub/sub helper (defined in assets/pubsub.js). It’s NOT the same as Glood.upsell.subscribe — that one targets the storefront’s internal bus. The typeof subscribe === 'function' guard handles themes that don’t expose it (custom themes, older Dawn forks).
Some merchants prefer to drop this into a theme JS file (e.g. assets/glood-init.js + a &#123;% script %&#125; tag) instead of the app embed’s Custom JS field. Both work — the embed’s Custom JS is just the path with no theme-file edit required. If you’re committing to a Git-managed theme, prefer the file path so the change is reviewable.

Verify the integration end-to-end

After all seven steps:
  1. PDP variant switch — open a PDP that triggers an offer. Switch the variant picker. The upsell panel should re-mount under the active variant’s anchor; the previous variant’s anchor should lose its data-active attribute.
  2. PDP add-to-cart — pick a gift, click Add to cart. The cart drawer should open with the main product AND the bundled gift line, no flicker.
  3. Multi-qty — set the PDP quantity stepper to 3 with one or more picks, click Add. Cart should show 3 split bundles (one per main unit), each carrying its own gifts.
  4. Mini-cart Reselect — open the drawer, click Reselect on a gift line, pick a replacement, Confirm. The drawer should re-render with the new gift in place of the old one (no separate manual reload).
  5. Free-gift auto-add — add enough to cart to cross a free-gift tier. The gift should auto-add at €0 and the drawer should re-render to show it.

Common pitfalls