A 'your quiz picked this' marker on the product page
Goal: a shopper finishes the quiz, opens a recommended product, and sees “Recommended for you by the quiz” under the title; when the quiz picked a variant, “Your quiz pick: Large” lights up while that variant is selected and dims on another. A product the quiz never showed has no badge.
Uses: Storefront JS API, product.shown, quiz.finished (products[]) through the document mirror, localStorage, and a Liquid block on the product template. Nothing to install; no API key. Events fire on the Plus and Enterprise plans.
Remember the picks (every page)
Section titled “Remember the picks (every page)”product.shown fires once per product card the session showed, with product_id, variant_id (null when the card is not locked to a variant) and the product object, whose variants[] carry each variant’s title. quiz.finished repeats them all in products[]. This script runs on every page (it is tiny) and only works when a quiz fires; listening on document means it needs no octaneai.on registration and works when loaded before the quiz script.
<script> (function () { var KEY = 'octane:quiz_picks'; var TTL_MS = 7 * 24 * 60 * 60 * 1000;
function numeric(id) { return id == null ? null : String(id).replace(/^.*\//, ''); } function read() { try { return JSON.parse(localStorage.getItem(KEY)); } catch (e) { return null; } } function write(store) { try { localStorage.setItem(KEY, JSON.stringify(store)); } catch (e) {} }
function remember(sessionId, shown) { var store = read(); if (!store || store.session_id !== sessionId) store = { session_id: sessionId, products: [] }; shown.forEach(function (p) { var productId = numeric(p.product_id); var variantId = numeric(p.variant_id); var known = store.products.some(function (q) { return q.product_id === productId && q.variant_id === variantId; }); if (known) return; var variant = p.product && p.product.variants.find(function (v) { return numeric(v.variant_id) === variantId; }); store.products.push({ product_id: productId, variant_id: variantId, variant_title: variant ? variant.title : null }); }); store.expires_at = Date.now() + TTL_MS; write(store); }
document.addEventListener('octaneai:product.shown', function (e) { var o = e.detail.data.object; remember(o.session_id, [o]); }); document.addEventListener('octaneai:quiz.finished', function (e) { var o = e.detail.data.object; remember(o.session_id, o.products); }); })();</script>A new session_id replaces the list, so a retake starts clean; nothing but ids, variant titles and a timestamp is stored.
Show the badge (product template)
Section titled “Show the badge (product template)”Put this in a Custom Liquid block inside the main product section (the theme editor offers one on Online Store 2.0 themes). Liquid fills in the product’s numeric id and the variant selected on load; the script reads the store and follows the variant picker.
<div id="octane-pick" hidden data-product-id="{{ product.id }}" data-initial-variant="{{ product.selected_or_first_available_variant.id }}"> <span id="octane-pick-text"></span></div><style> #octane-pick { display: inline-block; margin: 8px 0; padding: 4px 12px; border-radius: 999px; font: 13px system-ui, sans-serif; background: #ecfdf5; color: #065f46; border: 1px solid #a7f3d0; } #octane-pick.other { opacity: 0.5; }</style><script> (function () { var el = document.getElementById('octane-pick'); var store = null; try { store = JSON.parse(localStorage.getItem('octane:quiz_picks')); } catch (e) {} if (!store || !store.products || Date.now() > store.expires_at) return; var mine = store.products.filter(function (p) { return p.product_id === el.dataset.productId; }); if (!mine.length) return; var picked = mine.filter(function (p) { return p.variant_id; }); var text = document.getElementById('octane-pick-text');
function currentVariant() { var fromUrl = new URLSearchParams(location.search).get('variant'); if (fromUrl) return fromUrl; var input = document.querySelector('form[action*="/cart/add"] input[name="id"]'); return (input && input.value) || el.dataset.initialVariant; }
function render() { el.hidden = false; if (!picked.length) { text.textContent = 'Recommended for you by the quiz'; return; } var current = String(currentVariant()); var match = picked.find(function (p) { return p.variant_id === current; }); el.classList.toggle('other', !match); text.textContent = 'Your quiz pick: ' + (match ? (match.variant_title || 'this option') : picked.map(function (p) { return p.variant_title || p.variant_id; }).join(', ')); }
render(); document.addEventListener('change', function (e) { if (e.target && e.target.name === 'id') render(); }, true); setInterval(render, 500); })();</script>The current variant comes from ?variant= in the URL (Dawn rewrites it on every change), else the cart form’s hidden id input, else the variant Liquid selected on load. The change listener and the half-second poll cover themes that update the input without an event.
Errors to handle
Section titled “Errors to handle”localStoragethrows in some private modes and is empty in an in-app browser (Instagram, the Shop app) that never ran the quiz; both paths leave the badge hidden, never wrong.productisnullon aproduct.shownfor a product the quiz never received; the pick is stored without a variant title and the badge says “this option”.
Things to know
Section titled “Things to know”- Storage is per origin: a quiz on
quiz.brand.comand a product page onwww.brand.comdo not share it. SwaplocalStoragefor a cookie on.brand.comwhen the quiz lives on a subdomain. - Safari clears script-written storage after seven days without a visit; the seven-day expiry above matches that.
product.shownalso fires for manually placed products and for cards a rule reveals; filter onrecommendation_source(manual,rankings,points,smart) in the first script if only picks should badge.- Two quizzes on one store overwrite each other’s picks (one
session_idat a time); key the store byquiz_idwhen both should badge. - A product deleted from Shopify still comes through
product.shown; there is no product page to badge, so it is harmless. - Headless storefronts render the same badge from the same storage key in the product route, with the selected variant from the route’s own state.
