Skip to content

Claude Code Shopify Development

How to Bulk Edit Shopify SEO Titles and Meta Descriptions With Claude Code

Editing SEO metadata across a catalogue is five steps, and only one of them involves a machine writing anything. The review is the part that matters — including the row where the model invented a lifetime guarantee, and a person caught it.

Claude Code Guides: Bulk Edit Shopify SEO With Claude Code. A review panel of four metadata rows, two approved, one unchanged, one rejected.

The short answer

Editing SEO titles and meta descriptions across a catalogue is five steps, and only one of them involves a machine writing anything: export the current values, draft replacements from facts the product already states, review them one by one and record a decision, apply only the approved rows, then verify what is stored and what renders. The review is the part that matters. Everything either side of it is plumbing.

Before any of that: you may not need an API workflow at all. Shopify's own bulk editor handles the SEO title and description columns directly, and for thirty products on a quiet afternoon it is the right tool. The workflow below earns its place when the batch is large, recurring, or needs a record of who approved what.

The worksheet this guide uses — blank template and a filled-in example — is a free download, and it is useful on its own with no code involved.

Pick the method before you pick the tool

Three routes, and the honest comparison is about batch size and auditability rather than sophistication.

Route Good for What it costs you
Product page, one at a time
Admin → Products → a product → Search engine listing → Edit
A handful of products, or a single fix you want to see rendered immediately. Nothing, until the tenth product. No record of what the old value was.
Native bulk editor
Select products → Bulk edit → add the SEO title and SEO description columns
Tens of products. A spreadsheet-like grid in the admin, no export, no import, no access tokens. No before/after record, no approval trail, and it is a live edit — there is no staging step between typing and saving.
Product CSV export and import Larger catalogues, and teams who already work in spreadsheets. The file has to keep Shopify's own column names and row structure. A CSV round-trip touches more fields than the two you meant to change, so an error is wider than an error in the bulk editor.
Reviewed Admin API workflow Hundreds of products, a recurring job, or when you need to answer "who approved this, and what did it say before?" Setup: an app, scopes, a token you have to keep out of your repository. Worth it on the second run, rarely on the first.

Nothing below requires a paid toolkit. If you finish this guide and conclude the native bulk editor is enough for your store, that is the correct outcome and you should use it.

Access, if you go the API route

You need an app with the write_products scope (and read_products, which it implies for this work) and an Admin API access token installed on the store.

Check the date before you follow an older tutorial. Shopify's changelog states: "Starting January 1, 2026, you can no longer create new custom apps in the Shopify admin." Existing ones are unaffected — "Existing custom apps aren't affected and will continue to work" — but a new one is created in the Dev Dashboard and then installed on the store. A guide that tells you to go to Settings → Apps and sales channels → Develop apps is describing the route that closed. If you are working on a client's store rather than your own, the right authentication depends on that relationship — a token installed on one store is not the same thing as an app distributed to several — and it is worth settling before you write any code.

Four fields that are easy to confuse

This is where the damage happens. Shopify keeps four separate things, and only two of them are the ones you came to edit:

  • SEO title — the <title> of the product page. In the Admin API this is seo.title.
  • SEO description — the meta description. In the API, seo.description.
  • Product title — the name shown on the page, in collections, in the cart and on the order. In the API, title. Changing this changes what customers see everywhere.
  • Handle — the URL slug. In the API, handle. Changing it changes the product's URL and breaks any link to the old one.

They really are distinct inputs, not aliases. Asking the live schema what ProductInput accepts returns seo, title, handle and descriptionHtml as four separate fields, and SEOInput itself has exactly two members:

seo         -> SEOInput   { title: String, description: String }
title       -> String     (the displayed product name)
handle      -> String     (the URL slug)
descriptionHtml -> String (the body copy on the page)

# checked against the Admin GraphQL schema, API version 2025-07

The practical rule: a metadata job sends seo and nothing else. If your payload contains title or handle, you are no longer doing the job you thought you were doing. The worksheet in this guide carries a handle column for human readability and never writes it, which is a deliberate asymmetry worth copying.

Export, and take a small batch

Export first, because the export is your backup. Here is a real read-only export — three products, run against this store on 24 September 2026, no writes:

id:         gid://shopify/Product/10808909201700
handle:     claude-code-agency-client-delivery-system
title:      Claude Code Agency & Client Delivery System
seo.title:  'Claude Code Agency & Client Delivery System | SiteBuilderStack'

id:         gid://shopify/Product/10793863250212
handle:     claude-code-conversion-revenue-optimization-toolkit
title:      Claude Code Conversion & Revenue Optimization Toolkit
seo.title:  'Claude Code Conversion & Revenue Toolkit'

