This guide walks through the deep integration path — appropriate when:Documentation Index
Fetch the complete documentation index at: https://docs.glood.ai/llms.txt
Use this file to discover all available pages before exploring further.
- 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.
Overview — seven steps
- 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. - PDP markup — drop an anchor element next to the variant you want the panel under. The
variant-id="..."attribute on the anchor is whatrenderIntotargets. assets/global.js— insideVariantSelects.onVariantChange, callGlood.upsell.renderInto(...)so the panel re-mounts on every variant switch.assets/product-form.js— insideProductForm.onSubmitHandler, awaitGlood.upsell.commitPickSelections({ triggerVariantId })andcommitFreeGiftSelections()BEFORE the cart sections re-render. The picks ride along on the same Add-to-cart click.snippets/cart-drawer.liquid— drop thedata-glood-mini-cart-linemarker<div>under each cart item. Required for the carousel / Reselect CTA to render in the mini-cart.assets/cart-drawer.js— callGlood.upsell.miniCart.hydrate()on every cart-rendered event AND register confirm/swap callbacks that re-fetch the cart sections.- 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-updateevent so the integration “just works” without further theme changes.
Step 1 — Theme editor: PDP block, deferred + linked
- In the Shopify admin → Online Store → Themes → Customize, open a product template.
- In the Product information section, click Add block → Glood Upsell for PDP.
- With the block selected, set:
- Mode:
linked— picks are written tostate.selectionsbut NOT auto-committed; the theme’s Add-to-cart commits them atomically. - Render mode:
deferred— the block renders hidden and waits forGlood.upsell.renderInto(...)to mount it into a theme anchor. Without this the block paints into its default position immediately andrenderIntohas nothing to relocate.
- Mode:
- 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 withvariant-id="{{ variant.id }}". The Glood JS uses the selector to mount the deferred block:
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 whateverstate.variantIdholds (which may lag the customer’s click by one tick on slow variant pickers).
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
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
| Attribute | Source | Purpose |
|---|---|---|
data-line-key | item.key | Stable Shopify cart-line identifier — scopes swap-mode state. |
data-line-product-id | item.product_id | Matched against product-mode offer triggers. |
data-line-variant-id | item.variant_id | Matched against variant-mode offer triggers. |
data-line-qty | item.quantity | Drives slot budget when the line is a trigger product. |
data-line-properties-json | item.properties | json | escape | JSON of the line’s properties. The | escape is load-bearing — JSON inside an HTML attribute MUST be entity-encoded. |
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 callhydrate(). 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
Theopen() 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:
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:
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
| Block | Why it’s load-bearing |
|---|---|
initGloodUpsell() + the final initGloodUpsell() call inside DOMContentLoaded | Bootstraps the PDP panel on FIRST page load. Step 3’s renderInto only fires on variant CHANGE — without this initial call, customers who land directly on a product page (e.g., via a deep link) see no upsell until they touch the variant picker. |
?variant= URL param + first-variant fallback | Handles both deep links (/products/x?variant=42) and the bare PDP (/products/x). The fallback walks window.glood.product.variants[0] — populated by app-embed.liquid via {{ product | json }}. |
refreshCartDrawer(open) | Single source of truth for re-rendering the drawer. Used by three callbacks: free-gift auto-add (opens the drawer), mini-cart confirm (silent refresh), mini-cart swap (silent refresh). Centralising means you tweak the refresh behaviour in one place. |
.quantity__input change listener | Mirrors the PDP qty stepper into Glood.upsell.setQty(). Without this, the slot budget (slotsPerMainQty × qty) stays at qty=1 in the panel even when the customer bumps the stepper to 3. |
registerFreeGiftCallback → refreshCartDrawer(true) | When the customer crosses a free-gift tier the Cart Transform Function injects the gift server-side. Opening the drawer surfaces the new line so the customer notices what they got. |
registerSwapCallback + registerConfirmCallback → refreshCartDrawer() | The mini-cart Confirm/Reselect flows fire /cart/change.js + /cart/add.js directly. The theme’s drawer DOM doesn’t know about those mutations unless you re-render — same logic from Step 6, registered here so it always wires up at boot regardless of which page loads. |
subscribe('cart-update', ...) | Dawn publishes cart-update via its internal PUB_SUB_EVENTS.cartUpdate after every Add-to-cart. Re-hydrating the mini-cart + re-initing the PDP upsell here keeps both surfaces in sync with the latest cart state. The setTimeout of 1s accommodates the theme’s own re-render pipeline. |
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 {% script %} 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:- 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-activeattribute. - 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.
- 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.
- 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).
- 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
| Symptom | Likely cause | Fix |
|---|---|---|
| Block renders in the default position, not in your variant anchor | Render mode is auto, not deferred | Theme Editor → block settings → Render mode = deferred |
| Mini-cart counter shows “1 / 2” when cart has 2 gifts | Older glood-upsell.js — pre-fix counters didn’t multiply by line quantity | Update to the latest embed; redeploy via shopify app deploy |
| 9 mains + 9 gifts at checkout after qty=3 add | Cart Transform Function not yet deployed OR commitPickSelections runs with stale state.cart | Verify the Function is deployed; confirm await Glood.upsell.commitPickSelections(...) runs INSIDE the .then(async ...) of /cart/add.js, not before |
id: 0 422 errors on mini-cart Reselect | hostLine variant-id resolution falling through (older glood-upsell.js) | Update embed |
| Upsell CTA shows under products not configured for the offer | Trigger-product gate missing in older embed | Update embed |
| Drawer paints with the host but no bundled gifts; resolves a moment later | refresh() not awaited | Confirm await Glood.upsell.refresh() is in the Add-to-cart chain (step 4) |