AppbrewDevelopers
Build

Metafields

Read Shopify metafields from blocks and integrations.

Metafields are custom data attached to Shopify resources: a product, a collection, or the shop itself. They are the right home for any store-specific content your block renders: care instructions, size charts, announcement text. Merchants edit them in Shopify; your block picks up the change with no code deploy.

All metafield reads are asynchronous, so values are undefined until they load. Type every result with an interface and null-check before access.

Which hook to use

Metafields on…Read withImport from
a productuseMultipleMetafields(handle, fields)@gauntlet/state
a collectionuseCollectionMetafields(id, fields)@gauntlet/state
the shopuseShopifyQuery(query)@gauntlet/state

fields is an array of { namespace, key } descriptors.

Product metafields

useMultipleMetafields returns the values directly, keyed as result[namespace][key]. Define the shape you expect and assert it:

src/app/blocks/product-care.tsx
import { useMultipleMetafields } from '@gauntlet/state'

interface CareMetafields {
  custom?: {
    care_instructions?: string
    specs?: string
  }
}

const metafields = useMultipleMetafields(productHandle, [
  { namespace: 'custom', key: 'care_instructions' },
  { namespace: 'custom', key: 'specs' },
]) as CareMetafields | undefined

if (!metafields?.custom?.care_instructions) {
  return null
}

let specs: Array<{ label: string; value: string }> = []
try {
  specs = JSON.parse(metafields.custom.specs ?? '[]')
} catch {}

The product handle comes from your block's props or the product hooks. One quirk to know: a dash in a namespace becomes an underscore in the result, so a my-fields namespace is read as metafields.my_fields.

Collection metafields

Same call shape, but the result holds metafield nodes, so read .value off each:

import { useCollectionMetafields } from '@gauntlet/state'

interface FooterMetafields {
  custom?: { footer_text?: { value?: string } }
}

const metafields = useCollectionMetafields(collectionId, [
  { namespace: 'custom', key: 'footer_text' },
]) as FooterMetafields | undefined

const footer = metafields?.custom?.footer_text?.value
if (!footer) return null

Only have the handle? useCollectionMetafieldsByHandle(handle, fields) takes the same arguments and returns the same shape.

Shop metafields

There is no dedicated hook. Write a small Storefront query against shop, alias each metafield, and run it with useShopifyQuery, typed with an interface:

import { useShopifyQuery } from '@gauntlet/state'

const SHOP_QUERY = `query {
  shop {
    announcement: metafield(namespace: "custom", key: "announcement") { value }
  }
}`

interface ShopResult {
  shop: {
    announcement: { value: string } | null
  }
}

const data = useShopifyQuery<ShopResult>(SHOP_QUERY)

const announcement = data?.shop?.announcement?.value
if (!announcement) return null

useShopifyQuery returns the data or null. There are no separate loading or error flags, and no variables argument; interpolate values into the query string.

Working with the values

  • A metafield value is always a string, numbers included. JSON-typed metafields arrive as a stringified blob, so parse inside try/catch; merchant data is not guaranteed valid.
  • To resolve a linked resource instead of its id string, add reference to the field descriptor: { namespace, key, reference: 'image' } (also 'product', 'variant'). The result is the referenced object.
  • Match namespace and key exactly to what is configured in Shopify, minding the dash-to-underscore rule on product results.

Namespace conventions

Give your integration its own namespace, usually the integration name, and keep every metafield it reads under it. That prevents collisions with the store's custom namespace and with other integrations. It also makes your metafield requirements easy to state in the README: one namespace, a list of keys, and a type for each.

On this page