id:         gid://shopify/Product/10793567224100
handle:     claude-code-seo-website-audit-toolkit
title:      Claude Code SEO & Website Audit Toolkit
seo.title:  'Claude Code SEO & Audit Toolkit'

Two things to notice. The SEO title and the product title are different strings on two of the three, which is normal and usually deliberate — the displayed name can afford to be long, the <title> cannot. And the id, not the handle, is what you key on: a handle can change, and if it does mid-job your rows silently stop matching.

Do not export the whole catalogue and rewrite it. A full-catalogue pass is the version of this job most likely to go wrong and least likely to be reviewed properly, because nobody reads four hundred rows carefully. Take a batch with a reason attached to it: products with an empty SEO description, or one collection, or the twenty products with impressions and no clicks in Search Console. A batch you can explain is a batch you can check.

Drafting from facts you already have

The failure mode when a model writes product metadata is not clumsy prose. It is invention — a material, a certification, a guarantee, a dimension, a shipping promise. None of it malicious, all of it plausible, and all of it now published on a page a customer can act on.

The defence is structural: give the model only what the product already says, and require it to flag a gap rather than fill it.

You are drafting SEO metadata for Shopify products. You are drafting only.
You will not call any API and you will not modify anything.

INPUT
I will give you rows exported from a Shopify store. Each row has: product_id,
handle, product_url, current_seo_title, current_seo_description, and the
product's existing title and body copy.

YOUR TASK
For each row, propose a seo title and a meta description.

RULES ON FACTS
- Use ONLY facts present in that row's own title and body copy.
- Do not add a material, size, weight, capacity, certification, warranty,
  guarantee, delivery time, price, discount or country of origin that is not
  already stated in that row.
- Do not write comparative or superlative claims ("best", "leading",
  "highest quality"). They cannot be checked and they date badly.
- Do not write a policy as if it were a product fact. Shipping and returns
  are set elsewhere and change without the product changing.
- If a row does not contain enough to write something specific, put
  "TBC - needs product facts" in both proposed fields, set approval_status
  to "needs info", and say in reviewer_notes exactly which fact is missing.

RULES ON WRITING
- Lead with what the thing is, in the words a buyer would use.
- Prefer a concrete detail over an adjective: a number, a size, a count.
- Roughly 60 characters for the title and roughly 155 for the description,
  as guidance for readability. Do not pad to reach it and do not truncate a
  clearer sentence to meet it.
- If the current value is already accurate and specific, propose the SAME
  text and set approval_status to "no change". Do not churn a good field.

OUTPUT
A CSV with exactly these columns, every value quoted:
product_id, handle, product_url, current_seo_title, current_seo_description,
proposed_seo_title, proposed_seo_description, approval_status, reviewer_notes

Set approval_status to "needs review" for anything you are proposing to
change. A human sets "approved" or "rejected", not you.

In reviewer_notes, say which facts in the source you used. One line.

STOP
Output the CSV and stop. Do not write to Shopify. Do not suggest a command
that would.

The approval_status rule is the load-bearing one. The model never marks its own work approved, so a script that writes only approved rows cannot act on anything a person has not read.

The review, with three real decisions

Here is the middle of a review on a fictional homewares store. Three rows, three different outcomes — this is what the worksheet looks like when it is doing its job:

Product Current Proposed Decision
Linen tea towel set Title: Linen Tea Towel Set
Description: Linen tea towel set. Buy now. Best prices. Free shipping on all orders.
Title: Linen Tea Towel Set of 3 | Example Store
Description: Three washed-linen tea towels, 50 x 70 cm, with hanging loops. Sold as a set.
Approved. "Best prices" cannot be checked and "free shipping" is a policy that changes without the product changing. The count and the dimensions are in the product body, so they are safe to promote into the description.
Navy enamel mug Title: Navy Enamel Mug 350ml | Example Store
Description: A 350 ml navy enamel camping mug with a rolled rim. Dishwasher safe.
Identical to current. No change. Already specific, accurate and readable. Rewriting it would churn the field, cost a review, and give a reader nothing.
Cast iron skillet 26cm Title: Cast Iron Skillet 26cm | Example Store
Description: A 26 cm pre-seasoned cast iron skillet with a helper handle.
Title: Cast Iron Skillet 26cm - Lifetime Guarantee | Example Store
Description: Our lifetime-guaranteed 26 cm skillet is the last pan you will ever buy. Oven safe to 260 C.
Rejected. There is no lifetime guarantee on this product. "The last pan you will ever buy" is unsupportable, and the 260 °C figure appears nowhere in the product data. Three inventions in one row, each individually plausible. Keep the current description.

