Metaobject Migration Blueprint: Replace Tag Sprawl with Scalable Shopify Filtering

By Milan Dhameliya · · 7 min read

Migrate Shopify tags and legacy metafields to metaobjects with GraphQL mapping, canonical filter keys, and storefront-safe filtering.

Tag-based faceting collapses when your catalog reaches scale. Synonyms drift, filters fragment, and merchandising becomes spreadsheet maintenance instead of system governance.

Where this breaks on scaling stores

  1. One intent creates multiple tags (organic cotton, organic-cotton, org-cotton).
  2. Filter values exceed practical UI limits and hurt conversion paths.
  3. Headless storefront filtering becomes unstable because taxonomy is not relational.
  4. Teams cannot safely enforce naming with manual tag edits.

Migration architecture

Use metaobjects as the controlled taxonomy layer and map products to canonical filter keys.

Define the taxonomy object

UI path: Settings > Custom data > Metaobjects.

mutation CreateMaterialDefinition {
  metaobjectDefinitionCreate(
    definition: {
      name: "Material"
      type: "material"
      fieldDefinitions: [
        {
          key: "label"
          name: "Label"
          type: "single_line_text_field"
          required: true
        }
        {
          key: "filter_key"
          name: "Filter key"
          type: "single_line_text_field"
          required: true
          capabilities: {
            adminFilterable: { enabled: true }
          }
        }
        {
          key: "synonyms"
          name: "Synonyms"
          type: "list.single_line_text_field"
        }
      ]
    }
  ) {
    metaobjectDefinition {
      id
      type
    }
    userErrors {
      field
      message
      code
    }
  }
}

Seed normalized entries

mutation CreateMaterialOrganicCotton {
  metaobjectCreate(
    metaobject: {
      type: "material"
      fields: [
        { key: "label", value: "Organic Cotton" }
        { key: "filter_key", value: "organic-cotton" }
        { key: "synonyms", value: "[\"organic cotton\",\"org cotton\"]" }
      ]
    }
  ) {
    metaobject {
      id
      handle
      type
    }
    userErrors {
      field
      message
      code
    }
  }
}

Product mapping pattern

Use dual metafields on the product:

  1. custom.material_refs as list.metaobject_reference
  2. custom.material_keys as list.single_line_text_field for storefront filters
mutation AttachMaterials {
  metafieldsSet(
    metafields: [
      {
        ownerId: "gid://shopify/Product/1234567890"
        namespace: "custom"
        key: "material_refs"
        type: "list.metaobject_reference"
        value: "[\"gid://shopify/Metaobject/111\",\"gid://shopify/Metaobject/112\"]"
      }
      {
        ownerId: "gid://shopify/Product/1234567890"
        namespace: "custom"
        key: "material_keys"
        type: "list.single_line_text_field"
        value: "[\"organic-cotton\",\"recycled-poly\"]"
      }
    ]
  ) {
    userErrors {
      field
      message
      code
    }
  }
}

Storefront filter integration

Configure Search & Discovery

UI path: Apps > Search & Discovery > Filters > Add filter.

Select custom.material_keys as the filter source.

Query filters in Storefront API

query CollectionFilters($handle: String!, $filters: [ProductFilter!]) {
  collection(handle: $handle) {
    id
    title
    products(first: 24, filters: $filters) {
      nodes {
        id
        handle
        title
      }
      filters {
        id
        label
        type
        values {
          id
          label
          count
          input
        }
      }
    }
  }
}

Variables:

{
  "handle": "mens-shirts",
  "filters": [
    {
      "productMetafield": {
        "namespace": "custom",
        "key": "material_keys",
        "value": "organic-cotton"
      }
    }
  ]
}

Controlled migration execution

Build one-time mapping script

// pseudo-logic for migration batch
for (const product of legacyProducts) {
  const normalizedKeys = normalizeLegacyTags(product.tags, taxonomyMap);
  await adminGraphql(`
    mutation UpdateProduct($id: ID!, $keys: String!, $refs: String!) {
      metafieldsSet(metafields: [
        { ownerId: $id, namespace: "custom", key: "material_keys", type: "list.single_line_text_field", value: $keys },
        { ownerId: $id, namespace: "custom", key: "material_refs", type: "list.metaobject_reference", value: $refs }
      ]) {
        userErrors { field message code }
      }
    }
  `, {
    id: product.id,
    keys: JSON.stringify(normalizedKeys),
    refs: JSON.stringify(toMetaobjectIds(normalizedKeys))
  });
}

Deprecate tags only after parity checks

  1. Compare collection counts by filter key between old and new systems.
  2. Validate no-impact on top SEO landing collections.
  3. Freeze tag editing permissions in merchant SOPs.

Technical Checklist

SEO Checklist