Native bouwenBuild
Eén metaobject, zes oppervlakken: de creator-funnel op onze demo-store
Diek Thunnissen8 sep 202611 min leestijd
Dezelfde entry op de productpagina, op een eigen landingspagina, in de structured data, in een Flow, in een checkout-blok en via de Storefront API. Stap voor stap met de code die op onze demo-store draait, en een actie-coupon die zichzelf aan- en uitzet als klein voorbeeld.

De regel achter dit stuk staat in de gids Metaobjects als besturingslaag: een theme setting sterft met je thema, een metafield reist met je product, een metaobject leeft overal. Hier de praktijk. Op onze demo-store bestaat een creator (naam, handle, kanaal, intro, galerij, producten) precies één keer, als metaobject-entry. Zes plekken lezen die entry. We lopen ze langs, met de code.
Oppervlak 1: het thema
In Liquid bereik je een metaobject op twee manieren. Via de brug (product.metafields.custom.config.value, dan de velden) of rechtstreeks op handle via shop.metaobjects. Onze cart-drawer doet het tweede: het cart-attribuut bron bevat de handle van de creator via wie de bezoeker binnenkwam, en de drawer zoekt de bijbehorende entry op.
{% comment %} snippets/diek-cart-creator.liquid (demo-store), kern {% endcomment %}
{%- liquid
assign diek_cc_bron = cart.attributes.bron
assign diek_cc_creator = blank
if diek_cc_bron != blank
assign diek_cc_creator = shop.metaobjects.creator[diek_cc_bron]
endif
-%}
{%- if diek_cc_creator != blank -%}
{%- liquid
assign diek_cc_label = diek_cc_creator.handle_label.value
if diek_cc_label == blank
assign diek_cc_label = diek_cc_creator.system.handle | prepend: '@'
endif
assign diek_cc_avatar = diek_cc_creator.gallery.value | first
assign diek_cc_deals = shop.metafields.custom.creator_deals.value
if diek_cc_deals
assign diek_cc_deal = diek_cc_deals[diek_cc_bron]
endif
-%}
<div class="diek-cart-creator">
{%- if diek_cc_avatar -%}
{{ diek_cc_avatar | image_url: width: 80 | image_tag: loading: 'lazy', alt: diek_cc_label }}
{%- endif -%}
<p><strong>{{ diek_cc_label }}'s korting actief.</strong> {{ diek_cc_deal.percent }}% korting, automatisch verrekend.</p>
</div>
{%- endif -%}Let op de vorm: een veld is een object met een .value, en system.handle en system.type zitten op elke entry. Eén valkuil kostte ons een uur: op lijstvelden van een metaobject werkt geen bracket-index en geen .first als property. Alleen een forloop, of de filter | first. Wil je het derde beeld uit een galerij, dan loop je erdoorheen.
{% comment %} sections/diek-creator-products.liquid (demo-store) {% endcomment %}
# Gotcha: bracket-indexing/.first werken niet op metaobject-lijstvelden,
# alleen forloop en de | first-filter. Index 2 = het full-body look-beeld.
assign look_beeld = blank
for galerij_item in creator.gallery.value
if forloop.index0 == 2
assign look_beeld = galerij_item
break
endif
endfor
if look_beeld == blank
assign look_beeld = creator.gallery.value | first
endifWat de brug via een metafield extra oplevert: in de thema-editor kan een marketeer die velden als dynamische bron aan een blok koppelen. Geen developer nodig om een nieuw veld op de productpagina te krijgen, zolang de sectie het al ondersteunt.
Oppervlak 2: een eigen pagina per entry
Een metaobject-definitie kan entries publiceren als webpagina. Eén vinkje in de definitie ("publiceer entries als webpagina's"), een URL-prefix, en elke entry heeft een adres. Op de demo-store is dat de creator-landingspagina: de kale URL is de link die de creator deelt, en de pagina zet zelf het cart-attribuut dat de rest van de funnel aanstuurt.
Het thema krijgt daarvoor één template, templates/metaobject/creator.json, en de secties erin lezen het metaobject-object dat Shopify op zo'n pagina beschikbaar stelt. Zonder metaobject-context renderen ze niets, zodat je ze veilig in de editor kunt laten staan.
{% comment %} sections/diek-creator-hero.liquid (demo-store), kop {% endcomment %}
{%- assign creator = metaobject -%}
{%- if creator != blank and creator.system.type == 'creator' -%}
{%- liquid
assign handle_label = creator.handle_label.value
if handle_label == blank
assign handle_label = creator.system.handle | prepend: '@'
endif
assign deal = blank
assign deals = shop.metafields.custom.creator_deals.value
if deals
assign deal = deals[creator.system.handle]
endif
assign tag_product = creator.products.value | first
assign avatar = creator.gallery.value | first
-%}
<h1>{{ creator.headline.value | default: creator.name.value }}</h1>
{%- if creator.intro.value != blank %}<p>{{ creator.intro.value }}</p>{% endif %}
{%- if deal %}<span>{{ deal.label | default: 'Creator-deal' }}</span>{% endif %}
{%- endif -%}Dezelfde entry, twee oppervlakken: de landingspagina en de cart-drawer. De creator zelf bestaat één keer, in Content, Metaobjecten. Een nieuwe creator toevoegen is een entry invullen. Geen deploy, geen template kopiëren.
Oppervlak 3: structured data
Zoekmachines en AI-assistenten lezen je productdata via de standaardvelden en de categorie-metafields. Alles wat daar niet in past (materiaal, herkomst, compatibiliteit, garantie) ziet niemand, tenzij je het als structured data meegeeft. Als die data al in een metaobject staat, is dat een snippet van tien regels.
{% comment %} snippets/jsonld-config.liquid: velden uit het config-metaobject in schema.org/Product {% endcomment %}
{%- assign config = product.metafields.custom.config.value -%}
{%- if config -%}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Product",
"@id": {{ canonical_url | json }},
"name": {{ product.title | json }},
"material": {{ config.materiaal.value | json }},
"countryOfOrigin": {{ config.herkomst.value | json }},
"additionalProperty": [
{%- for spec in config.specificaties.value -%}
{ "@type": "PropertyValue", "name": {{ spec.naam.value | json }}, "value": {{ spec.waarde.value | json }} }{% unless forloop.last %},{% endunless %}
{%- endfor -%}
]
}
</script>
{%- endif -%}Eén bron, dus ook hier: wijzig je het materiaal in de entry, dan klopt de productpagina, de landingspagina en de structured data tegelijk. Dat is het argument tegen een apart SEO-veld in een app.
Oppervlak 4: Flow
Flow heeft triggers op metaobjects: entry aangemaakt, entry bijgewerkt. Daarmee wordt een entry een knop. Op de demo-store is de pre-ordercampagne één metaobject ("Najaar 2026") met een releasedatum, een limiet, een toelichting en een lijst producten. Sla je die entry op, dan stampt Flow de waarden als metafields op elk product in de lijst.
Waarom die omweg? Omdat Shopify Functions geen metaobject-referenties volgen. Een Function leest een metafield op het product of de shop, maar kan niet doorklikken naar de entry erachter. Het metaobject is dus de beheer-bron, de metafields zijn de kopie waar de checkout mee rekent, en Flow houdt ze gelijk.
Flow: Pre-order campagne sync (flow/preorder-campagne-sync.md, kern)
Trigger Metaobject entry updated → definitie: Pre-order
(tweede workflow met Metaobject entry created)
For each metaobjectEntry.fields.products
Actie Send Admin API request → metafieldsSet
{
"metafields": [
{ "ownerId": "{{ forEachItem.id }}", "namespace": "custom", "key": "preorder_release_date", "type": "date", "value": "{{ metaobjectEntry.release_date.value }}" },
{ "ownerId": "{{ forEachItem.id }}", "namespace": "custom", "key": "preorder_limit", "type": "number_integer", "value": "{{ metaobjectEntry.limit.value }}" },
{ "ownerId": "{{ forEachItem.id }}", "namespace": "custom", "key": "preorder_note", "type": "single_line_text_field", "value": "{{ metaobjectEntry.note.value }}" }
]
}De teller (hoeveel pre-orders er nog zijn) staat bewust buiten de sync. Die telt een andere Flow af per bestelling, en een campagne-update mag hem niet resetten.
Oppervlak 5: de checkout
Checkout-extensies kunnen metaobjects rechtstreeks opvragen via de Storefront API, als de definitie storefront-toegang heeft. Op de demo-store leest het bedankpagina-blok de entry van het kanaal waar de klant vandaan kwam: TikTok-koper ziet TikTok-content, creator-koper ziet de creator terug.
// extensions/checkout-creator-thankyou/src/CreatorThankyou.tsx (diek-checkout), kern
const CREATOR_QUERY = `#graphql
query CreatorThankyou($handle: MetaobjectHandleInput!) {
metaobject(handle: $handle) {
channel: field(key: "channel") { value }
media: field(key: "thankyou_media") {
reference {
... on MediaImage {
image { url }
}
}
}
text: field(key: "thankyou_text") { value }
link: field(key: "creator_link") { value }
}
}
`;
const bron = bronAttr.toLowerCase().replace(/[^a-z0-9_-]/g, '');
if (!bron || bron === 'default') return;
const { data } = await shopify.query(CREATOR_QUERY, {
variables: { handle: { type: 'channel_content', handle: bron } },
});
const entry = data?.metaobject;
if (!entry || (!entry.text?.value && !entry.media?.reference?.image?.url)) return;De definitie erachter is zelf een mooi voorbeeld van metaobjects die naar metaobjects wijzen: channel_content heeft een kanaal, een kopregel en twee lijsten met referenties naar testimonial- en usp-entries. Eén rij per kanaal, en het testimonials-blok in de checkout toont per kanaal een andere selectie uit dezelfde reviews.
# metaobjects/seed-advanced.sh (diek-checkout), definitie channel_content
type: "channel_content", name: "Kanaal-content", displayNameKey: "channel",
access: { storefront: "PUBLIC_READ" },
fieldDefinitions: [
{ key: "channel", name: "Kanaal", type: "single_line_text_field", required: true },
{ key: "headline", name: "Kopregel", type: "single_line_text_field" },
{ key: "testimonials", name: "Reviews", type: "list.metaobject_reference", validations: [{ name: "metaobject_definition_id", value: <testimonial-definitie> }] },
{ key: "usps", name: "USP's", type: "list.metaobject_reference", validations: [{ name: "metaobject_definition_id", value: <usp-definitie> }] }
]Oppervlak 6: headless en andere kanalen
Hier zit het punt dat Taylor Page tot zijn stelling bracht. Een merk verkocht via een extern kanaal dat zijn productconfiguratie nodig had, en die stond in theme settings. Onbereikbaar. Na de verhuizing naar een metaobject was het één Storefront API-query, dezelfde die je thema ook zou kunnen doen.
# Storefront API: product → brug → metaobject → gerefereerde entries, in één request
query ProductConfig($handle: String!) {
product(handle: $handle) {
title
config: metafield(namespace: "custom", key: "config") {
reference {
... on Metaobject {
handle
materiaal: field(key: "materiaal") { value }
herkomst: field(key: "herkomst") { value }
specificaties: field(key: "specificaties") {
references(first: 20) {
nodes {
... on Metaobject {
naam: field(key: "naam") { value }
waarde: field(key: "waarde") { value }
}
}
}
}
}
}
}
}
}Een app, een marketplace-koppeling, een kiosk in de winkel, een headless storefront: allemaal dezelfde query. Voor de meeste Nederlandse MKB-shops is headless geen doel, maar het argument geldt ook voor het kleinere geval: de feed-app, de PIM-koppeling of de e-mailtool die je productdata wil hebben.
De actie die zichzelf aan- en uitzet
Terug naar de vraag waar dit stuk mee begon, met een klein voorbeeld dat alle drie de opties naast elkaar zet. Een automatische korting loopt van maandag tot zondag op een deel van het assortiment. De productpagina moet die week een coupon tonen, daarna niet meer, zonder dat iemand op zondagnacht iets hoeft te doen.
Als theme setting: een tekstveld en een aan/uit-knop. Dan staat de coupon op elke productpagina of nergens, en de knop moet met de hand om. Als metafield per product: dertig keer invullen, dertig keer leegmaken, en opnieuw beginnen als de actie verhuist. Als metaobject: één entry met naam, coupontekst, begin, einde en een productlijst. De productpagina kijkt of er een actie loopt die dit product bevat.
Metaobject-definitie: actie (custom.actie) naam single_line_text_field "Zomerdeal" coupon single_line_text_field "Deze week 15% korting, automatisch in je winkelwagen" start date 2026-09-14 einde date 2026-09-20 producten list.product_reference [Het Overshirt, De Sweater, ...] collecties list.collection_reference (optioneel) Toegang: Storefront = PUBLIC_READ
{% comment %} snippets/actie-coupon.liquid {% endcomment %}
{%- assign vandaag = 'now' | date: '%s' | plus: 0 -%}
{%- assign actieve_actie = blank -%}
{%- for actie in shop.metaobjects.actie.values -%}
{%- assign start = actie.start.value | date: '%s' | plus: 0 -%}
{%- assign einde = actie.einde.value | date: '%s' | plus: 86399 -%}
{%- if vandaag < start or vandaag > einde -%}
{%- continue -%}
{%- endif -%}
{%- for p in actie.producten.value -%}
{%- if p.id == product.id -%}
{%- assign actieve_actie = actie -%}
{%- break -%}
{%- endif -%}
{%- endfor -%}
{%- if actieve_actie == blank and actie.collecties.value != blank -%}
{%- for c in actie.collecties.value -%}
{%- for pc in product.collections -%}
{%- if pc.handle == c.handle -%}
{%- assign actieve_actie = actie -%}
{%- break -%}
{%- endif -%}
{%- endfor -%}
{%- if actieve_actie != blank -%}{%- break -%}{%- endif -%}
{%- endfor -%}
{%- endif -%}
{%- if actieve_actie != blank -%}{%- break -%}{%- endif -%}
{%- endfor -%}
{%- if actieve_actie != blank -%}
<div class="actie-coupon" role="note">
<span class="actie-coupon__naam">{{ actieve_actie.naam }}</span>
<span class="actie-coupon__tekst">{{ actieve_actie.coupon }}</span>
<span class="actie-coupon__einde">t/m {{ actieve_actie.einde.value | date: '%-d %B' }}</span>
</div>
{%- endif -%}De einddatum krijgt 86.399 seconden erbij zodat de laatste dag tot middernacht loopt, en plus: 0 dwingt een getalvergelijking af. En omdat het een metaobject is, staat dezelfde entry meteen klaar voor de collectiebalk, een Slack-bericht via Flow op de startdag, en een eigen landingspagina per actie met één vinkje.
Wil je dat de korting zelf ook uit die entry komt, dan geldt de Flow-regel van hierboven: de Function leest geen metaobject, dus Flow zet bij het opslaan van de actie het percentage en de datums als metafields op de producten (of als json op de shop), en de Discount Function leest die. Eén beheerplek, twee kopieën die zichzelf bijhouden.
Veelgestelde vragen.
- Kan een Shopify Function een metaobject lezen?
- Niet rechtstreeks. Input queries van Functions volgen geen metaobject-referenties. Zet wat de checkout nodig heeft als metafield op het product of als json-metafield op de shop, en laat Flow die bij elke wijziging van het metaobject bijwerken. Het metaobject blijft de beheer-bron.
- Hoeveel entries kan een thema in Liquid ophalen?
- shop.metaobjects geeft per type een beperkt aantal entries terug, geen onbeperkte lijst. Voor acties, creators en configuraties is dat ruim genoeg. Voor grote sets koppel je entries via een metafield op het product, zodat je alleen ophaalt wat bij dat product hoort, of je zoekt direct op handle.
- Waarom niet gewoon een app voor dit soort content?
- Een app slaat de data op in een eigen database buiten Shopify. Het thema kan erbij via het script van de app, maar Flow, de checkout, de Storefront API en andere kanalen niet. Een metaobject leeft in Shopify zelf, elke laag van het platform kan hem lezen, en hij blijft bestaan als je de app opzegt.
Diek ThunnissenFounder & lead developer. Bouwt, verbetert en migreert de Shopify-laag voor DTC- en B2B-merken. LinkedInPlan een gesprek →

