Storefront Extensibility Framework
Future extension plan for the Shopify storefront search widget. Adds a tiered customization model and extensibility hooks comparable to Rebuy Smart Search.
Current State
The storefront widget supports three tiers of customization:
| Tier | Mechanism | Who | Where |
|---|---|---|---|
| 1. Dashboard knobs | 121 UI settings (colors, fonts, layout, toggles) | Non-technical merchant | Admin UI |
| 2. Custom CSS | CSS editor with full selector access | Merchant with CSS knowledge | Admin UI |
| 3. Advanced Template Editor | Per-component HTML/CSS (Handlebars) | Developer | Admin UI (stored in DDB) |
Missing capabilities:
- No JS extensibility hooks (can't integrate quickshop, analytics, or third-party widgets)
- No way to inject non-product content into the results grid (promo tiles, video, editorial)
- No theme-level template override (merchants who prefer Shopify theme workflow over our admin UI)
Proposed Features
Feature 1: DOM Event System
Emit CustomEvents at lifecycle points so merchants can hook in custom behavior from a <script> tag in their Shopify theme. Same pattern as Rebuy's event listeners (rebuy.add, rebuy.view, etc.).
Events:
| Event | When | Key Payload |
|---|---|---|
marqo:ready | Widget initialized | { config } |
marqo:destroy | Widget torn down | {} |
marqo:search.start | Search request fired | { query } |
marqo:search.results | Results rendered | { products, query, total, facets, container } |
marqo:search.error | Search failed | { error } |
marqo:search.empty | Zero results | { query } |
marqo:card.render | Each card rendered | { product, element, index } |
marqo:cta.click | CTA button clicked (cancelable) | { product, handle, productUrl, shopifyProductId, element } |
marqo:filter.change | Filter value changed | { field, type, value } |
marqo:filter.clear | All filters cleared | {} |
marqo:sort.change | Sort changed | { sort } |
marqo:page.change | Page changed | { page } |
marqo:grid.injected | Grid injections rendered | { injections } |
marqo:cta.click is cancelable — calling e.preventDefault() stops default navigation to the PDP. This enables quickshop modals, custom add-to-cart flows, and any click override.
Implementation: A dispatchMarqoEvent() helper in dom-manager.ts, called from existing rendering and event handler functions. Follows the existing dispatchPageRerenderedEvent() pattern.
Example — quickshop bridge for a customer using theme web components:
<script>
document.addEventListener('marqo:cta.click', function(e) {
e.preventDefault();
var modal = document.querySelector('#MarqoQuickAdd');
if (!modal) {
document.body.insertAdjacentHTML('beforeend',
'<quick-add-modal id="MarqoQuickAdd" class="quick-add-modal">' +
'<div role="dialog" class="quick-add-modal__content global-settings-popup" tabindex="-1">' +
'<button type="button" class="quick-add-modal__toggle" aria-label="Close">×</button>' +
'<div class="quick-add-modal__content-info"></div>' +
'</div>' +
'</quick-add-modal>'
);
modal = document.querySelector('#MarqoQuickAdd');
}
e.detail.element.setAttribute('data-product-url', e.detail.productUrl);
modal.show(e.detail.element);
});
</script>
Effort: ~1 day.
Feature 2: Grid Injections
Insert non-product content (promo banners, video tiles, editorial) at configurable positions within the product results grid. Each injection is a standalone grid item.
Visual layout:
+----------+ +----------+ +----------+ +----------+
|Product 1 | |Product 2 | |Product 3 | |Product 4 |
+----------+ +----------+ +----------+ +----------+
+-----------------------------------------------------+
| Grid Injection (position: 4, span: 4) |
| "Complete Your Routine -- Shop Routines" |
+-----------------------------------------------------+
+----------+ +----------+ +----------+ +----------+
|Product 5 | |Product 6 | |Product 7 | |Product 8 |
+----------+ +----------+ +----------+ +----------+
Data model — new UIComponent:
"grid_injections": UIComponent(
name="Grid Injections",
enabled=True,
value={
"items": [
# {
# "id": "spring-promo",
# "position": 4, # Insert after 4th product
# "span": 4, # Span all grid columns
# "html": "<div>...</div>",
# "css": ".promo { ... }",
# "enabled": True,
# }
]
},
)
Rendering: Array splice in renderResults() before safeSetInnerHTML(). Injections are <div class="marqo-grid-injection" style="grid-column: span N"> elements inserted between product card HTML strings.
Admin UI: A "Grid Content" section with add/remove injection items, each with position, span, HTML editor, CSS editor, and enable/disable toggle.
After render: Fires marqo:grid.injected event (Feature 1) so third-party scripts (Videowise, Visually) can hydrate their containers.
Effort: ~1 day (rendering logic + backend model + admin UI + converter).
Feature 3: Theme Template Overrides (Tier 4) + Vue Migration
This feature has two phases that we now plan to ship together as part of a full Vue migration (see tmp/agent-plans/2026-04-10-vue-migration/plan.md for implementation details).
Decision: Rather than building a dual-engine system (Handlebars for Tiers 1-3, Vue only at Tier 4), we are migrating the entire rendering layer to Vue. This decision was made because:
- No merchants are using the app yet — zero migration cost, clean slate
- Dual-engine is more expensive long-term — every new interactive feature (variant selectors, add-to-cart, quickshop) would need to be built twice
- Vue eliminates ~1,600 lines of imperative DOM manipulation — swatch handlers, carousel handlers, CTA handlers, filter event wiring all become declarative Vue template bindings
- The architecture doesn't change — UIComponent model, DDB storage, API, admin UI, settings converter, CSS variables, DOM events all stay identical. Only the rendering layer (~30% of widget code) changes.
- Net bundle size increase is only ~15KB — Vue (~34KB) replaces Handlebars (~20KB)
Tier 4: Theme template overrides
Merchants place custom Vue templates in their Shopify theme code. Our widget detects them and uses them instead of the DDB settings template.
Detection: The widget scans the DOM for <script> tags with matching IDs before rendering:
function getTemplate(componentKey: string, fallback: string): string {
const el = document.querySelector(
`script#marqo-${componentKey}-template[type="text/template"]`
);
if (!el) return fallback; // Use DDB template (Tier 3)
return el.textContent || fallback; // Use theme override (Tier 4)
}
Supported override IDs:
| Script ID | Overrides |
|---|---|
marqo-product-card-template | Product card HTML |
marqo-filters-template | Filter sidebar |
marqo-pagination-template | Pagination controls |
marqo-sort-template | Sort dropdown |
marqo-loading-template | Loading state |
marqo-error-template | Error state |
marqo-no-results-template | No results state |
Per-component independence: Each component checks for its own theme override independently. A merchant can override product cards while letting filters, pagination, and sort use the DDB templates:
<!-- Only override the product card — everything else uses DDB settings -->
<script id="marqo-product-card-template" type="text/template">
{% raw %}
<div class="marqo-product-card">
<span v-if="salePercent" class="marqo-sale-badge">SAVE {{ salePercent }}%</span>
<a :href="productUrl">
<img :src="imageUrl" :alt="title" />
<h3>{{ title }}</h3>
<span>{{ price }}</span>
</a>
<button v-if="ctaEnabled" @click="addToCart(product._id)">
{{ ctaButtonText }}
</button>
</div>
{% endraw %}
</script>
Fallback chain per component:
Tier 4: Theme override (Shopify theme snippet) ← checked first, wins if present
|
v (if not found)
Tier 3: Advanced Template Editor (DDB settings) ← editable from admin UI
|
v (if not found)
Tier 2: Custom CSS (additive — always applied)
|
v (applied on top of)
Tier 1: Dashboard knobs (CSS variables — always applied)
Tiers 1 and 2 always apply regardless of which tier provides the HTML template — CSS variables and custom CSS are injected independently.
Why per-component matters: A merchant who overrides the product card via Tier 4 still gets all their Tier 1 dashboard knobs for filters, pagination, sorting, and other components. They don't have to override everything.
What Vue enables that Handlebars cannot
Based on research into Rebuy's custom template system, Vue templates unlock interactive state changes on product cards without custom JS code. These are real patterns from Rebuy's production templates:
Variant selector with live price update:
<select v-model="selectedVariantId" @change="selectVariant(product)">
<option v-for="variant in product.variants"
:value="variant.id"
:disabled="!variantAvailable(variant)"
v-html="formatVariantOptionTitle(variant)">
</option>
</select>
<span v-html="formatPrice(variantPrice(product, selectedVariant))"></span>
User picks a shade/size → price, image, and availability update instantly. No manual DOM manipulation.
Add-to-cart with reactive loading states:
<button @click="addToCart(product)"
:disabled="!variantAvailable(selectedVariant)"
:class="{ working: status !== 'ready' }">
{{ status === 'working' ? 'Adding...' : ctaButtonText }}
</button>
Button text, disabled state, and CSS class all react to status as the cart request progresses.
Inventory-aware option filtering:
<option v-for="variant in product.variants"
:disabled="!variantAvailable(variant)"
:class="{ 'oos': !variantAvailable(variant) }">
Out-of-stock variants automatically greyed out based on live data.
Summary — Handlebars vs Vue by use case:
| Feature | Handlebars | Vue |
|---|---|---|
| Static product cards | Works fine | Same |
| Custom badges from metafields | Works ({{#if field}}) | Same (v-if="field") |
| Variant selector on PLP card | Not possible without custom JS | Declarative in template |
| Add-to-cart on PLP | Navigate to PDP only | @click="addToCart(product)" |
| Live price updates on variant change | Not possible without custom JS | Reactive — automatic |
| Loading states (Adding → Added) | Manual JS per button | Inline expression |
| Quickshop modal | DOM Events bridge (theme-specific) | Could be built entirely in template |
| Inventory-aware UI | Manual JS | :disabled="!variantAvailable(variant)" |
The pattern: Handlebars is sufficient for display. Vue is needed for interactivity on the card itself.
Practical impact: Building a quickshop-style experience (variant selector + add-to-cart + live pricing on the PLP card) currently requires us to write custom JS for each merchant. With Vue templates, the merchant's agency builds it themselves in a theme snippet — we provide the data and methods.
Variant data for interactive templates
Variant data (for selectors, inventory-aware UI) is fetched on demand from Shopify's public endpoint when a user interacts with a variant selector:
User interacts with variant selector
|
v
fetch('/products/{handle}.json') <- public Shopify endpoint, no auth needed
|
v
{ product: { variants: [{ id, title, price, available, options }] } }
|
v
Vue component populates selector reactively, caches result
We have shopifyProductId and productHandle in every search result. On-demand fetch is better than indexing variants because:
- No indexer changes needed
- Always fresh inventory data (not stale until re-index)
- ~100-200ms latency on first interaction, cached after
Methods available in Vue templates
| Method | Purpose | Rebuy equivalent |
|---|---|---|
addToCart(variantId, qty) | Shopify Cart API (/cart/add.js) | addToCart(product) |
selectVariant(product, optionName, value) | Change variant, update image/price | selectVariant(product) |
formatPrice(cents) | Currency formatting | formatMoney() |
variantAvailable(variant) | Check inventory | variantAvailable(variant) |
variantPrice(product, variant) | Variant-specific price | variantPrice(product, variant) |
Implementation phasing by component
Why phase: Product cards are where 95% of the interactivity demand is (variant selectors, add-to-cart, quickshop). Other components (filters, pagination, sort) benefit from Vue but are lower priority and less complex. The architecture supports all components from day one — but the implementation work (computed properties, methods, data bindings) is done incrementally.
| Phase | Components | Why this order | Effort |
|---|---|---|---|
| 1 | Product cards + grid app | Highest value — unlocks variant selectors, add-to-cart, quickshop. Most complex. | 3-4 days |
| 2 | Filters (categorical, range, hierarchical) + accordion + mobile drawer | Second most complex — three filter types with accordion state + mobile responsive. Eliminates ~610 lines of imperative handler code. | 2-3 days |
| 3 | Pagination, Sort, Items Per Page, Results Count | Simple components — v-for for page buttons, v-model for dropdowns. | 0.5-1 day |
| 4 | Loading, Error, No Results states | Trivial — conditional v-if/v-else-if based on search state. | 0.5 day |
All phases ship the Tier 4 detection for the components they implement. After Phase 1, merchants can override product cards via theme snippets. After Phase 2, they can override filters too. Etc.
Total: ~7-11 days for the full migration. See tmp/agent-plans/2026-04-10-vue-migration/plan.md for detailed implementation spec and tmp/agent-plans/2026-04-10-vue-migration/feature-audit.md for the complete per-feature migration audit.
Support boundary
Following Rebuy's model:
| Tier | Support level |
|---|---|
| 1. Dashboard knobs | Fully supported |
| 2. Custom CSS | Supported (basic guidance) |
| 3. Advanced Template Editor | Supported for default templates. Custom edits are merchant's responsibility. |
| 4. Theme template overrides | Unsupported — "at your own discretion" |
Rebuy's exact policy for reference:
"We do not offer support for third-party plugins or customizations like custom templates. Support for custom template work falls outside the scope of the Rebuy Support Team. Using custom templates is at your own discretion."
What Vue enables that Handlebars cannot
Based on research into Rebuy's custom template system, Vue templates unlock interactive state changes on product cards without custom JS code. These are real patterns from Rebuy's production templates:
Variant selector with live price update:
<select v-model="product.selected_variant_id" @change="selectVariant(product)">
<option v-for="variant in product.variants"
:value="variant.id"
:disabled="!variantAvailable(variant)"
v-html="formatVariantOptionTitle(variant)">
</option>
</select>
<span v-html="formatMoney(variantPrice(product, product.selected_variant))"></span>
When the user picks a different shade/size, the price, image, and availability update instantly. With Handlebars, this requires ~100 lines of imperative JS outside the template.
Add-to-cart with reactive loading states:
<button @click="addToCart(product)"
:disabled="!variantAvailable(product.selected_variant)"
:class="{ working: product.status !== 'ready' }">
{{ product.status === 'working' ? 'Adding...' : 'Add to Cart' }}
</button>
Button text, disabled state, and CSS class all react to product.status as the cart request progresses. No manual DOM manipulation.
Swatch-driven image swap:
<img :src="productImage(product)" />
<input type="radio"
@change="selectVariantByOption(product, option.name, value)"
:checked="hasSwatchOptionSelected(product, value)" />
Clicking a color swatch updates the selected variant, which reactively changes the image source. We currently implement this with attachSwatchHandlers() — ~100 lines of imperative DOM code.
Inventory-aware option filtering:
<option v-for="variant in product.variants"
:disabled="!variantAvailable(variant)"
:class="{ 'oos': !variantAvailable(variant) }">
Out-of-stock variants are automatically greyed out based on live data. The template declares the rule, Vue enforces it.
Summary — Handlebars vs Vue by use case:
| Feature | Handlebars (today) | Vue (Tier 4) |
|---|---|---|
| Static product cards | Works fine | Same |
| Custom badges from metafields | Works ({{#if field}}) | Same (v-if="field") |
| Variant selector on PLP card | Manual JS (swatch handlers) | Declarative in template |
| Add-to-cart on PLP | Navigate to PDP or manual JS | @click="addToCart(product)" |
| Live price updates on variant change | Not possible without custom JS | Reactive — automatic |
| Loading states (Adding → Added) | Manual JS per button | Inline expression |
| Quickshop modal | DOM Events bridge (theme-specific) | Could be built entirely in template |
| Inventory-aware UI | Manual JS | :disabled="!variantAvailable(variant)" |
The pattern: Handlebars is sufficient for display. Vue is needed for interactivity on the card itself. Every time we want something that responds to user action without a page reload (variant picker, add-to-cart, loading states, swatch image swap), Handlebars requires imperative JS outside the template. Vue lets the template express the behavior declaratively.
Practical impact: Building a quickshop-style experience (variant selector + add-to-cart + live pricing on the PLP card) currently requires us to write custom JS in the storefront widget for each merchant. With Vue templates, the merchant's agency builds it themselves in a theme snippet — we provide the data and methods.
Rebuy's Support Policy for Custom Templates
Rebuy explicitly states that custom template customization is unsupported:
"Our support services cover all aspects directly linked to Rebuy, providing assistance and guidance for standard features and functionalities. Please note that we do not offer support for third-party plugins or customizations like custom templates as outlined in this document."
"Support for custom template work falls outside the scope of the Rebuy Support Team. Using custom templates is at your own discretion."
Source: How To Use a Custom Template for Smart Search Quick View and Results page
This is the industry-standard boundary: the platform provides the feature, documentation, and starter templates. Once a merchant customizes, they own the result. We should adopt the same policy for our Tier 3 (Advanced Template Editor) and future Tier 4 (Theme Template Overrides).
How Rebuy's three-tier customization model works
For context, Rebuy Smart Search uses a similar tiered approach:
| Tier | Mechanism | Support |
|---|---|---|
| Dashboard knobs | No-code settings: colors, fonts, image ratio, CTA styling, review stars | Fully supported |
| Custom CSS | CSS editor in Rebuy dashboard targeting .rebuy-* selectors | Supported (basic guidance) |
| Custom Vue template | Full template override via Shopify theme snippet | Unsupported — "at your own discretion" |
When a merchant activates a custom template, Rebuy's widget detects a <script> tag with a matching ID in the DOM and uses it instead of the built-in default. It's a complete replacement — the entire component template is overridden. Dashboard settings only continue to work if the custom template preserves the Vue data bindings that reference them (e.g., v-if="config.language.title"). If a merchant removes a binding while customizing, that dashboard setting silently stops working.
This is the same trade-off our Tier 3/4 would have: custom templates give full control at the cost of maintaining compatibility with dashboard knobs.
Implementation Priority
| Feature | Effort | Unlocks | Priority |
|---|---|---|---|
| 1. DOM Events | 1 day | Quickshop, analytics, any JS integration | P0 — shipped |
| 2. Grid Injections | 1 day | Promo tiles, video tiles, editorial content | P0 — shipped |
| 3. Vue Migration + Tier 4 Overrides | 7-11 days | Reactive templates, variant selectors, add-to-cart, theme overrides, agency workflow | P0 — next |
Features 1 and 2 are shipped. Feature 3 (Vue migration with Tier 4 support) is the next major work item — it eliminates the Handlebars limitation and gives us full Rebuy parity on template customization.
References
- Rebuy Custom Templates — Rebuy's equivalent of Feature 3 (theme-level template overrides)
- Rebuy Widget Custom Templates — Vue template code examples (variant selectors, add-to-cart, swatches)
- Rebuy Quick View Templates — Quickview Vue template with search, variant selection, and cart integration
- Rebuy Results Page Templates — Results page Vue template with filters, sorting, and product grid
- Rebuy Widget Event Listeners — Rebuy's equivalent of Feature 1 (20+ lifecycle events)
- Rebuy Widget Methods — Rebuy's programmatic API (addToCart, show/hide, cart observation)
- Rebuy Smart Search Results Page — Rebuy's no-code dashboard settings (comparable to our Tiers 1-2)
- Rebuy Custom Template Support Policy — "We do not offer support for custom templates"
- Rebuy JS Callbacks for Custom UX — Event listener patterns for merchant integrations
- Internal:
docs/dev/storefront-extensibility.md— developer guide for shipped features (DOM Events + Grid Injections) - Internal:
docs/plans/competitive-analysis-rebuy-2026-04-10.md— full Rebuy vs Marqo competitive comparison - Internal:
tmp/agent-plans/2026-04-10-extensibility-framework/plan.md— detailed implementation spec for Features 1 and 2 - Internal:
tmp/agent-plans/2026-04-10-vue-migration/plan.md— Vue migration implementation plan - Internal:
tmp/agent-plans/2026-04-10-vue-migration/feature-audit.md— per-feature migration audit (18 features)