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

> Render Layers recommendation blocks in a Shopify theme as a Liquid section with a block picker setting, cart context, product deduplication, and pagination.

## Overview

A recommendation block in a Shopify theme is a Liquid section with a single setting: which Layers block to render. The Layers theme app extension does the rest — fetching products, tracking cart context, deduplicating across blocks on the same page, and submitting feedback when a shopper clicks a result.

You only need to:

1. Read block metaobjects in Liquid to power the section setting (so merchandisers pick a block from a dropdown, not by typing an ID).
2. Render a placeholder element with the selected block's ULID handle.
3. Call the [Blocks API](/api-reference/blocks) to fill it with products.

Use this version when you're scripting against the storefront API directly with `fetch`. For the SDK version, see [Rendering blocks with the SDK](/developers/rendering-blocks-with-the-sdk).

## Prerequisites

* The Layers Shopify app installed. The app embed handles cart context, dedup, and click attribution automatically — your section only needs to fetch products.
* A storefront access token from `shop.metafields.layers.embed_settings`. See [Liquid integration](/developers/liquid-integration).

```liquid theme={null}
{% comment %} theme.liquid layout {% endcomment %}
{% assign embed_settings = shop.metafields.layers.embed_settings.value %}
<script>
  window.layersConfig = {
    storefrontApiToken: '{{ embed_settings.storefrontApiToken }}',
    apiBase: 'https://app.uselayers.com/storefront/v1',
  }
</script>
```

## The section

Create `sections/layers-recommendations.liquid`. The schema's only setting is a `select` whose options are generated at render time from the configured block metaobjects.

```liquid theme={null}
{% assign layers_blocks = shop.metaobjects['app--278936322049--block'] %}
{% 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 %}
```

<Note>
  Shopify schema is static JSON, so the block list can't be generated inline. Use the snippet below to populate the dropdown at render time.
</Note>

### Generating block options in the theme editor

Render a small script in the theme that mirrors block metaobjects into the section's setting list. Most themes do this with a `theme.liquid` snippet that updates the section schema via the Shopify Admin API on app install — but for storefronts that want a Liquid-only path, populate a `<datalist>` and switch the schema setting from `select` to `text`:

```liquid theme={null}
{% comment %} snippets/layers-blocks-datalist.liquid {% endcomment %}
{% assign layers_blocks = shop.metaobjects['app--278936322049--block'] %}
<datalist id="layers-block-ids">
  {% for block in layers_blocks %}
    <option value="{{ block.handle }}">{{ block.title.value }} — {{ block.anchor_type.value }}</option>
  {% endfor %}
</datalist>
```

Either way, the merchandiser ends up picking from the live list of Layers blocks instead of pasting a ULID.

## Fetching products

The section renders an empty placeholder with the block handle and the current page's anchor. A small script fetches and hydrates each one.

```js theme={null}
async function hydrate(section) {
  const blockId = section.dataset.blockId
  const anchorId = section.dataset.anchorId

  const response = await fetch(
    `${window.layersConfig.apiBase}/blocks/${blockId}/products`,
    {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Storefront-Access-Token': window.layersConfig.storefrontApiToken,
      },
      body: JSON.stringify({
        ...(anchorId ? { anchor_id: anchorId } : {}),
        pagination: { page: 1, limit: 12 },
      }),
    },
  )

  if (!response.ok) {
    section.hidden = true
    return
  }

  const { results } = await response.json()
  if (!results.length) {
    section.hidden = true
    return
  }

  renderProducts(section.querySelector('.layers-recommendations__products'), results)
}

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.price_range?.min_variant_price ?? ''}</span>
          </a>
        </li>
      `,
    )
    .join('')
}
```

## Anchors

The section reads the right anchor automatically based on `template`:

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

The Layers theme app extension supplies cart context and shopper identity to **every** Blocks request on the storefront, so cart-anchored blocks work without your section reading `/cart.js` or wiring up `productsInCart`.

## What the app embed handles for you

Once the Layers theme app extension is installed and enabled, you do **not** need to:

* Send `context.productsInCart` for cart-anchored blocks — the embed injects it.
* Pass `excludeProductIds` between blocks — the embed dedupes by tracking product IDs rendered earlier on the page.
* Wire up click attribution or feedback — the embed listens for clicks on rendered cards and submits feedback automatically.
* Provide identity (`deviceId`, `sessionId`) — the storefront pixel manages it.

Your section only renders the placeholder and issues the products request. Everything else is managed for you.

## Empty blocks and fallbacks

The Blocks API runs the block's [fallback chain](/platform/blocks/fallback-chains) server-side. If the chain doesn't produce results, the response is `{ results: [] }` — hide the section (as in the snippet above) rather than rendering an empty heading.

## Troubleshooting

**The section is empty in the theme editor.** Make sure the merchandiser has selected a block in the section's settings. The section hides itself when `block_id` is blank.

**The block fetches but renders nothing.** The block fell through its fallback chain — review configuration in the [dashboard](/platform/blocks/fallback-chains).

**Cart recommendations look stale after `/cart` updates.** The app embed re-runs cart-anchored blocks when the Shopify cart changes. If you're not seeing updates, confirm the Layers theme app extension is enabled in **Online Store → Themes → Customize → App embeds**.

## See also

* [Rendering blocks with the SDK](/developers/rendering-blocks-with-the-sdk) — SDK equivalent
* [Blocks API](/api-reference/blocks) — request and response reference
* [Blocks](/platform/blocks) and [Fallback chains](/platform/blocks/fallback-chains) — dashboard configuration
* [Liquid integration](/developers/liquid-integration)