Note what the reasoning is not: nobody here claims the approved rows will rank better. The case for the tea towel rewrite is that it is true, specific and useful to someone deciding whether to click. That is a defensible reason to change a field. "It will improve our SEO" is not, because you cannot check it afterwards.

The rejected row is the argument for the whole workflow. A model produced it in good faith from a plausible-sounding pattern, and an unreviewed pipeline would have published all three inventions to a live product page.

Applying only what was approved

Be precise about what protects you here, because it is easy to assume Shopify provides safeguards it does not.

Shopify gives you: a GraphQL API that validates input against a schema, and userErrors on the mutation response. Note that userErrors arrives inside an HTTP 200 — a write can fail completely while the request "succeeds", so a script that only checks the status code will report success it did not have.

Shopify does not give you a dry-run mode for productUpdate, a transaction you can roll back, or an undo. There is no --dry-run flag. Anything resembling one is something your own script implements, and it is worth implementing:

  • A reviewed target list. Read the worksheet, keep only approval_status == "approved", and refuse to run if any row has a status the script does not recognise.
  • A backup. Write the current values to a timestamped file before the first mutation. This is your restore path; there is no other one.
  • A preview. Print every planned change — id, field, from, to — and require an explicit confirmation before the first write. This is your dry run, because Shopify has not got one.
  • A staleness check. Re-read each product immediately before writing it and compare against current_seo_title and current_seo_description from the export. If they differ, someone edited that product after your export — skip the row and report it. Overwriting a colleague's fix with a two-day-old draft is the quiet failure in this job.
  • A read-back. After each write, read the product again and assert the stored value equals the approved value. A mutation that returned no error is not the same as a field that changed.
  • Error handling that stops. On the first unexpected userErrors, stop the batch rather than continuing through two hundred products.

The mutation itself is small. It sends seo, and that is all it sends:

mutation UpdateProductSeo($input: ProductInput!) {
  productUpdate(input: $input) {
    product { id handle seo { title description } }
    userErrors { field message }
  }
}

# variables, one product per call:
# { "input": { "id": "gid://shopify/Product/1111111111111",
#              "seo": { "title": "...", "description": "..." } } }
#
# No "title". No "handle". No "descriptionHtml". The payload is the
# smallest thing that does the job, which is also the smallest thing
# that can go wrong.

The implementation prompt should refuse to move without both the list and the confirmation:

Write a script that applies approved SEO metadata changes to Shopify
products from a reviewed worksheet.

REQUIRED BEFORE ANY WRITE
- Read the worksheet CSV I name. Keep only rows where approval_status is
  exactly "approved". If any row has a status that is not one of
  approved / rejected / no change / needs info, stop and tell me.
- Write every current value to a timestamped backup file. Stop if it fails.
- For each target, re-read the live product and compare its stored seo
  title and description with current_seo_title and current_seo_description
  from the worksheet. If either differs, SKIP that row, record it as stale,
  and continue.
- Print the full plan: product id, handle, field, from, to. Then stop and
  wait for me to type the number of rows I expect you to change. Do not
  write until I answer, and refuse if my number does not match your plan.

THE WRITE
- productUpdate, sending only the seo field. Never title, handle or
  descriptionHtml.
- One product per call.
- Check userErrors on every response. It arrives inside an HTTP 200, so a
  200 is not success on its own. On the first unexpected userErrors, stop
  the batch and report what was already written.

AFTER EACH WRITE
- Read the product back and assert the stored values equal the approved
  values. Report any mismatch as a failure, not a warning.

OUTPUT
A report: changed, skipped-as-stale, failed, and the backup file path.
Tell me the exact command that restores from that backup.

Never print or log the access token.

Verifying, and what Google actually promises

Three checks, in this order:

  1. Stored values. Read seo { title description } back from the API and compare against what you approved.
  2. Rendered output. Fetch the live product URL and read the <title> and <meta name="description"> from the HTML. A theme can append a store suffix, or ignore the field altogether. The stored value and the rendered value are different things, and only one of them is what a crawler sees.
  3. The fields you did not touch. Confirm the handle, product title and body are unchanged. If your batch is large, diff the whole product record against the backup rather than eyeballing it.

Then the expectation-setting, which matters more than the checks. A correct meta description does not guarantee Google will show it. Google's own documentation is direct about this: "Snippets are primarily created from the page content itself", and Google uses the meta description tag "if we think it gives users a more accurate description than would be possible purely from the on-page content". On length, Google states there is "no limit on how long a meta description can be, but the snippet is truncated in Google Search results as needed, typically to fit the device width".

