Liquid reference

Everything the app writes is a normal metafield. This page collects the Liquid you need and the platform limits that apply.

Paths

ContextPath
Store-wideshop.metafields.lists.<handle>.value
Per productproduct.metafields.lists.<handle>.value
Per collectioncollection.metafields.lists.<handle>.value
Per entryshop.metafields.lists.<handle>.value[metaobject.system.handle]

The namespace is always lists and the key is the list's handle, which the editor derives from the name with dashes, so featured-books. Dot notation works with dashed keys in Shopify's Liquid; bracket notation, shop.metafields.lists['featured-books'], is equivalent if your editor prefers it. The handle is shown in the editor and on the home page.

Store-wide, per-product and per-collection lists

These are list.metaobject_reference metafields. Add .value to get an array of metaobjects, then loop:

{%- assign entries = shop.metafields.lists.featured-books.value -%}
{%- if entries.size > 0 -%}
  <ul>
    {%- for entry in entries -%}
      <li>
        {%- if entry.system.url != blank -%}
          <a href="{{ entry.system.url }}">{{ entry.title }}</a>
        {%- else -%}
          {{ entry.title }}
        {%- endif -%}
      </li>
    {%- endfor -%}
  </ul>
{%- endif -%}

Each entry is a full metaobject. Field access is by key (entry.pages), references resolve through .value (entry.author.value.name), rich text renders with metafield_tag, and images work with image_url. The editor's Fields to render panel writes these expressions for you, and the comment at the top of every snippet lists the expression for each field on the type.

The 50-entry rule

Liquid exposes only the first 50 entries of a list metafield. entries.size reports the true count, up to 250, but a for loop stops after 50, offset past 50 returns nothing, index access such as entries[60] is empty, and reversed is ignored. We measured this on Horizon with a 250-entry list; it is a platform limit, not a theme one.

So plan for 50 per list on the storefront. For a longer page, make further lists in the app with an offset: the first list with a limit of 50, the second with an offset of 50 and a limit of 50, and so on. Each is its own metafield and its own loop:

{%- for entry in shop.metafields.lists.all-books-1.value -%} … {%- endfor -%}
{%- for entry in shop.metafields.lists.all-books-2.value -%} … {%- endfor -%}

The metafield still holds up to 250 entries, which matters if you read it through the Storefront API rather than Liquid.

Shopify also resolves at most 20 metaobjects by handle per page render. That limit applies to metaobjects.type[handle] lookups, not to reference metafields, and it is why per-entry lists are capped at 20.

Per-entry lists

A per-entry list, and a per-referenced-entry list such as books by author, is a json metafield: a map from a handle to an array of entry handles. Look the current entry up by its handle, then load each listed entry:

{%- assign related = shop.metafields.lists.books-by-author.value[metaobject.system.handle] -%}
{%- if related.size > 0 -%}
  <ul>
    {%- for h in related -%}
      {%- assign entry = metaobjects.book[h] -%}
      <li><a href="{{ entry.system.url }}">{{ entry.title }}</a></li>
    {%- endfor -%}
  </ul>
{%- endif -%}

metaobject is only set on a metaobject's own page, so this snippet belongs in the metaobject template, here the author's. Replace book with the type's handle. On a product page, get the entry from a product metafield instead: Four Mile Books uses product.metafields.custom.book.value.system.handle as the key.

Dynamic sources in the theme editor

Per-product and per-collection lists appear in the theme editor's dynamic source picker on the matching template, because the app creates a metafield definition for them. Any block or section with a metaobject_list setting can be connected to the list without editing code.

Two limits to know:

  • Shop-level metafields cannot be dynamic sources, so store-wide lists are read in Liquid or from the section's own settings.
  • A metaobject_list setting shows at most 50 entries. For more, read the metafield in Liquid.

If you write a section that reads the setting yourself, the value is already an array of metaobjects:

{%- for entry in section.settings.books -%}
  {{ entry.title }}
{%- endfor -%}

The .value rule

In Liquid, shop.metafields.lists.featured-books is the metafield object and .value is its content. In a template's JSON, a dynamic source must include .value too:

"reading_lists": "{{ product.metafields.lists.reading-lists-for-book.value }}"

Leaving .value off is the most common reason a bound block renders nothing.

Empty lists

A list with no results has no metafield. entries.size is 0 and entries is blank, so a guard of if entries.size > 0 handles both cases. A product that no entry references has no per-product metafield at all.

Caching

Shopify caches storefront pages. After a list changes, allow a minute for the page to update, and longer if a CDN is in front of the store.