Snippets / Shopify

Shopify Product Metafield — Clean Liquid Render Pattern

A reusable Liquid snippet that safely renders Shopify product metafields with type detection, fallback handling, and null safety — one include, every metafield on the site.

Liquid utility Production-tested ~4 min setup Theme helper Intermediate Shopify 2.0+ Updated May 15, 2026
TL;DR

This Shopify product metafield Liquid snippet safely renders Shopify product metafields with type detection, fallback handling, and null safety.

Use this when your theme renders product metafields — size charts, care instructions, ingredient lists, technical specs — and you want consistent HTML output with automatic type detection and null-safe fallbacks without repeating the same Liquid guards in every template.

01 — Building it step by step

Shopify product metafield — The Code

This Shopify product metafield snippet gives you a clean render pattern for any metafield type with a safe fallback.

We’ll build this in 5 small steps. Each step adds exactly one new concept — raw output, null safety, type branches, extract-to-snippet, and the full production renderer with five metafield types. Every step is a complete, working checkpoint you can stop at and test. No prior Shopify metafield experience assumed beyond knowing they exist.

Step 1: The raw, unsafe metafield output. You reference a product metafield exactly the same way you reference any other property in Liquid — product.metafields.namespace.key where namespace is the group name (e.g., custom) and key is the metafield identifier (e.g., care_instructions). Here’s the simplest possible render — one line, no checks, no wrapping HTML. If the metafield is empty, this outputs a blank space. If the metafield doesn’t exist, the page throws a Liquid error and the entire product template breaks. This is the pain point: raw metafield access is fragile.

Step 1 — Raw unsafe output.liquid
liquid
{%- comment -%}Shopify product metafield - Liquid snippet by Mosharaf Hossain{%- endcomment -%}
{{ product.metafields.custom.care_instructions }}

Step 2: Wrap it in a null guard. The {% if metafield != blank %} check catches two failure modes at once — a completely nonexistent metafield (namespace/key combo was never set in Shopify admin) and a metafield that exists but has no value assigned. Both evaluate to blank in Liquid’s truthiness system, so one guard covers both. Without this, an empty metafield renders empty <div> tags with invisible content — HTML pollution that bloats the DOM and breaks layout assumptions. This is the minimum safe wrapper for every metafield you render.

Step 2 — Add null guard.liquid
liquid
{%- comment -%}Shopify product metafield{%- endcomment -%}
{% if product.metafields.custom.care_instructions != blank %}
  <div class="metafield-care">
    {{ product.metafields.custom.care_instructions }}
  </div>
{% endif %}

Step 3: Split rendering by metafield type. A text metafield (like “care instructions”) renders fine as plain text — but an image metafield (like a size chart) needs an <img> tag with image_url filter, a measurement metafield needs .value and .unit properties, and a URL metafield needs an <a> tag. Raw output treats them all the same — it dumps whatever Liquid’s implicit string conversion produces, which for file references is a path string like files/size-chart.png rather than a rendered image. Branching by type means each metafield renders the correct HTML for its data shape.

Step 3 — Type-aware rendering.liquid
liquid
{%- comment -%}Shopify product metafield{%- endcomment -%}
{% assign mf = product.metafields.custom %}

{% if mf.care_instructions != blank %}
  <div class="metafield-care">
    <h3>Care Instructions</h3>
    {{ mf.care_instructions }}
  </div>
{% endif %}

{% if mf.size_chart_image != blank %}
  <div class="metafield-size-chart">
    <h3>Size Chart</h3>
    <img
      src="{{ mf.size_chart_image | image_url }}"
      alt="Size chart"
      loading="lazy"
    >
  </div>
{% endif %}

Step 4: Extract into a reusable snippet. Copy-pasting the same {% if %} guard and type branches into every template where you render a metafield is the long road to an unmaintainable theme. Shopify’s {% render %} tag includes a separate Liquid file (stored in the snippets/ directory of your theme) and accepts parameters via the with keyword. This step creates snippets/metafield.liquid that accepts three parameters — metafield (the metafield object), type (what kind of HTML to output), and label (a heading above the value). Now every metafield in the theme calls the same one file.

