> ## Documentation Index
> Fetch the complete documentation index at: https://docs.uselayers.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Rendering blocks with the SDK

> Render Layers recommendation blocks in a Shopify theme as a Liquid section, hydrated by the @commerce-blocks/sdk blocks controller with cart context.

## Overview

This is the SDK equivalent of [Rendering blocks in Liquid](/developers/rendering-blocks-in-liquid). The shape of the section is the same — one Liquid section, one merchandiser-facing block-picker setting — but `client.blocks()` does the HTTP call, dedupes against in-flight requests, caches results in `localStorage`, and exposes the products on a reactive signal.

You do **not** need to handle cart context, click feedback, or cross-block product deduplication. The Layers theme app extension handles those for every Blocks request on the storefront.

Use this version when you've already installed [`@commerce-blocks/sdk`](/sdk/installation) in your theme. For a no-dependencies version, see the [Liquid + Fetch guide](/developers/rendering-blocks-in-liquid).

## Prerequisites

* The Layers Shopify app installed and the theme app extension enabled in **Online Store → Themes → Customize → App embeds**.
* The SDK installed and a client created in your theme — see [SDK installation](/sdk/installation) and the [setup snippet](/developers/rendering-facets-with-the-sdk#prerequisites) for generating `sorts`/`facets` from metaobjects.

## The section

`sections/layers-recommendations.liquid` is the same Liquid scaffold as the Fetch version — a section with a block-picker setting and a single placeholder element carrying the selected block handle.

```liquid theme={null}
{% assign selected_block_id = section.settings.block_id %}

{% if selected_block_id != blank %}
  <section
    class="layers-recommendations"
    data-block-id="{{ selected_block_id }}"
    data-anchor-id="{% if template == 'product' %}{{ product.id }}{% elsif template == 'collection' %}{{ collection.handle }}{% endif %}"
  >
    {% if section.settings.heading != blank %}
      <h2>{{ section.settings.heading }}</h2>
    {% endif %}
    <ol class="layers-recommendations__products" data-loading="true"></ol>
  </section>
{% endif %}

{% schema %}
{
  "name": "Layers Recommendations",
  "settings": [
    {
      "type": "text",
      "id": "heading",
      "label": "Heading",
      "default": "You may also like"
    },
    {
      "type": "select",
      "id": "block_id",
      "label": "Block",
      "info": "Choose a Layers recommendation block.",
      "options": [
        { "value": "", "label": "Select a block" }
      ]
    }
  ],
  "presets": [{ "name": "Layers Recommendations" }]
}
{% endschema %}
```

The `select` options are populated from block metaobjects — see the [Fetch guide](/developers/rendering-blocks-in-liquid#generating-block-options-in-the-theme-editor) for the pattern that mirrors metaobjects into the dropdown so merchandisers pick by name instead of pasting a ULID.

## Hydrating the section

For each rendered section, create a `client.blocks()` controller with the selected handle and the page's anchor. Subscribe to the state signal and render on `data` changes.

```js theme={null}
function hydrate(section) {
  const blockId = section.dataset.blockId
  const anchorId = section.dataset.anchorId || undefined
  const list = section.querySelector('.layers-recommendations__products')

  const blocks = window.layers.blocks({ blockId, anchor: anchorId })

  blocks.subscribe(({ data, error }) => {
    if (error || (data && !data.products.length)) {
      section.hidden = true
      return
    }
    if (data) renderProducts(list, data.products)
  })

  blocks.execute({ limit: 12 })
}

document.querySelectorAll('.layers-recommendations').forEach(hydrate)
```

```js theme={null}
function renderProducts(list, products) {
  list.removeAttribute('data-loading')
  list.innerHTML = products
    .map(
      (p) => `
        <li>
          <a href="/products/${p.handle}">
            <img src="${p.featured_media?.preview_image?.src ?? ''}" alt="" />
            <h3>${p.title}</h3>
            <span>${p.priceRange?.formatted ?? ''}</span>
          </a>
        </li>
      `,
    )
    .join('')
}
```

That's the whole integration. Multiple sections on the same page each get their own controller, run independently, and reuse the SDK's cache and dedup machinery.

## What the app embed handles for you

When the Layers theme app extension is enabled, the storefront-side embed augments every Blocks request — including the SDK's — so you don't have to:

* **Cart context** — current cart line items are sent automatically; cart-anchored blocks work without you reading `/cart.js`.
* **Product deduplication** — products rendered earlier on the page are excluded from later blocks automatically.
* **Click attribution and feedback** — the embed listens for clicks on rendered cards and submits feedback to keep ranking models fresh.
* **Identity** — the storefront pixel manages `deviceId` and `sessionId`.

Your section only renders the placeholder and creates a controller. Everything else is managed for you.

## Anchors

`client.blocks({ blockId, anchor })` resolves the right value based on `template`:

| Template      | `anchor`                                                      |
| :------------ | :------------------------------------------------------------ |
| `product`     | `{{ product.id }}`                                            |
| `collection`  | `{{ collection.handle }}`                                     |
| Anything else | omitted (cart-anchored and global blocks resolve without one) |

Both numeric IDs and Shopify GIDs are accepted on product anchors; both IDs and handles are accepted on collection anchors.

## Why the SDK over Fetch

| Concern                  | Fetch version                      | SDK version                                             |
| :----------------------- | :--------------------------------- | :------------------------------------------------------ |
| Driving the request      | `fetch` to `/blocks/{id}/products` | `blocks.execute()`                                      |
| Re-renders               | DIY DOM updates                    | Signal subscriber                                       |
| Caching across nav       | DIY                                | Restored from `localStorage`                            |
| Multiple blocks per page | Manual orchestration               | Independent controllers, SDK dedupes identical requests |

Cart context, dedup, and attribution behavior is identical between the two — the theme app extension does that work either way.

## Troubleshooting

**The section renders but the grid stays empty.** The block fell through its fallback chain. Review [fallback configuration](/platform/blocks/fallback-chains) in the dashboard.

**Block-picker dropdown is empty.** Confirm block metaobjects are visible under **Content → Metaobjects** in Shopify admin, and that you've populated the section setting from those metaobjects (see the Fetch guide for the pattern).

**Cart-anchored blocks don't refresh on cart changes.** The theme app extension re-runs cart-anchored blocks. Confirm it's enabled under **Online Store → Themes → Customize → App embeds**.

## See also

* [Rendering blocks in Liquid (Fetch)](/developers/rendering-blocks-in-liquid) — the no-dependency equivalent
* [Client methods → `client.blocks()`](/sdk/api-reference/client-methods)
* [Blocks API](/api-reference/blocks)
* [Blocks](/platform/blocks) and [Fallback chains](/platform/blocks/fallback-chains)
