Content Inside Search and Collection Grids
Search Results and Product Grid collection App Blocks can place promotions, ads, editorial panels, and other theme content between product cards. This is a developer integration configured through window.Nimstrata.search.gridContent; there is no App Block setting or app UI for it.
Grid content can come from either:
- A Liquid-rendered HTML
<template>for static, server-rendered content - A registered Web Component for interactive or search-aware content
The App Block owns placement and a wrapper around the content. The template or Web Component owns everything inside that wrapper.
Basic Liquid Template
Add the template and configuration to snippets/nimstrata.liquid. Render that snippet before Shopify loads the App Block assets.
{%- if request.page_type == 'collection' -%}
<template id="collection-grid-promotion">
<aside class="collection-grid-promotion" aria-label="Collection promotion">
<h2>Explore more from this collection</h2>
<a href="/collections/all">Shop all products</a>
</aside>
</template>
<script>
window.Nimstrata = window.Nimstrata || {};
window.Nimstrata.search = window.Nimstrata.search || {};
window.Nimstrata.search.gridContent = [
{
id: 'collection-promotion',
afterProduct: 4,
columnSpan: 'full',
templateId: 'collection-grid-promotion',
pageTypes: ['collection'],
},
];
</script>
{%- endif -%}
This places a full-width promotion after the fourth rendered product. In a four-column desktop grid, that is immediately after the first product row.
The <template> must be outside .rcc-search. The App Block clears its own root before Preact renders the live experience, so content nested inside that root does not survive startup. Template IDs must be unique in the document.
Liquid runs on Shopify's server before the page reaches the browser. The App Block clones the resulting HTML; it cannot evaluate Liquid supplied later in a JavaScript string.
Placement Examples
afterProduct counts rendered product cards, starting at the beginning of the current grid. It does not count earlier grid-content entries.
Full width after the first row of a four-column grid
{
id: 'row-banner',
afterProduct: 4,
columnSpan: 'full',
templateId: 'row-banner-template',
}
Two columns after the first product
{
id: 'double-tile',
afterProduct: 1,
columnSpan: 2,
templateId: 'double-tile-template',
}
In a four-column grid, the first product occupies column one and this content uses columns two and three. The next product can occupy column four.
Two columns and two rows after the first two products
{
id: 'large-feature',
afterProduct: 2,
columnSpan: 2,
rowSpan: 2,
templateId: 'large-feature-template',
}
In a four-column grid, the first two products occupy columns one and two. This content can then occupy columns three and four across two rows while later products continue in the available cells.
These examples are alternatives. Add only the entries that the storefront needs, or use different afterProduct values when several entries should appear together. Entries with the same afterProduct value render in array order.
Configuration Reference
Every entry requires a unique id, an afterProduct position, and exactly one content source: templateId or component.
Malformed entries are ignored. If more than one applicable entry uses the same id, the first one is used.
Web Component Content
Use a Web Component when content needs interactive behavior, third-party ad logic, or the active search query. Register the component before the Search Results or Product Grid App Block renders.
(function () {
'use strict';
class StoreGridAd extends HTMLElement {
set campaign(value) {
this._campaign = value;
this.render();
}
set searchGridContext(value) {
this._searchGridContext = value;
this.render();
}
connectedCallback() {
this.render();
}
render() {
if (!this.isConnected || !this._campaign) return;
this.replaceChildren();
const panel = document.createElement('aside');
panel.className = 'store-grid-ad__panel';
panel.setAttribute('aria-label', 'Featured promotion');
const heading = document.createElement('h2');
heading.textContent = this._campaign.heading;
const context = document.createElement('p');
context.textContent = this._searchGridContext?.activeQuery
? `Selected for ${this._searchGridContext.activeQuery}`
: 'Featured for this collection';
panel.append(heading, context);
this.append(panel);
}
}
customElements.define('store-grid-ad', StoreGridAd);
})();
Configure it through the same array:
window.Nimstrata.search.gridContent = [
{
id: 'campaign-ad',
afterProduct: 2,
columnSpan: 2,
rowSpan: 2,
component: 'store-grid-ad',
attributes: {
placement: 'search-grid',
sponsored: true,
},
properties: {
campaign: {
id: 'summer-2026',
heading: 'The summer edit',
},
},
},
];
The App Block assigns a searchGridContext property before connecting the element:
{
activeQuery: string;
pageType: 'collection' | 'search';
}
A component can be recreated when its definition, page type, or active query changes. Its property setters, connectedCallback, event listeners, and cleanup should therefore be safe to run more than once.
Updating Content After Startup
Initial content should be assigned directly to gridContent before the App Block bundle starts. After the bundle has loaded, replace the array with the installed setter:
window.Nimstrata.search.setGridContent([
{
id: 'late-campaign',
afterProduct: 4,
columnSpan: 'full',
component: 'store-grid-ad',
properties: {
campaign: {
id: 'autumn-2026',
heading: 'The autumn edit',
},
},
},
]);
setGridContent() replaces the whole array and updates every mounted Search Results or Product Grid App Block. It is available only after search-react.js has loaded. Directly mutating the existing array after startup does not trigger an immediate render.
Pagination and Responsive Behavior
- Numbered pagination applies positions to each newly displayed page. An entry with
afterProduct: 4appears after the fourth product on every numbered page where at least four products render. - Load more counts across all currently loaded product groups. Each configured entry appears once, as soon as its
afterProductposition has loaded. - An entry whose position has not loaded does not render yet. Grid content does not render when there are no product cards.
- Positions are product-based, not visual-row-based.
afterProduct: 4remains after the fourth product if a four-column grid changes to two columns on a smaller screen. - A two-column span becomes full width in the App Block's built-in one-column layouts. A
'full'span always follows the grid's current explicit width. - CSS Grid does not move later product cards ahead of content to fill an earlier gap. Choose positions that leave enough columns for the requested span when visual continuity matters.
rowSpan: 2 controls grid occupancy rather than height. Give images explicit width and height, use aspect-ratio, or reserve another stable size so late-loading promotions and ads do not cause layout shifts.
Styling the Wrapper
Every inserted entry is a direct grid child with these public hooks:
<div
class="rcc-search__grid-content campaign-grid-content"
data-rcc-grid-content="campaign-ad"
></div>
className adds the custom class shown above. Span modifier classes are also present, but storefront styles should normally target the stable base class, custom class, or data attribute.
.rcc-search__grid-content[data-rcc-grid-content='campaign-ad'] {
min-height: 320px;
overflow: hidden;
border-radius: 12px;
}
.campaign-grid-content > store-grid-ad {
display: block;
height: 100%;
}
Do not add .rcc-search__product to inserted content. That class is reserved for product-card behavior and metrics.
Rendering and Security Boundaries
- Grid content is a JavaScript enhancement. It is not added to the App Blocks' no-JavaScript product fallback, product result counts, or product JSON-LD.
- Template markup must be trusted theme-authored Liquid or HTML. Escape shopper and API values in Liquid, or assign them with
textContentin a Web Component. - Do not place
<script>tags, inline event-handler attributes, or runtime Liquid inside a content template. Load behavior from a theme or app-extension JavaScript asset and use a Web Component. - A missing template or unregistered component renders nothing and writes a warning to the browser console.
- The public
retail-connect:search-grid:readyevent still reports product counts only. Asynchronous work inside a Web Component can finish after that event.
Test the integration on search and collection pages, numbered or load-more pagination, all enabled grid-column controls, and narrow mobile widths before publishing the theme.