Step 4 — Extracted snippet.liquid
liquid
{%- comment -%}Shopify product metafield{%- endcomment -%}
{# snippets/metafield.liquid #}
{% assign mf = metafield %}

{% if mf != blank %}
  {% if type == 'text' %}
    <div class="metafield metafield--text">
      <span class="metafield__label">{{ label }}</span>
      <p class="metafield__value">{{ mf }}</p>
    </div>

  {% elsif type == 'image' %}
    <div class="metafield metafield--image">
      <span class="metafield__label">{{ label }}</span>
      <img
        src="{{ mf | image_url }}"
        alt="{{ label | escape }}"
        loading="lazy"
      >
    </div>
  {% endif %}
{% endif %}

Step 5: Full production snippet. All concepts combined with production polish: 1) Five metafield type handlers — text uses newline_to_br for multiline content, image wraps in a responsive <img>, measurement splits .value and .unit into separate elements, url renders a styled link, and rich_text uses Shopify’s metafield_tag filter for formatted HTML output. 2) A fallback parameter accepts a default string shown when the metafield is blank — “No specs available” instead of empty whitespace. 3) All user-facing strings pass through escape to prevent injection. 4) A {% case %} block replaces the {% if %}/{% elsif %} chain for cleaner type dispatch. 5) An {% else %} fallback clause catches unrecognized types gracefully instead of rendering nothing.

