Using AI-Generated Schema on Shopify Product Pages
Generating Product markup for a Shopify page takes seconds, and the result usually looks better than what the theme outputs: more detail, a proper brand, materials and identifiers. The trouble starts when you paste it in.
A generated block is a snapshot of one product on one day, while Shopify prices, stock, variants and currencies change constantly, and the theme and review apps are already writing their own Product markup. This guide shows how to use generated markup as a pattern rather than a paste: decide who owns each field, turn the moving values into Liquid, keep the per-product extras in metafields and test it across variants.
What a Shopify product page already outputs
Before adding anything, find out what is there. Open a product page, view the source and search for
application/ld+json. On a typical store you will find markup from up to three places:
- The theme. Recent versions of Dawn and many Online Store 2.0 themes output Product
markup with Liquid's
structured_datafilter, usually in the main product section. Older and third-party themes often have a hand-written block in a snippet. - A review app. Most review apps add ratings, either inside their own Product block or as a separate one.
- An SEO app. Several apps add or replace Product, Organization and breadcrumb markup, and some have their own settings for it.
Run the product URL through the Rich Results Test and open the Product snippets and Merchant listings items. If one Product appears with no errors and the fields you care about, you may not need generated markup on this template at all. If there are two or three Product items, sort that out first with the duplicates guide, because adding a generated block would make a fourth.
Why a pasted block goes stale on Shopify
The generator reads the product page as it is when you run it. Everything it writes is fixed from then on unless you change it. On a Shopify store, many of those values move without anyone touching the markup:
| Property | Changes when | Result if pasted |
|---|---|---|
offers.price | A sale starts, a price is edited, a discount ends | Markup price disagrees with the page and the product feed |
offers.availability | Stock sells out or is restocked | InStock shown for a sold-out item, or the reverse |
offers.priceCurrency | A visitor from another market sees a converted price | GBP markup beside a euro price |
sku, gtin | The shopper picks another variant | Identifiers for the wrong size or colour |
image | Photos are reordered or replaced | Addresses that no longer exist on the CDN |
aggregateRating | A new review arrives | A frozen score and count that drift from the widget |
By contrast, some things the generator does well are stable: the material, the dimensions, care instructions,
the intended audience, a clean description, and the choice between Product and ProductGroup.
Those are the parts worth keeping as written.
Deciding who owns each property
The simplest reliable arrangement is one Product block per page, built by your theme, with generated content used to improve it. Split the properties like this:
| Property | Owner | Liquid or source |
|---|---|---|
name | Shopify | product.title |
url, @id | Shopify | canonical_url |
image | Shopify | product.images with image_url |
price, priceCurrency, availability | Shopify | The
selected variant and cart.currency.iso_code |
sku, gtin | Shopify | variant.sku, variant.barcode |
brand | Shopify or metafield | product.vendor if it holds
the brand, otherwise a metafield |
description | Generated, reviewed | A metafield, or the product description stripped of HTML |
material, size, audience and similar extras | Generated, reviewed | Product metafields |
aggregateRating, review | Review app | The app's own output, and nothing else |
| Shipping and returns | Store-wide | One shared snippet, or Merchant Center account settings |
Everything in the Shopify rows is read at page load, so it can never go stale. Everything in the generated rows is stable information that a person checks once per product.
Turning generated output into Liquid
Take one good generated block and replace each moving value with the Liquid that produces it. The
json filter wraps text in quotes and escapes anything that would break the block, such as
inch marks in a product title. The image_url filter returns an address starting with //,
so https: is added in front.
{%- assign v = product.selected_or_first_available_variant -%}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Product",
"@id": "{{ canonical_url }}#product",
"name": {{ product.title | json }},
"url": "{{ canonical_url }}",
"description": {{ product.metafields.custom.schema_description.value | default: product.description | strip_html | strip_newlines | json }},
"image": [
{%- for img in product.images limit: 3 -%}
"https:{{ img | image_url: width: 1200 }}"{% unless forloop.last %},{% endunless %}
{%- endfor -%}
],
"sku": {{ v.sku | json }},
{%- if v.barcode != blank %}
"gtin": {{ v.barcode | json }},
{%- endif %}
{%- if product.metafields.custom.material != blank %}
"material": {{ product.metafields.custom.material.value | json }},
{%- endif %}
"brand": { "@type": "Brand", "name": {{ product.vendor | json }} },
"offers": {
"@type": "Offer",
"url": "{{ canonical_url }}",
"price": "{{ v.price | divided_by: 100.0 }}",
"priceCurrency": "{{ cart.currency.iso_code }}",
"availability": "https://schema.org/{% if v.available %}InStock{% else %}OutOfStock{% endif %}",
"itemCondition": "https://schema.org/NewCondition"
}
}
</script>
A few details that matter:
- Prices are stored in pence.
v.priceis 3400 for ยฃ34.00, so it is divided by100.0. Avoid themoneyfilters here: depending on your store's currency format they can add a sign, a thousands comma or a decimal comma, all of which make the price invalid. - Currency follows the visitor. With Shopify Markets, a visitor may see prices converted
into another currency.
cart.currency.iso_codegives the code for the currency the page is showing, so price and currency stay in step. - Optional fields are wrapped in conditions so that a product with no barcode or material does not produce an empty value.
- Brand from vendor only works if your vendor field holds the brand. Stores that use vendor for a supplier or wholesaler should use a brand metafield instead.
Per-product extras, and moving them in bulk
The parts of generated markup worth keeping, such as a tidy description, material, dimensions or care
details, belong in product metafields. Create the definitions once in Settings, Custom data, Products,
for example custom.material as single-line text and custom.schema_description
as multi-line text. They then appear as fields on every product in the admin, and the Liquid above reads
them.
Working through a catalogue
- Generate markup for a sample of products from each product type and review it against the product pages.
- Copy the stable values you want to keep into a spreadsheet, one row per product handle, one column per metafield.
- Check each row against the product. Delete anything that is not stated on the page or in your supplier data.
- Load the columns into the metafields. Shopify's bulk editor can show metafield columns for editing, and the product CSV import in current versions can include metafield columns; for large catalogues a bulk import app is another option.
- Spot-check a few products in the Rich Results Test after the import.
Review apps and ratings
Generated markup is at its most dangerous around ratings. The model may read a star widget, a "4.8
from 212 reviews" line or a single testimonial and write an aggregateRating. On Shopify,
review widgets usually load with JavaScript after the page, so the generator may have seen stale numbers,
placeholder text or nothing at all.
- Delete any rating in the generated block. Ratings must come from a live source that matches what shoppers see.
- Let the review app own ratings. Most review apps output rating markup, and several
can also write Shopify's standard product review metafields,
reviews.ratingandreviews.rating_count. Use one route, not both. - Check the result after installing or changing a review app. Some apps add a whole second Product block rather than a rating inside yours. If that happens, look in the app's settings for an option to switch its markup off or change its output.
The fix, step by step
Step 1
See what the theme and apps already output
Open a product page, view the source and search for application/ld+json. Note
every block, which file or app produced it and which types it contains. Run the same URL through the
Rich Results Test and open the Product snippets and Merchant listings items. If there is already more
than one Product, choose which one will survive before going further, because a generated block added
on top would only make the conflict worse.
Step 2
Generate from one representative product
Pick a product with several variants, a full description, a barcode and some reviews,
so the generator sees the widest set of fields. Use its live, public address on your primary domain,
not the myshopify.com address or a theme preview link. Review the output against the page
and delete any value you cannot see there, especially ratings, prices and identifiers that the model
may have guessed or copied from the wrong variant.
Step 3
Decide which source owns each property
Go through the reviewed block line by line and label each property: Shopify data, a metafield, the review app or store-wide settings. Prices, stock, currency, images, identifiers and addresses should always be Shopify data read at page load. Ratings belong to the review app. The stable descriptive extras are the generator's real contribution. Anything with no owner and no true source gets removed rather than kept as fixed text.
Step 4
Turn the moving values into Liquid
Rewrite
the block as a Liquid snippet, replacing each Shopify-owned value with the object that produces it. Output
text with the json filter so quotes and inch marks cannot break the block, divide variant.price
by 100.0 rather than using money filters, take the currency from cart.currency.iso_code,
and prefix image_url output with https:. Wrap optional fields in conditions
so products without them stay valid.
Step 5
Move per-product extras into metafields
Create product metafield definitions for each stable extra you decided to keep, such as material or a plain-text schema description. Fill them for your sample products by hand, then for the rest of the catalogue in bulk through the bulk editor, a CSV import or an import app, after exporting a backup. Read each metafield in the snippet, wrapped in a condition, so that products with an empty field simply leave the property out of the block.
Step 6
Add the snippet to the product template
Duplicate the live theme first so you have a copy to return to. In the code editor, save
the Liquid as a snippet such as snippets/product-schema.liquid and render it from the main
product section with the product passed in. Then remove or disable the theme's existing Product output,
usually a structured_data line or an older snippet, so that the page carries exactly one
Product block.
Step 7
Test several products, variants and markets
Preview the duplicated theme and test a simple product, a product with variants, a sold-out product and one with no barcode in the Rich Results Test. Select a different variant and confirm the price, SKU and availability change with it. If you sell in more than one market, check a page in another currency. Publish the theme, request indexing for a few products and watch both Search Console product reports.
Variants, markets and theme updates
Products with variants
The snippet above describes the selected variant, which is what the page shows when opened with a
?variant= parameter and the first available variant otherwise. That is a sound starting
point. If you want every variant described, the pattern is a ProductGroup with one Product
per variant in hasVariant, which means looping over product.variants in the
snippet. Generated markup for a variant product is useful here as a model of the structure, but it will
have listed only the variants the model saw, with the prices it saw.
Several markets and domains
If each market has its own domain or subfolder, canonical_url follows the page being
viewed, so the markup's addresses stay correct. Keep the currency code dynamic; a generated block with
a fixed GBP will be wrong on every non-UK page.
Theme updates
Updating a theme from the theme store creates a new version, and edits you made directly in the theme code may not come across. After any theme update, check that your snippet is still rendered and that the theme's own Product output has not been switched back on. Metafield values are stored against products, not the theme, so they survive.
1. Can I just paste generated schema into a Shopify product description?
You can, but it is the least reliable place for it. The rich text editor can strip or alter script tags, the markup cannot change when the price or stock does, and it is duplicated separately on every product. It also sits alongside whatever Product markup your theme already outputs, giving Google two conflicting descriptions. Using the generated block as a pattern for a Liquid snippet, with stable extras in metafields, avoids all of those problems.
2. Does Shopify already add Product schema?
Most current themes do. Dawn and many other Online Store 2.0 themes use the structured_data Liquid filter to output Product markup on product pages, and older themes often include a hand-written block in a snippet. What they output varies by theme and version, so view the source of a product page and run it through the Rich Results Test to see exactly which fields are already there before deciding what generated markup should add.
3. Why is my Shopify schema price wrong?
The usual causes are a pasted block that has not been updated since a price change, a money filter that added a currency sign or a thousands comma, or a fixed currency code shown next to a converted price for visitors from another market. Read the price from the selected variant and divide it by 100.0, take the currency code from the cart, and remove any fixed prices from generated markup stored in the page or a metafield.
4. Should my review app or my own snippet output the rating?
One or the other, never both. The review app knows the live score and count, so it is the natural owner. Most review apps output rating markup themselves, and several can write Shopify's standard review metafields that a theme snippet can read instead. Whichever route you choose, remove any rating from generated markup, because a figure the model read from the page on one day will drift away from the real score as new reviews arrive.
5. Will generated schema help my products appear in Google Shopping?
Structured data on product pages can make them eligible for merchant listings, which include shopping-style results in Search, without a Merchant Center account. It does not guarantee they appear, and the Shopping tab also draws on product feeds. If you use Shopify's Google channel to send a feed, the price and availability in your markup must match the feed and the page, so keep both driven by live Shopify data.
6. How do I handle schema for products with many variants?
For most stores, describing the variant currently selected on the page is enough, and it keeps price, SKU and availability accurate as shoppers change options. If you want Google to understand the full range, use a ProductGroup with one Product per variant, generated by looping over the product's variants in Liquid. Treat any generated variant markup as a structural example only, since it will list just the variants and prices the model happened to see.
7. Do theme updates remove custom schema snippets?
They can. When you update a theme from the theme store, Shopify adds the new version to your theme library, and changes made directly in theme code may need to be applied again. Keep your snippet in its own file so it is easy to copy across, note the line you added to render it, and check that the theme's built-in Product output is still switched off. Metafield data lives with the products, so it is not affected.
The AI Schema Generator reads your page content with an AI model, writes JSON-LD tailored to the content type and validates it against schema.org and Google rich result criteria before delivery, ready for you to review and adapt to your store.
Open AI Schema Generator Fixes โ