Skip to main content

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:

TierMechanismWhoWhere
1. Dashboard knobs121 UI settings (colors, fonts, layout, toggles)Non-technical merchantAdmin UI
2. Custom CSSCSS editor with full selector accessMerchant with CSS knowledgeAdmin UI
3. Advanced Template EditorPer-component HTML/CSS (Handlebars)DeveloperAdmin 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:

EventWhenKey Payload
marqo:readyWidget initialized{ config }
marqo:destroyWidget torn down{}
marqo:search.startSearch request fired{ query }
marqo:search.resultsResults rendered{ products, query, total, facets, container }
marqo:search.errorSearch failed{ error }
marqo:search.emptyZero results{ query }
marqo:card.renderEach card rendered{ product, element, index }
marqo:cta.clickCTA button clicked (cancelable){ product, handle, productUrl, shopifyProductId, element }
marqo:filter.changeFilter value changed{ field, type, value }
marqo:filter.clearAll filters cleared{}
marqo:sort.changeSort changed{ sort }
marqo:page.changePage changed{ page }
marqo:grid.injectedGrid 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">&times;</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:

  1. No merchants are using the app yet — zero migration cost, clean slate
  2. Dual-engine is more expensive long-term — every new interactive feature (variant selectors, add-to-cart, quickshop) would need to be built twice
  3. Vue eliminates ~1,600 lines of imperative DOM manipulation — swatch handlers, carousel handlers, CTA handlers, filter event wiring all become declarative Vue template bindings
  4. 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.
  5. 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 IDOverrides
marqo-product-card-templateProduct card HTML
marqo-filters-templateFilter sidebar
marqo-pagination-templatePagination controls
marqo-sort-templateSort dropdown
marqo-loading-templateLoading state
marqo-error-templateError state
marqo-no-results-templateNo 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:

FeatureHandlebarsVue
Static product cardsWorks fineSame
Custom badges from metafieldsWorks ({{#if field}})Same (v-if="field")
Variant selector on PLP cardNot possible without custom JSDeclarative in template
Add-to-cart on PLPNavigate to PDP only@click="addToCart(product)"
Live price updates on variant changeNot possible without custom JSReactive — automatic
Loading states (Adding → Added)Manual JS per buttonInline expression
Quickshop modalDOM Events bridge (theme-specific)Could be built entirely in template
Inventory-aware UIManual 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

MethodPurposeRebuy equivalent
addToCart(variantId, qty)Shopify Cart API (/cart/add.js)addToCart(product)
selectVariant(product, optionName, value)Change variant, update image/priceselectVariant(product)
formatPrice(cents)Currency formattingformatMoney()
variantAvailable(variant)Check inventoryvariantAvailable(variant)
variantPrice(product, variant)Variant-specific pricevariantPrice(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.

PhaseComponentsWhy this orderEffort
1Product cards + grid appHighest value — unlocks variant selectors, add-to-cart, quickshop. Most complex.3-4 days
2Filters (categorical, range, hierarchical) + accordion + mobile drawerSecond most complex — three filter types with accordion state + mobile responsive. Eliminates ~610 lines of imperative handler code.2-3 days
3Pagination, Sort, Items Per Page, Results CountSimple components — v-for for page buttons, v-model for dropdowns.0.5-1 day
4Loading, Error, No Results statesTrivial — 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:

TierSupport level
1. Dashboard knobsFully supported
2. Custom CSSSupported (basic guidance)
3. Advanced Template EditorSupported for default templates. Custom edits are merchant's responsibility.
4. Theme template overridesUnsupported — "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:

FeatureHandlebars (today)Vue (Tier 4)
Static product cardsWorks fineSame
Custom badges from metafieldsWorks ({{#if field}})Same (v-if="field")
Variant selector on PLP cardManual JS (swatch handlers)Declarative in template
Add-to-cart on PLPNavigate to PDP or manual JS@click="addToCart(product)"
Live price updates on variant changeNot possible without custom JSReactive — automatic
Loading states (Adding → Added)Manual JS per buttonInline expression
Quickshop modalDOM Events bridge (theme-specific)Could be built entirely in template
Inventory-aware UIManual 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:

TierMechanismSupport
Dashboard knobsNo-code settings: colors, fonts, image ratio, CTA styling, review starsFully supported
Custom CSSCSS editor in Rebuy dashboard targeting .rebuy-* selectorsSupported (basic guidance)
Custom Vue templateFull template override via Shopify theme snippetUnsupported — "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

FeatureEffortUnlocksPriority
1. DOM Events1 dayQuickshop, analytics, any JS integrationP0 — shipped
2. Grid Injections1 dayPromo tiles, video tiles, editorial contentP0 — shipped
3. Vue Migration + Tier 4 Overrides7-11 daysReactive templates, variant selectors, add-to-cart, theme overrides, agency workflowP0 — 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)