snippets/metafield.liquid
liquid
{%- comment -%}Shopify product metafield{%- endcomment -%}
{# snippets/metafield.liquid — production version #}
{#
  Usage:
    {% render 'metafield',
       metafield: product.metafields.custom.specs,
       type: 'text',
       label: 'Technical Specs',
       fallback: 'No specs available' %}
#}
{% liquid
  assign mf = metafield
  if mf == blank and fallback != blank
    assign mf = fallback
  endif
%}

{% if mf != blank %}
  {% case type %}

    {% when 'text' %}
      <div class="metafield metafield--text">
        {% if label != blank %}
          <h4 class="metafield__label">{{ label | escape }}</h4>
        {% endif %}
        <p class="metafield__value">{{ mf | newline_to_br | strip }}</p>
      </div>

    {% when 'image' %}
      <div class="metafield metafield--image">
        <img
          src="{{ mf | image_url }}"
          alt="{{ label | escape }}"
          loading="lazy"
        >
      </div>

    {% when 'measurement' %}
      <div class="metafield metafield--measurement">
        {% if label != blank %}
          <span class="metafield__label">{{ label | escape }}</span>
        {% endif %}
        <span class="metafield__value">{{ mf.value }}</span>
        <span class="metafield__unit">{{ mf.unit }}</span>
      </div>

    {% when 'url' %}
      <a
        href="{{ mf | escape }}"
        class="metafield metafield--link"
        rel="noopener"
        target="_blank"
      >
        {{ label | default: mf | escape }}
      </a>

    {% when 'rich_text' %}
      <div class="metafield metafield--rich-text">
        {% if label != blank %}
          <h4 class="metafield__label">{{ label | escape }}</h4>
        {% endif %}
        <div class="metafield__richtext">
          {{ mf | metafield_tag }}
        </div>
      </div>

    {% else %}
      <p class="metafield metafield--fallback">
        {{ mf }}
      </p>

  {% endcase %}
{% endif %}

Where to put the code

Save the final version as snippets/metafield.liquid in your Shopify theme. The snippet is self-contained — no global JavaScript, no theme settings changes, no app dependencies. Use it from any template with a single line:

{% render 'metafield', metafield: product.metafields.custom.specs, type: 'text', label: 'Tech Specs' %}

The snippets/ directory is Shopify’s convention for reusable Liquid partials. Any {% render %} tag looks for the file there automatically — you never hardcode a path.

02 — How it works

How Shopify product metafield Works — Why Each Layer Exists

Here is how this Shopify product metafield works step by step: Every line in this snippet serves a purpose. Here's why each safeguard exists, how Shopify stores metafield data, and how the type system determines what HTML to render.

Every layer in this snippet solves a real failure mode. Here’s why each safeguard exists, how Shopify stores metafield data, and how the type system determines what HTML to render.

1

Metafield types, namespace/key structure, and the Metafield object

Metafields are key-value data attached to Shopify resources — products, variants, collections, customers, orders. Every metafield lives in a namespace (a group identifier) and has a key (the field name within that group). The custom namespace is reserved for metafields you create in Shopify admin — it’s the most common namespace. Third-party apps create their own namespaces (e.g., seo.hidden for SEO data). Access syntax is resource.metafields.namespace.key. Inside the Liquid context, a metafield is a Metafield object — not a string, not a number — with properties like .value, .type, and .unit (for measurements). Understanding this object shape is the key difference between “just output the value” and “render the right HTML for each type.”

2

Liquid null safety — blank, nil, empty, and missing metafields

Liquid’s truthiness system treats all of these as blank: nil (metafield doesn’t exist), an empty string "" (metafield exists but has no value), an empty array [], and the boolean false. The {% if metafield != blank %} guard catches all of them in one comparison. This is more robust than checking {% if metafield %} alone, because Liquid’s implicit truthiness can be surprising — a string "false" is truthy, a metafield object wrapping an empty string might evaluate differently depending on the Shopify API version. Explicitly comparing to blank is the idiomatic Shopify pattern. Without this guard, rendering a nonexistent metafield throws a Liquid error: undefined method that breaks the entire page — not just the metafield, the whole template stops rendering at that line.

3

Snippet parameters — render, with, and scoped variables

Snippet parameters pass data from the calling template into the reusable file. The {% render 'metafield', metafield: value, type: 'text' %} syntax creates named variables that exist only inside the rendered snippet — they don’t pollute or collide with the parent template’s variables. This is called snippet scoping. The with keyword (old syntax: {% render 'name' with variable %}) sets a single parameter accessible inside the snippet as the snippet’s basename — e.g., inside snippets/metafield.liquid, with sets {{ metafield }}. The newer named-parameter syntax (metafield: value) is preferred for multiple parameters and clearer intent. Step 4 uses with for simplicity; Step 5 upgrades to named parameters for the production version.

4

Type-aware rendering — text, image, measurement, URL, rich text

Each metafield type produces a different HTML structure because the underlying data has a different shape. Single line text (single_line_text_field): the metafield object stringifies directly to the text value — use {{ mf }} or {{ mf | escape }}. Multi-line text (multi_line_text_field): same stringification, but newlines are lost unless you apply | newline_to_br to convert n into <br> tags. File reference / image (file_reference): the metafield resolves to a Shopify file object — stringifying gives a CDN path, but to actually render an image you need {{ mf | image_url }} which produces a full CDN URL with the correct dimensions. Measurement (weight or dimension): the metafield is an object with .value (the numeric amount) and .unit (the unit string like “kg” or “cm”) — you must access properties separately; direct output produces the internal object representation. Rich text (rich_text_field): stores HTML with inline formatting — use Shopify’s metafield_tag filter which wraps the rich text in a <p> tag and renders valid HTML.

5

Case dispatch, fallback values, and graceful degradation

The {% case type %} block in Step 5 replaces the {% if type == 'text' %} / {% elsif type == 'image' %} chain from Step 4. Case dispatch is cleaner when you have 3+ branches — it communicates “we’re choosing one of N equal options” rather than “we’re testing an ordered sequence of conditions.” The {% else %} fallback at the end is critical: if someone passes type: 'video' (a type you haven’t implemented yet), the snippet renders the value as plain text inside a metafield--fallback wrapper rather than silently producing nothing. This degrades gracefully — the metafield is visible, it’s just not in the ideal HTML format. The fallback parameter (optional string shown when the metafield itself is blank) uses the same Liquid trick as Step 2: {% if mf == blank and fallback != blank %} — both checks in one condition, short-circuit evaluation prevents evaluating fallback if mf already has a value.

03 — Setup & integration

Shopify product metafield — Setup & Integration

To integrate this Shopify product metafield into your project: One snippet file, metafield definitions in the Shopify admin, and you're rendering every metafield on the site through one consistent interface.

One snippet file, metafield definitions in the Shopify admin, and you’re rendering every metafield on the site through one consistent interface. Here’s where everything lives and how to wire it up.

your-shopify-theme/
│   ├── snippets/
│   │   └── metafield.liquid
│   ├── sections/
│   │   └── main-product.liquid
│   └── templates/
        └── product.json
1

Create the metafield definitions in Shopify admin

Create the metafield definitions in Shopify admin. Go to Settings → Custom data → Products and click Add definition. Create one definition for each metafield your snippet will render. The fields you need for this snippet to work across all types:

  • Care instructions — Type: Multi-line text, Namespace/key: custom.care_instructions. This tests the text renderer with newline_to_br.
  • Size chart — Type: File (accept images only), Namespace/key: custom.size_chart. This tests the image renderer with image_url filter.
  • Product weight — Type: Weight, Namespace/key: custom.product_weight. This tests the measurement renderer with .value and .unit.
  • External guide — Type: URL, Namespace/key: custom.external_guide. This tests the url renderer with an <a> tag.
  • Full description — Type: Rich text, Namespace/key: custom.full_description. This tests the rich_text renderer with metafield_tag filter.

All five definitions use the custom namespace — that’s the convention for admin-created metafields and it means your snippet references them as product.metafields.custom.care_instructions.

2

Place the snippet and call it from your product templates

Place the snippet file and call it from your templates. Copy the Step 5 code into a new file snippets/metafield.liquid. In sections/main-product.liquid (or wherever your product template lives), add the render calls below the product description. Each metafield gets its own render tag with the appropriate type:

{% render 'metafield', metafield: product.metafields.custom.care_instructions, type: 'text', label: 'Care Instructions' %}nn{% render 'metafield', metafield: product.metafields.custom.size_chart, type: 'image', label: 'Size Chart' %}nn{% render 'metafield', metafield: product.metafields.custom.product_weight, type: 'measurement', label: 'Weight' %}nn{% render 'metafield', metafield: product.metafields.custom.external_guide, type: 'url', label: 'View Guide' %}nn{% render 'metafield', metafield: product.metafields.custom.full_description, type: 'rich_text', label: 'Full Description' %}

Each {% render %} call is independent — you can add or remove metafields without touching the snippet itself. This is the core design principle: the snippet handles how metafields render; the templates decide which metafields to show.

3

Follow naming conventions for metafield keys

Naming conventions matter. Shopify metafield keys are case-sensitive and must match exactly between admin definitions and Liquid templates. Use snake_case for keys — care_instructions not CareInstructions — because Shopify admin uses snake_case by default when you create definitions. If you rename a metafield key in the admin, every {% render %} call referencing the old key silently goes blank (the null guard catches it, so no page crash — but content disappears). Document your metafield keys in a comment block at the top of metafield.liquid so the next developer (or you, six months from now) can see which keys the snippet expects.

4

Test with real product data in Shopify admin

Test with real product data. Open any product in Shopify admin, scroll to the Metafields section at the bottom, and fill in each metafield with real content. The care_instructions metafield should have multiple lines (to test newline_to_br). The size_chart metafield should have an uploaded image. The product_weight metafield should have a numeric value with a unit. After saving, preview the product on your storefront — each metafield should render in its typed wrapper with the correct HTML structure.

04 — Making it your own

Making This Shopify product metafield Your Own

Customise this Shopify product metafield to match your specific needs: The snippet is a pattern, not a closed box. Add custom type renderers, extend it to other Shopify resources, and control when entire sections render.

1

Add a color metafield renderer with swatch + hex code

Add a color metafield renderer. Shopify’s color metafield type returns a hex value and an optional label. Add this {% when %} branch to the {% case type %} block in metafield.liquid:

{% when 'color' %}n  <div class="metafield metafield--color">n    {% if label != blank %}n      <span class="metafield__label">{{ label | escape }}</span>n    {% endif %}n    <spann      class="metafield__swatch"n      style="background-color: {{ mf }}"n      title="{{ mf | escape }}"n    ></span>n    <code>{{ mf | escape }}</code>n  </div>

This renders the hex value both as a colored swatch and as a readable text value. The background-color: {{ mf }} expects a valid hex string like #ff6b35 — no extra filter needed because Shopify color metafields already return hex.

2

Add a list metafield renderer that loops through values

Add a list metafield renderer that loops through values. The list.single_line_text_field metafield type stores multiple values. In Liquid, it stringifies as a comma-separated list by default — but to render it as proper HTML, you need to split and loop. Add this {% when %} branch:

{% when 'list' %}n  <div class="metafield metafield--list">n    {% if label != blank %}n      <h4 class="metafield__label">{{ label | escape }}</h4>n    {% endif %}n    <ul class="metafield__list">n      {% for value in mf.value %}n        <li>{{ value | escape }}</li>n      {% endfor %}n    </ul>n  </div>

The mf.value property on a list metafield returns an actual Liquid array — that’s the secret. Direct {{ mf }} on a list metafield produces a comma-concatenated string, which is never what you want for HTML. Always access .value and loop.

3

Use the snippet on collections, orders, and other resources

Use the snippet on collection pages, too. Metafields aren’t only for products — they work on collections, orders, customers, and pages. The same metafield.liquid snippet works unchanged because it accepts a generic metafield parameter, not a hardcoded product.metafields.whatever. On a collection template, you’d call:

{% render 'metafield', metafield: collection.metafields.custom.banner_text, type: 'text', label: 'Banner' %}

The snippet doesn’t care what resource the metafield belongs to — a metafield object is a metafield object. This means you can reuse the exact same file across your entire theme.

4

Conditionally render sections — only show wrappers with content

Conditional rendering — only show sections with content. A section that has three metafields shouldn’t render an empty wrapper when all three are blank. Use {% assign %} to collect the metafields into variables first, then check if any of them has a value before rendering the wrapper:

{% assign care = product.metafields.custom.care_instructions %}n{% assign size = product.metafields.custom.size_chart %}n{% assign weight = product.metafields.custom.product_weight %}nn{% if care != blank or size != blank or weight != blank %}n  <section class="product-metafields">n    {% render 'metafield', metafield: care, type: 'text', label: 'Care Instructions' %}n    {% render 'metafield', metafield: size, type: 'image', label: 'Size Chart' %}n    {% render 'metafield', metafield: weight, type: 'measurement', label: 'Weight' %}n  </section>n{% endif %}

This pattern prevents empty <section> elements from appearing in the DOM when a product has no metafield data filled in — cleaner markup, fewer layout gaps.

Common pitfalls when working with Shopify metafields:

1. Namespace is the first-level key, not a compound string. product.metafields.custom.care_instructions works. product.metafields['custom.care_instructions'] does not. The dot between namespace and key is Liquid’s property accessor, not part of the key name.

2. Metafield definitions must match the theme’s expected types. If you create a metafield definition as single_line_text_field in the admin but your snippet calls type: 'measurement', the snippet will try to access mf.value and mf.unit — both will be nil on a text metafield, and the snippet silently renders nothing. Always match the admin definition type to the render type parameter.

3. The metafield_tag filter is only for rich text fields. Using it on a plain text metafield produces unpredictable output — sometimes an empty string, sometimes a mangled version of the text. The text branch in Step 5 deliberately uses {{ mf | newline_to_br | strip }} instead, which is the correct filter for plain text.

4. File references vs images — related but distinct. A metafield of type file_reference can accept any file, not just images. If a product’s “size chart” metafield has a PDF instead of a PNG, image_url filter returns nil and the <img> tag has no src. The production snippet handles this implicitly — if image_url returns blank, the {% if mf != blank %} outer guard skips rendering entirely. No broken image icons appear on the page.

Performance notes:

  • Metafields load with the product object. When Shopify renders a product page, it populates product.metafields as part of the standard product drop — no extra API calls, no database queries. The metafield data is already in memory when your snippet runs. Rendering 5 metafields vs 50 metafields adds negligible overhead because Liquid string interpolation and filter application are in-process operations.
  • Metafield caching is automatic. Shopify’s storefront server caches the product object (including all metafields) at the CDN edge. The first request to a product after a metafield update regenerates the cache; all subsequent requests serve from cache. If a metafield hasn’t changed, the snippet’s output hasn’t changed either — the cached product object produces identical Liquid output, and Shopify serves the cached HTML without running Liquid again.
  • Image CDN URLs from image_url are stable. The image_url filter produces a path on Shopify’s CDN — cdn.shopify.com. The image file itself is cached with a long-lived cache header. The snippet doesn’t upload or transform images; it just constructs a URL to the already-cached file reference.
  • Rich text metafield_tag performs server-side rendering. Shopify’s metafield_tag filter converts the internal rich text representation (similar to ProseMirror JSON) into HTML on the server before the page is served. This happens once per metafield per request, which is the same cost as rendering any other Liquid output. If your product page renders 3 rich-text metafields, you pay the conversion cost 3 times — negligible in practice, but worth noting if you have a page with 50+ rich-text fields.
  • The case block is a compile-time optimization. Liquid compiles {% case type %} into a jump table — not a sequential chain of {% if %} comparisons. The runtime cost is the same whether you have 2 branches or 20. Adding more type handlers (color, list, date, JSON) doesn’t slow down the text/image/measurement branches.
0 comments

No comments yet. Be the first.

Leave a Reply

Table of Contents

Shopify product metafield — Code Snippet

Shopify product metafield — code snippet by Mosharaf Hossain

The Shopify product metafield pattern keeps Shopify Liquid templates predictable because the badge or metafield logic lives in one reusable snippet instead of being repeated across product cards and templates.

This Shopify product metafield is a production-tested PHP snippet for WordPress developers. See the Shopify metafield Liquid documentation for related official documentation.

The Shopify product metafield pattern provides a consistent, nil-safe way to read and render product metafields in any theme section — without conditionals scattered across every template that touches metafield data. It handles the three most common metafield types in one reusable snippet: single line text, multi-line text, and rich text.

The problem with reading metafields directly in Liquid is that they return nil when not defined, causing Liquid to output nil as a string in some contexts and nothing in others. If you wrap every metafield output in a separate conditional block for each template that uses metafield data, the templates become cluttered and the nil-safe logic gets duplicated across the codebase. This snippet centralises the nil check and the type-conditional rendering in one place, called from any section with a single render tag.

The snippet checks three conditions before rendering: the namespace and key must resolve to a non-nil value, the value must have a truthy type, and the rendered output must be a non-empty string. Only then does it render the HTML wrapper. This prevents empty containers from appearing in the DOM, which cause layout shifts and accessibility issues. Empty heading elements with no content are flagged by accessibility auditors and screen readers.

For rich text metafields, Shopify returns an HTML string that requires the metafield_tag filter for correct output. The raw value contains Liquid-escaped HTML that does not render correctly without it. The snippet applies this filter conditionally based on the metafield type property, so each metafield type gets the correct output method.

Place the snippet at snippets/product-metafield.liquid and call it from any section that displays product data. The label parameter renders a visually visible label above the value. Pass an empty string to suppress the label for design-only metafield display.

Tested on Shopify 2.0+ with metafield definitions created in the Admin under Products then Metafields. Browse all Shopify snippets at Code Snippets or see the companion Shopify Product Badge pattern. Production-tested in the Omnia Stores product page template.

Related: Omnia Stores Shopify project — and browse the full code snippet library.

Chat on WhatsApp