Per-product content without the tag-if spaghetti: 3 levels (metafields and metaobjects)

A question that comes up constantly, in a dozen shapes: “I sell products in series, motifs or collections (unicorn, beetle, a band, a material) and I want a section on the product page that shows the right text and image for that product, automatically.” The first answer everyone reaches for (I did too) is a tag-if chain. It works for two motifs. Then it quietly becomes a maintenance trap. Here is the same goal built three ways, from quick-and-fragile to the one that scales to hundreds of products without touching code again.

LEVEL 1. Tag-if chain (fast, but it explodes)
You tag products and branch on the tag in the template:

{% if product.tags contains 'unicorn' %}
  <h2>The Unicorn series</h2>
  <p>Hand-drawn, small-batch, a bit of sparkle.</p>
{% elsif product.tags contains 'beetle' %}
  <h2>The Beetle series</h2>
  <p>Matte finish, built to last.</p>
{% endif %}

Fine for two or three fixed cases that never change. The problem: every new motif means another code edit, the content lives in code (your merchant can’t touch it), and if you also branch on product type or colour the combinations multiply until nobody can read it. If you catch yourself copy-pasting a fourth elsif, stop. You want one of the next two.

LEVEL 2. Product metafields (content out of code, editable per product)
Define the fields once under Settings > Custom data > Products, for example a single-line text “Motif heading”, a rich-text “Motif text”, an image “Motif image”. Now every product has those fields on its admin page, and the template just prints whatever is there:

{% assign heading = product.metafields.custom.motif_heading %}
{% if heading != blank %}
  <section class="motif">
    <h2>{{ heading }}</h2>
    {{ product.metafields.custom.motif_text | metafield_tag }}
    {% if product.metafields.custom.motif_image != blank %}
      {{ product.metafields.custom.motif_image.value | image_url: width: 1200
         | image_tag: loading: 'lazy', class: 'motif__image' }}
    {% endif %}
  </section>
{% endif %}

No more code edits when you add a motif, and the merchant edits the words themselves. The catch: the content is per product. If fifty products share the same “Unicorn series” blurb, you’re pasting that blurb fifty times, and re-editing fifty products when it changes. Perfect for genuinely unique per-product content, wasteful for shared content.

LEVEL 3. A metaobject (define the shared content ONCE, reference it everywhere)
This is the piece most people don’t know exists, and it’s exactly what “same content across many products” is built for. Under Settings > Custom data > Metaobjects, create a definition called “Motif” with fields: heading (text), description (rich text), image (file). Add one entry per motif (Unicorn, Beetle, and so on), each filled in a single place. Then add ONE product metafield (Settings > Custom data > Products) of type “Metaobject reference” pointing at Motif, called “motif”. On each product you just pick its motif from a dropdown. The template follows the reference:

{%- assign motif = product.metafields.custom.motif.value -%}
{%- if motif -%}
  <section class="motif">
    {%- if motif.image.value != blank -%}
      {{ motif.image.value | image_url: width: 1200
         | image_tag: loading: 'lazy', class: 'motif__image' }}
    {%- endif -%}
    <h2>{{ motif.heading.value }}</h2>
    {{ motif.description | metafield_tag }}
  </section>
{%- endif -%}

Now change the Unicorn description once, and every unicorn product updates. Add a new motif by adding one metaobject entry and picking it on the products, still zero code. A few things worth knowing:

  • .value on the metaobject-reference metafield gives you the metaobject; from there you read each field by its key (a plain text field as motif.heading.value, an image as motif.image.value).
  • Rich text fields render as HTML through the metafield_tag filter (motif.description | metafield_tag). Printing .value on a rich text field instead gives you raw JSON, which is the classic “why is my metafield showing code” surprise.
  • Make the product “motif” metafield a LIST of metaobject references if a product can belong to several motifs, then loop: {% for m in product.metafields.custom.motifs.value %}.
  • Where to put the snippet: paste it into a Custom Liquid block on the product template (no theme-file edits), or wrap it in its own section so you can position it in the editor.

Which level to pick, honestly:

  • Two or three cases that never change: the tag-if is fine, don’t over-engineer it.
  • Content unique to each product: Level 2 (product metafields).
  • The same content shared across many products (series, motif, brand, material, care instructions): Level 3 (metaobject). This is the one that saves you months later.

The general rule: content belongs in data (metafields and metaobjects), not in {% if %} branches. The moment “which content” is a property of the product rather than a rule in your template, you stop editing code and start just filling in fields.

Happy to help map this to your specific case. Tell me what varies (motif? series? material?) and whether it’s unique per product or shared, and I’ll point you at the exact field setup.

1 Like