Most DTC Shopify themes hit a wall where product-specific merchandising content lives in section settings, collection descriptions, or an old app's database. Moving that content to metafields is the unlock for a cleaner theme and a merchandising surface the team can actually edit. This is the migration sequence I run without breaking production traffic during the cutover.
Why Shopify metafield migration is worth the effort
Three things stop improving on a DTC theme until you migrate merchandising content to metafields.
First, merchandising velocity. If the hero copy for a product lives inside a section setting in the theme editor, only someone who knows the theme editor can change it. Metafields put the content on the product itself, so a merchandiser can edit from the product page without touching layout code. A week of marketing backlog clears overnight.
Second, theme portability. Themes that read everything from section settings are hard to migrate because the settings disappear when you switch themes. Themes that read from metafields survive theme upgrades, forks, and full rebuilds because the content lives on the product, not on the theme version. The metafield-driven section architecture is what this unlocks.
Third, agent-assisted development. An agent that generates new Liquid sections can reliably read from a known metafield contract. It cannot reliably read from a section setting schema that changes per theme version. If you want the benefits covered in the dev loop rhythm I run with Claude Code, a clean metafield contract is a prerequisite.
The migration itself is not hard. The work is boring and the failure modes are specific. The sequence below avoids the worst of them, and it drops into the DTC Shopify theme architecture I run past 2M as the merchandising layer.
Prerequisites
- An Online Store 2.0 Liquid theme with a reasonably clean structure (if the theme is already a mess, fix the theme first; a metafield migration on a bad theme is lipstick on a pig).
- Admin API access to the store with
read_products,write_products, andwrite_metaobjectsscopes if you use metaobjects. - A staging storefront or preview theme to verify the migration before cutover.
- A scripting environment for the bulk write (I use Node.js with
@shopify/admin-api-client; Deno or Python work too). - A spreadsheet of the legacy content if the data doesn't live cleanly in the theme or an app export. You will need to see it all at once during the mapping step.
Step 1: Inventory the legacy content
Before defining any metafields, catalog what you actually have. For each piece of merchandising content, record where it lives now (section setting key, collection description, app field, hardcoded Liquid, product description HTML blob), how often it changes, and who owns editing it today.
A typical DTC theme surfaces four or five kinds of product-specific content that are candidates for metafield migration: product hero copy, accent color, product-specific callouts, bundle configuration, and sometimes a per-product comparison table or faq block.
The inventory output is a spreadsheet with columns: legacy location, content example, edit frequency, current owner, target metafield name (leave blank for now). That spreadsheet is the migration plan. Do not skip it; the next four steps depend on it being complete and accurate.
Step 2: Define the metafield schema in admin
Shopify has had proper metafield definitions with native admin UI since 2022. Use them. Do not use the old "just write a metafield with whatever key you want" approach; defined metafields get real admin form fields, validation, and nicer display in the product page.
For each row in your inventory, define a metafield:
- Namespace: a single namespace for the whole theme, typically
themeor your brand's namespace like<brand>_theme. - Key: snake_case, descriptive.
hero_copy,hero_accent,bundle_items,feature_callouts. - Type: pick the narrowest correct type.
single_line_text_fieldfor short headlines.rich_text_fieldfor paragraph content that needs formatting.colorfor colors.list.referencefor related products.jsononly as a last resort when the content is genuinely unstructured. - Validation: add a max length for text fields, a regex for strict formats, a reference type for references.
- Access: mark as "storefront accessible" so Liquid can read it. Mark admin-editable unless there's a reason not to.
Define the metafields before you migrate any data. Shopify's bulk-write API requires the definition to exist first; it will silently accept writes to undefined metafields, but they won't appear in the product admin UI the way the merchandising team expects.
Step 3: Map legacy data to the new fields
Open your inventory spreadsheet and fill in the target metafield name column. For each product, you need to know which legacy value maps to which new metafield. This is the step that people want to skip. Don't.
If the legacy content lives in section settings, you can usually extract it by walking the theme's JSON template files (under templates/product.*.json or sections/*.json). If it lives in an app's database, the app usually has an export or at least an admin-visible data table you can scrape. If it lives in product description HTML, you have more manual parsing ahead of you.
Produce a CSV with one row per product and columns for each target metafield. The format I use looks like this:
product_id,product_handle,hero_copy,hero_accent,bundle_items
7891234567890,sleepy-pillow,"The pillow you actually wake up on","#2e3a59","8901234567891|8902345678902"
Pipe-delimited lists for references so you can split them during the bulk write. Human-readable hex colors. ASCII text. The CSV is the source of truth going into step 4; if it's wrong, the migration is wrong.
Step 4: Bulk-write via the Admin API with rate limits
Now the script. For each row in the CSV, post one or more productUpdate mutations with the metafields payload. Shopify's Admin API throttles aggressively, so you need a leaky-bucket backoff. I cover the specifics in the Shopify Admin API backoff walkthrough; the short version is a serial queue that respects the Retry-After header and caps concurrent requests at 2.
import { createAdminApiClient } from "@shopify/admin-api-client";
import { parse } from "csv-parse/sync";
import fs from "node:fs";
import { writeBucket } from "./rate-limited-bucket.js"; // leaky-bucket helper
const client = createAdminApiClient({
storeDomain: process.env.SHOPIFY_STORE,
apiVersion: "2024-10",
accessToken: process.env.SHOPIFY_ADMIN_TOKEN,
});
const MUTATION = `#graphql
mutation updateProductMetafields($productId: ID!, $metafields: [MetafieldInput!]!) {
productUpdate(input: { id: $productId, metafields: $metafields }) {
product { id }
userErrors { field message }
}
}
`;
const rows = parse(fs.readFileSync("migration.csv"), { columns: true });
for (const row of rows) {
const metafields = [
{ namespace: "theme", key: "hero_copy", type: "single_line_text_field", value: row.hero_copy },
{ namespace: "theme", key: "hero_accent", type: "color", value: row.hero_accent },
{
namespace: "theme",
key: "bundle_items",
type: "list.product_reference",
value: JSON.stringify(
row.bundle_items
.split("|")
.filter(Boolean)
.map((id) => `gid://shopify/Product/${id}`)
),
},
].filter((m) => m.value && m.value !== "");
if (metafields.length === 0) continue;
await writeBucket(async () => {
const response = await client.request(MUTATION, {
variables: { productId: `gid://shopify/Product/${row.product_id}`, metafields },
});
const errors = response.data?.productUpdate?.userErrors;
if (errors?.length) {
console.error(`[${row.product_handle}] userErrors:`, errors);
} else {
console.log(`[${row.product_handle}] ok`);
}
});
}
The filter(m => m.value && m.value !== "") line matters. If a product has no hero copy in the legacy data, don't write an empty string to the metafield; skip that field. Empty strings render as empty in Liquid and will look like a regression if a merchandiser later wonders why the hero area went blank.
Run the script in batches: 50 products at a time, review the output, move on. Do not run all 10,000 products in one shot on the first pass. The Admin API will sometimes return spurious userErrors on edge cases (a product with an unusual variant structure, a product with a legacy metafield type conflict), and catching those early is worth the extra minutes.
Step 5: Update the Liquid to read the new fields
With the metafields populated in production, update the theme's Liquid to read them. The pattern I run uses a three-tier fallback chain: metafield first, section setting second, hardcoded default third. That fallback is what makes the cutover safe.
{% assign hero_copy = product.metafields.theme.hero_copy | default: section.settings.hero_copy | default: "Shop the collection" %}
{% assign hero_accent = product.metafields.theme.hero_accent | default: section.settings.hero_accent | default: "#1a1a1a" %}
<h2 style="color: {{ hero_accent }};">{{ hero_copy }}</h2>
During the switchover, products that were successfully migrated read from metafields. Products that failed or got skipped fall back to the section setting or hardcoded default. The theme doesn't break for any product; it just shows the old content for the stragglers until they're migrated in a follow-up pass.
This fallback pattern is also why you should migrate the Liquid as a separate PR from removing the section settings. Ship the fallback-capable Liquid first, verify production traffic is stable, then remove the now-unused section settings in a later PR.
Step 6: Switch over with a fallback window
Hold the fallback chain in production for a full merchandising cycle (usually 2-4 weeks, depending on how often the team edits products). Watch for products where the metafield is wrong or missing. Fix those one at a time. Once every product is surfacing from metafields and the merchandising team confirms the admin UX is working, ship the cleanup PR that removes the legacy section settings from the theme.
A clean cleanup PR: delete the legacy section settings, delete the fallback chain in Liquid, delete the migration script from the repo (archive it somewhere outside the main repo for future reference), update the theme's README to document the new metafield contract.
After the cleanup PR ships, the merchandising surface is fully metafield-driven. The theme is thinner. The agent workflow is reliable. The next section you add reads the same metafield contract as the ones already shipped.
“Ship the fallback-capable Liquid first, verify production traffic is stable, then remove the now-unused section settings in a later PR. Do not combine the two.”
Common mistakes
Writing undefined metafields. The Admin API accepts the write, but the values don't appear in admin as first-class fields. Define first, write second.
Empty-string writes. An empty string is a value. Filter blank fields out of the metafield payload so missing legacy data doesn't overwrite a field with emptiness.
Ignoring rate limits. The Admin API will 429 you if you fire thousands of writes without pacing. Leaky-bucket, honor Retry-After, and cap concurrency at 2.
Combining migration and theme cleanup in one PR. Separate them. The fallback Liquid is the safety net during migration; removing it before the migration is fully validated is how you take a store down at 3pm on a Friday.
Forgetting storefront access. A defined metafield with storefront access disabled doesn't render in Liquid. If your theme shows nothing after the migration, check the storefront-access flag on the definition.
What to try next
With the migration done, the next layer is building out sections that exploit the metafield contract. Heroes, PDP feature callouts, bundle configurations, cross-sell modules. Each one now reads from the product, not from theme settings, and ships with an agent-friendly pattern.
If your theme still feels heavy after the migration, run a performance pass. Metafield-driven sections are usually lighter because they remove data-fetching from client-side JS, but the theme might still carry the old settings schemas and legacy Liquid branches. The Core Web Vitals sequence for DTC Shopify is the cleanup pass to run after the structural work is done.
For the base theme that ships with metafield-driven sections already wired, see the Shopify Theme Starter repo. For the four-layout engagement that grounded this walkthrough, the four-variant content module build is the case study.
Can I migrate metafields without any theme downtime?
Yes, if you run the three-tier fallback chain during cutover. The Liquid reads the metafield first, then falls back to section settings, then to a hardcoded default. Products the migration finishes for start surfacing metafields immediately; stragglers keep the old content until they're migrated in follow-ups.
How long does a Shopify metafield migration typically take?
For a mid-market DTC store with 500-5,000 products and four or five legacy fields per product, the core migration runs 1-2 weeks: inventory and schema in the first few days, script and bulk write in a day, Liquid updates and fallback verification over the following week. The cleanup PR ships 2-4 weeks later after the merchandising team has edited the fields and confirmed the admin UX.
What's the difference between metafields and metaobjects?
Metafields attach data to existing Shopify resources (products, collections, customers). Metaobjects are standalone entities you define. For product-specific merchandising content, metafields are usually the right choice. Metaobjects shine for shared content that lives across products (a global faq library, a recipe catalog, a location list).
Should I migrate collection-specific content too?
If the theme shows collection-specific content that isn't just the collection description, yes. Define metafields on the Collection resource and run the same migration pattern. The logic and script shape are identical; only the resource type changes.
Can I use metafields for SEO content like schema markup?
Yes. Product-level SEO schema fields (aggregateRating, brand, specific identifiers) are a natural fit for metafields. Use single_line_text_field for simple values or a json metafield for more complex schema payloads. The Liquid renders them into a JSON-LD script tag on the PDP.
Does bulk-writing metafields hit any API rate limits?
Yes. Shopify's Admin API throttles around 2 requests per second for typical stores (higher on Plus). A migration of 5,000 products updating 4 metafields each is 5,000 API calls, which takes about 45 minutes with proper leaky-bucket pacing. Don't try to parallelize it past 2-3 concurrent requests; you'll melt the rate limit and everything will retry.
Sources and specifics
- The migration pattern was validated during a Q4 2025 DTC theme build with four layout variants and theme-owned Meta CAPI.
- The leaky-bucket pattern referenced uses a serial queue with 2 concurrent workers, Retry-After honoring, and exponential jitter on failure.
- Shopify's metafield definitions UI has been GA since 2022 and the GraphQL mutation is
metafieldDefinitionCreate. - Storefront access on a metafield definition must be explicitly enabled for Liquid to read the values.
- Code samples assume Shopify Admin API version 2024-10 and
@shopify/admin-api-clientv1.x.
