Skip to main content

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.

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

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.
{% 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.
{% 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 %}
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.

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:
{% 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.
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)
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:
Templateanchor_id sent
product{{ product.id }}
collection{{ collection.handle }}
Anything elseomitted (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 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. 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