Skip to main content

Overview

This guide is the SDK equivalent of Rendering banners in Liquid. The browse payload, layout selection, and grid math are identical. With @commerce-blocks/sdk, sdk.collection() makes the HTTP call, deduplicates requests, caches the response, and exposes data.banners on reactive state you can subscribe to. Use this version when you have already installed @commerce-blocks/sdk in your theme. For a no-dependencies version, see the Liquid + Fetch guide. Hero banners are horizontal banners spanning the entire product grid above the first product row. They are not restricted to inline tile sizes. Inline banners can span one or more columns, bounded by the grid rather than a fixed 1×1 or 2×2 preset. Inline sizes are grid-cell spans, not image dimensions in pixels. The editor can resize inline banners across columns and up to four rows.

Prerequisites

  • The Layers Shopify app installed and the theme app extension enabled in Online Store → Themes → Customize → App embeds.
  • The SDK installed and initialized in your theme. See SDK installation.
  • At least one merchandising rule with banners attached. See Add a banner to a rule.

The banner payload

Banners ship as a top-level, pre-sorted array on the collection controller’s data:
Each banner carries the rule_id of the merchandising rule that produced it. data._meta.appliedRules (a flat array of rule ID strings) still lists every applied rule. Four parts of the banner payload matter for rendering:
  • Per-device media and layout. Pick web_* on desktop and mobile_* on mobile. They’re independent. A hero on web can be an inline tile on mobile.
  • Mode. overtake replaces the product card at position. inject inserts the banner and shifts trailing products by one cell.
  • Link. On the built-in link type, only inject banners can carry a top-level link. Wrap them in an anchor. Render overtake banners as plain media. Custom banner types put their click-through URL inside data instead.
  • Type and data. type is the banner type slug: 'link' for the built-in type, or the slug of one of your custom types. data carries the values of that type’s field schema. Switch renderers on type and read fields out of data.

Step 1: Subscribe to the collection controller

sdk.collection() returns a reactive controller. Subscribe once and read data.products and data.banners from the same payload.
The SDK dedupes identical requests and caches responses in memory for the life of the page. It re-runs the subscriber whenever you call get() with new filter or pagination args.

Step 2: Pick the layout for the current device

A banner only has a usable layout when both the media slot and the layout for the active device are present. Skip banners that don’t have both.
Listen for mql.addEventListener('change', ...) and re-render when the breakpoint flips. Banners can have completely different layouts per device. A 2×2 web tile may be a hero on mobile.

Step 3: Place banners around the products

Hero banners render across the full grid width above the products. Ignore their inline width and height cell spans; your theme controls the hero’s height and image cropping. Inline banners slot into the product grid by position (row-major, 0-indexed). overtake replaces the product at that index; inject splices the banner in and shifts trailing products. The response is already sorted by sort_index, so no extra sort is needed.
Use grid-column: span <width>; grid-row: span <height> (or the --span-x / --span-y properties) on banner cells so 2×2 tiles actually cover four grid cells.

Re-running on filter or pagination changes

Reuse the same collection controller and subscriber, and call get() with new args. Layers recomputes the banner payload on every request, so banners are always in sync with the current filter set.

Custom banner types

The rendering steps on this page work unchanged for the built-in Link type. If you’ve defined custom banner types, each banner also carries a type slug and a data object with the type’s saved field values. Switch renderers on type and read fields out of data. The envelope (media, layout, mode) is the same across all types.
Reference fields (product_reference, collection_reference, variant_reference) arrive pre-resolved to { id, handle }. Unpublished references are dropped from the payload, so you don’t need to guard against dead handles. Retyped or removed fields on the banner type are dropped too. Treat any field on data as optional and fall back cleanly.

Fallback rules

Per Banner Injection:
  • A banner without both the device’s media slot and layout is treated as disabled. layoutForDevice returns null and you skip it.
  • A banner without web_media or mobile_media filled is never enabled by Layers, so it won’t appear in the payload in the first place.
  • Banners with mode: 'overtake' ignore any link value. Only inject banners are clickable.

What the app embed handles for you

The Layers theme app extension does not render banners. Your theme owns that. The embed does handle:
  • Authenticating the browse request (token, cart context, identity).
  • Sending the storefront pixel events that track shopper engagement on the underlying grid.
The storefront pixel also tracks banner_impression and banner_click events automatically for any rendered element whose data-banner-id matches a banner id from the Layers response, as in the examples above. Keep that attribute on your banner markup so these events fire.

Why the SDK over Fetch

Troubleshooting

data.banners is empty. No merchandising rule matched the request, or every matching rule had no enabled banners. Confirm at least one active rule on this collection has banners attached and both media slots filled. Banners only appear on first load, not after filtering. Your subscriber probably re-renders products but not banners. Re-read data.banners from the same payload on every state update. They ship together. Inline banner is at the wrong cell. position is row-major and 0-indexed. Verify the device’s *_layout.position matches the cell index you expect after flattening the grid to a single array. 2×2 inline tile renders as a 1×1. Your CSS isn’t honoring width/height. Apply grid-column: span <width>; grid-row: span <height> on banner cells.

See also