Overview
This guide is the SDK equivalent of Rendering blocks in Liquid. The shape of the section is the same: one Liquid section, one merchandiser-facing block-picker setting. The SDK’sblocks() controller does the HTTP call, deduplicates in-flight requests, caches responses in memory for the life of the page, and exposes the products on reactive state.
The SDK itself sends only the values your code supplies and returns results with an attributionToken for tracking. With the app embed enabled, the storefront pixel adds identity, cart context, and cross-block deduplication to those requests. See Identity, context, and attribution.
Use this version when you have already installed @commerce-blocks/sdk in your theme. For a no-dependencies version, see the Liquid + Fetch guide.
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.
The section
sections/layers-recommendations.liquid is the same Liquid scaffold as the Fetch version: a section with a native metaobject block-picker setting and a single placeholder element carrying the selected block ID.
metaobject_type is the Layers block definition type, app--278936322049--block. The theme editor lists Layers blocks by name. If your theme can’t use a metaobject setting, see the select and datalist alternatives.
Hydrating the section
For each rendered section, create ansdk.blocks() controller with the selected block ID and the page anchor. Subscribe to the state and render on data changes.
Rendering cards with your theme’s own Liquid snippets
A client-side card renderer drifts out of sync with the theme’s product card (badges, ratings, swatches, variant logic). The more durable pattern is Shopify’s Section Rendering API: create one thin section per surface that renders your existing card snippet for a single product, then fetch it once per result handle.{ "<sectionId>": "<rendered HTML>" }, a map of section ID to its HTML fragment. A missing product or a section that renders nothing produces an empty string or a missing key, which is why the helper filters blank cards.
- One request per result, fired in parallel. A failed fetch drops that card instead of breaking the rail.
- Pass the product’s
selectedVariant.numericIdas the product-pagevariantparameter so the section renders the variant Layers selected (filters, ranking, variant breakouts). Without it, Shopify rendersfirst_available_variantand two breakout tiles of one product collapse into identical cards. The SDK always requests the matched variant and exposes it asselectedVariant; usenumericId, becauseidis thelayers--prefixed SDK ID. If you call the API with Fetch instead, the raw result carries it asfirst_or_matched_variant.id. ?sections=accepts up to 5 comma-separated section IDs per request, so distinct surfaces can share a round trip.- Keep the section’s card arguments in sync with the native grid (rating settings,
hide-tag skips, favorite button). The section is the single place that mapping lives. - Grid markup still comes from the Liquid section wrapping the rail; only the tile HTML is fetched.
Identity, context, and attribution
The app embed loads the storefront pixel, which watches the page’s requests to the Layers API, including the ones the SDK sends, and fills in what your block requests need:- Identity: If a request has no
identity, the pixel adds the shopper’s device ID, session ID, and customer ID. If you passidentitytocreateSDK()(as a static object or a resolver that runs on every request), your values are used instead and the pixel doesn’t add any of its own. - Cart context: The pixel adds
productsInCartand the rest of the shopper context to every request, so cart-anchored blocks work without reading/cart.js. Anycontextfields you pass yourself take priority over the pixel’s. - Cross-block deduplication: On every Blocks request, the pixel adds its IDs to any
excludeProductIdsyou pass, keeping your IDs and including products already on the page: results from other blocks, items in the cart, the product being viewed, and products from the search or collection grid. To turn this off, check Disable block deduplication in the Layers app embed settings in the theme editor. - Click attribution: Results carry an
attributionToken. The storefront pixel covers standard themes. For custom-rendered cards, forwardattributionTokenon your click and add-to-cart tracking.
<head>, such as a <script type="module"> in theme.liquid. On a custom storefront without the app embed, pass identity, context, and excludeProductIds yourself.
Anchors
sdk.blocks({ blockId, anchorId }) resolves the right value based on template:
Both numeric IDs and Shopify GIDs are accepted on product anchors; both IDs and handles are accepted on collection anchors.
Search-anchored blocks
Blocks on thesearch anchor running searched_then_viewed or searched_then_purchased strategies do not read the query from anchor_id. They resolve the shopper’s query from context.priorSearches (specifically the last searchQuery), so the caller must record submitted search queries and pass them on the request:
context.priorSearches, a search-anchored block has no query to work from and falls through to its fallback chain. On a first visit before any search has been submitted there is nothing to anchor to. Give the block a fallback (trending, manual, or bestsellers) that still produces a rail on cold opens.
Why the SDK over Fetch
With the app embed enabled, both paths get the same identity, cart context, and exclusions from the storefront pixel.
Troubleshooting
The section renders but the grid stays empty. The block fell through its fallback chain. Review fallback configuration in the dashboard. No Layers requests fire at all. Work this checklist in order: (1)<script id="layers-config"> is present in the page <head> with a non-empty storefrontApiToken. If the tag is missing or blank, enable the app embed (see SDK installation). (2) The block picker in the section settings is assigned. An unpicked setting silently no-ops. (3) The ID reaching blockId is the raw ULID, not the layers--prefixed metaobject handle. See the handle transform in The section.
Block picker is empty. Confirm block metaobjects are visible under Content → Metaobjects in Shopify admin, and that the setting’s metaobject_type is exactly app--278936322049--block. If you use a select or datalist picker instead, confirm you populated it from those metaobjects (see Generating block options in the theme editor).
Cart-anchored blocks do not refresh on cart changes. The app embed keeps its cart context up to date, but it doesn’t re-request blocks that are already on the page. Call blocks.get() again after a cart change (for example, when your cart drawer updates). If the new request still uses old cart contents, confirm the app embed is enabled under Online Store → Themes → Customize → App embeds.