# 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.

<br/>

## Basic Liquid Template

Add the template and configuration to [`snippets/nimstrata.liquid`](/shopify/theme-installation/nimstrata-liquid/). Render that snippet before Shopify loads the App Block assets.

```liquid snippets/nimstrata.liquid
{%- 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.

<br/>

## 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

```js
{
  id: 'row-banner',
  afterProduct: 4,
  columnSpan: 'full',
  templateId: 'row-banner-template',
}
```

### Two columns after the first product

```js
{
  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

```js
{
  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.

<br/>

## Configuration Reference

Every entry requires a unique `id`, an `afterProduct` position, and exactly one content source: `templateId` or `component`.

| Property       | Required   | Value                                   | Behavior {.compact}                                                                                                              |
| -------------- | ---------- | --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `id`           | Yes        | Non-empty string                        | Stable identity for rendering, diagnostics, and the wrapper's `data-rcc-grid-content` value. Must be unique.                     |
| `afterProduct` | Yes        | Integer `0` or greater                  | Number of rendered product cards that come before the content. Use `0` to place content before the first product.                |
| `templateId`   | One source | `<template>` element ID                 | Clones server-rendered template content. Do not include `#`.                                                                     |
| `component`    | One source | Registered custom-element name          | Creates the named Web Component. Its name must contain a hyphen, such as `store-grid-ad`.                                        |
| `attributes`   | No         | String, number, or boolean values       | Sets HTML attributes on a configured Web Component. `true` creates an empty attribute; `false`, `null`, and `undefined` omit it. |
| `properties`   | No         | Object                                  | Assigns JavaScript properties to a configured Web Component before it connects.                                                  |
| `columnSpan`   | No         | `1`, `2`, or `'full'`                   | Defaults to one column. `'full'` uses the full explicit grid width.                                                              |
| `rowSpan`      | No         | `1` or `2`                              | Defaults to one row. A two-row span reserves grid tracks; it does not set a fixed pixel height.                                  |
| `className`    | No         | String                                  | Adds one or more classes to the App Block-owned grid-content wrapper.                                                            |
| `pageTypes`    | No         | `['search']`, `['collection']`, or both | Limits the entry to the selected App Block page types. Omit it to use the entry on both.                                         |

Malformed entries are ignored. If more than one applicable entry uses the same `id`, the first one is used.

<br/>

## 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.

```js assets/store-grid-ad.js
(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:

```js
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:

```ts
{
  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.

<br/>

## 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:

```js
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.

<br/>

## Pagination and Responsive Behavior

- Numbered pagination applies positions to each newly displayed page. An entry with `afterProduct: 4` appears 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 `afterProduct` position 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: 4` remains 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.

<br/>

## Styling the Wrapper

Every inserted entry is a direct grid child with these public hooks:

```html
<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.

```css
.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.

<br/>

## 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 `textContent` in 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:ready`](/shopify/theme-installation/app-block-events/#search-grid-ready) event 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.