The same applies to titles. Read Google's guidance on title links and snippets before you promise anyone a result. What you control is whether the page states clearly and truthfully what it is. What gets displayed is Google's call, and it may differ per query and per device.

When it goes wrong

Symptom Usually Do this
Access denied on productUpdate, reads fine The app has read scope but not write_products. Add the scope, reinstall the app, and get a new token. Changing scopes does not upgrade an existing token.
Invalid API key or access token The token was rotated, the app uninstalled, or you are pointing at the wrong store. Confirm the shop domain first — it is the more common of the two, and it looks like an auth failure.
HTTP 200, nothing changed userErrors is populated and nobody read it. Print userErrors on every response. Treat a non-empty array as a failure.
Fields you did not mean to edit have changed The payload carried title or handle, or a CSV round-trip rewrote columns you left blank. Restore from the backup. Then narrow the payload to seo and re-run.
A row wrote the wrong product Keyed on handle, and a handle changed between export and write. Key on product_id. Handles are not stable identifiers.
Your change disappeared a day later Someone edited in the admin, or an older export was re-applied. This is what the staleness check prevents. Add it before the next batch.
CSV import rejected The review worksheet was uploaded as if it were a Shopify import file. They are different formats. See the worksheet README — approved values go into Shopify's own export columns, or through the API.
Throttled on a large batch GraphQL cost limits. Read extensions.cost and pace the calls. It is a budget, not an error.

Download the review worksheet

A blank CSV template and a filled-in example on a fictional store, showing an approved rewrite, a product deliberately left alone, a rejected suggestion, and a row flagged for missing facts. The README explains the columns, the staleness check, and — importantly — why this is a review document and not a Shopify import file.

Every field is quoted, the encoding is UTF-8, and no value starts with =, +, - or @ — a cell beginning with one of those is executed as a formula by Excel and Google Sheets.

If you would rather not open a spreadsheet, the same review runs in a browser: the Shopify SEO Review Worksheet. It shows live length guidance, records a decision and a reason on each row, and exports the identical CSV. It holds nothing but what you type, connects to no store, and needs no account — which is also why it cannot fetch your products for you.

Questions

Do I need the API to bulk edit SEO fields?

No. Shopify's native bulk editor exposes the SEO title and description columns, and for a batch you can read in one sitting it is the faster, safer choice — no app, no token, no script. The API route earns its place when the batch is large, recurring, or needs an approval record.

Will this improve my rankings?

Nobody can promise that, and this guide does not. Accurate, specific metadata helps a searcher decide whether your page answers their question. Whether it ranks depends on a great deal you do not control, and Google may display text other than what you supplied.

Can Claude Code write the changes directly to my store?

It can, if you give it a token and ask it to. The argument of this guide is that it should not do so unreviewed. Keep the drafting step read-only, put a human decision between the draft and the write, and let the script act only on rows a person marked approved.

What about collections, pages and blog posts?

Same two fields, different resources — collectionUpdate, pageUpdate, articleUpdate. The export → review → apply → verify shape is identical. Start with products, get the review habit working, then widen.

How big should a batch be?

Small enough that you will genuinely read every row. In practice that is twenty to fifty. If a batch is too large to review properly, the review becomes a rubber stamp and the workflow has stopped protecting you.

Next action

Export ten products with an empty SEO description. Draft with the prompt above, review the ten rows, and apply the ones you approve through the native bulk editor. You will know by the end of it whether your store needs the API route at all — and if it does, you will have a reviewed batch already behind you.

Related reading: Shopify SEO With Claude Code covers what these fields do and the Shopify-specific traps around them, and Automating Shopify With the Admin API and Claude Code covers the API mechanics — clients, userErrors, rate limits and scripts you can safely run twice.

Read these next

See how this fits into Claude Code Shopify Development

Free download

The CLAUDE.md Starter Kit, free

Four working CLAUDE.md files you can drop into a project today, plus the one-page checklist for what belongs in one and how to tell whether yours is actually working.

  • CLAUDE.md for a static marketing site
  • CLAUDE.md for a web application, with security and migration rules
  • CLAUDE.md for a Shopify theme, including the gotchas that cost hours
  • CLAUDE.md for a shared package in a monorepo
  • A one-page checklist, and how to test the file is actually working
What are you working on?

The download appears here as soon as you submit. I will also email you when there is a new guide worth reading. No fixed schedule, no selling your address, unsubscribe from any email. See the privacy policy.