Back to Blog

Turn your product schema
into GA4 ecommerce data

Use your Schema.org Product markup to send brand, category and price data to GA4 with Google Tag Manager. Step-by-step scripts, testing and limits included.

Turn your product schema | into GA4 ecommerce data

Open GA4 on most ecommerce sites and you'll find reports full of URLs like /products/widget-123. They tell you which pages got traffic, but not which brands, categories or price points drive revenue.

If your product pages already carry Schema.org markup for SEO, that information is sitting in your HTML. A short Google Tag Manager (GTM) script can read it and send it to GA4 as standard ecommerce data. You can then report on product views, add-to-carts and purchases by brand and category, with no changes to your site's code.

Here's how the pieces fit together:

  1. Your product page carries Product markup in JSON-LD.
  2. A GTM Custom HTML tag reads that markup and pushes a view_item event to the dataLayer.
  3. A GA4 Event tag picks up the event and sends the product data to GA4.
  4. GA4 fills its built-in ecommerce reports, broken down by item name, brand and category.

This guide walks through each step, how to test it, and where the approach runs out.

What you'll need

  • GTM on your site, with your Google tag (GA4 configuration) already firing on every page.
  • Access: publish rights in GTM and Editor access to your GA4 property.
  • Product markup in JSON-LD on your product pages. If you use Shopify, WooCommerce with an SEO plugin, or most modern ecommerce themes, you probably have this already.
  • A little JavaScript confidence, so you can adjust the scripts to your site's markup and page structure.

Step 1: Check your Product markup

Start by confirming what your markup actually contains. Run a product URL through Google's Rich Results Test or the Schema Markup Validator and look for a detected Product item. A typical block looks like this:

json
{ "@context": "https://schema.org", "@type": "Product", "name": "Example Widget", "image": "https://www.example.com/images/widget.jpg", "brand": { "@type": "Brand", "name": "Acme Corp" }, "category": "Gadgets > Widgets", "sku": "WIDGET-123", "aggregateRating": { "@type": "AggregateRating", "ratingValue": "4.6", "reviewCount": "89" }, "offers": { "@type": "Offer", "priceCurrency": "GBP", "price": "49.95", "availability": "https://schema.org/InStock" } }

Google uses this markup to show price, availability and ratings in search results (Google's product structured data guide). The same fields (name, brand, category, SKU and price) are exactly what GA4's ecommerce reports need.

While you're in the validator, note how your markup is structured. Real sites rarely match the tidy example above:

  • SEO plugins often nest the Product inside an @graph array alongside Organization and WebSite items.
  • brand is sometimes a plain string rather than an object.
  • offers can be a list of offers, or an AggregateOffer with lowPrice and highPrice instead of price.

The script in Step 2 handles all three.

Step 2: Push the product data to the dataLayer

In GTM, create a new Custom HTML tag and paste in the script below. It searches every JSON-LD block on the page for a Product, pulls out the fields GA4 needs, and pushes a view_item event to the dataLayer.

html
<script> (function () { // Find the first Product, including inside lists, @graph and mainEntity function findProduct(node) { if (!node || typeof node !== 'object') return null; if (Array.isArray(node)) { for (var i = 0; i < node.length; i++) { var found = findProduct(node[i]); if (found) return found; } return null; } if ([].concat(node['@type']).indexOf('Product') > -1) return node; return findProduct(node['@graph'] || node.mainEntity); } var product = null; var blocks = document.querySelectorAll('script[type="application/ld+json"]'); for (var i = 0; i < blocks.length && !product; i++) { try { product = findProduct(JSON.parse(blocks[i].textContent)); } catch (e) { /* skip malformed blocks */ } } if (!product) return; var offer = [].concat(product.offers || [])[0] || {}; var brand = typeof product.brand === 'string' ? product.brand : (product.brand || {}).name; var category = (product.category && product.category.name) || product.category || ''; var levels = String(category).split(/\s*>\s*/); var price = parseFloat(offer.price || offer.lowPrice) || undefined; var item = { item_id: product.sku || product.productID || product.mpn, item_name: product.name, item_brand: brand, item_category: levels[0] || undefined, item_category2: levels[1], item_category3: levels[2], price: price }; window.dataLayer = window.dataLayer || []; window.dataLayer.push({ ecommerce: null }); // clear the previous ecommerce object window.dataLayer.push({ event: 'view_item', ecommerce: { currency: offer.priceCurrency, value: price, items: [item] } }); // Save the item for add-to-cart tracking (see Step 4) try { sessionStorage.setItem('schemaLastItem', JSON.stringify({ currency: offer.priceCurrency, item: item })); } catch (e) {} })(); </script>

A few choices in this script are worth knowing about:

  • It uses view_item, GA4's recommended event. Data sent in this format lands in GA4's ecommerce reports and built-in item dimensions (Item name, Item brand, Item category) without registering any custom dimensions. A custom event with its own parameters would show nothing in reports until you registered each one, and registration isn't retroactive.
  • It clears ecommerce before each push. Google recommends this so values from an earlier push don't merge into the new one.
  • It converts the price to a number. Schema.org allows prices as text, but GA4 needs numbers for price and value.
  • It splits breadcrumb-style categories. A category like "Gadgets > Widgets" becomes item_category and item_category2, so you can report at either level.

Step 3: Set up the triggers and GA4 tag

You need two triggers and one GA4 tag. Set them up in this order:

  1. Fire the script on product pages once the markup is ready. Give the Custom HTML tag from Step 2 a DOM Ready trigger, limited to product pages (for example, Page Path matches RegEx ^/products/). DOM Ready makes sure the JSON-LD has been parsed before the script looks for it.
  2. Create a Custom Event trigger with the event name view_item.
  3. Create a GA4 Event tag. Enter your Measurement ID and set the event name to view_item. Under More Settings > Ecommerce, tick Send Ecommerce data and set the data source to Data Layer. Attach the view_item trigger.

There's no need to map each field to a parameter by hand. With ecommerce data switched on, GTM passes the whole ecommerce object straight to GA4.

Tip: once you add more events in Step 4, you can use a single GA4 Event tag for all of them. Set its event name to the built-in {{Event}} variable and fire it on a Custom Event trigger matching the RegEx ^(view_item|add_to_cart)$.

If the script finds nothing: some platforms and apps inject their JSON-LD with JavaScript after the page loads. Try a Window Loaded trigger instead. On single-page apps, where the URL changes without a full page load, you'll also need a History Change trigger.

Step 4: Carry the data through to add-to-cart and purchase

Product views alone can't tell you which categories convert. For that, your add_to_cart and purchase events need the same item details. Many of those actions happen on pages without Product markup, such as listing pages, mini-carts and the order confirmation page.

For add-to-cart on product pages, reuse the item that Step 2 saved. Create another Custom HTML tag with the script below and fire it with a Click trigger on your add-to-cart button. Use GTM's Preview mode to find a selector that matches the button, for example Click Element matches CSS selector form[action*="/cart/add"] button.

html
<script> (function () { var saved; try { saved = JSON.parse(sessionStorage.getItem('schemaLastItem')); } catch (e) {} if (!saved || !saved.item) return; var item = saved.item; item.quantity = 1; // swap in your page's quantity field if it has one window.dataLayer = window.dataLayer || []; window.dataLayer.push({ ecommerce: null }); window.dataLayer.push({ event: 'add_to_cart', ecommerce: { currency: saved.currency, value: (item.price || 0) * item.quantity, items: [item] } }); })(); </script>

This fires on the click, so it counts attempts to add, not confirmed adds. If your cart sometimes rejects items (out of stock, say), the numbers will run slightly high.

For purchases, use your platform's data. A purchase event needs a transaction ID, order revenue, tax, shipping and every item in the basket. That information lives in your ecommerce platform, not in Product markup. Shopify, WooCommerce, Magento and most other platforms can output a GA4-ready purchase dataLayer, either natively or through a plugin. Make sure its item fields (item_id, item_brand, item_category) use the same values as your markup, so views, carts and purchases line up in reports.

Step 5: Test each stage before publishing

Check each link in the chain separately, so that if something breaks you know exactly where:

  1. The markup. The Rich Results Test or Schema Markup Validator detects a Product with a name, brand and price.
  2. The dataLayer push. In GTM Preview mode, load a product page. view_item should appear in the event list, and the Data Layer tab should show the ecommerce object with a numeric price (no quotation marks around it).
  3. The tag. Still in Preview, select the view_item event and confirm your GA4 Event tag fired.
  4. GA4. Open Admin > DebugView in GA4. The view_item event should arrive with an items array containing your product details.
  5. Add-to-cart. Click the add-to-cart button in Preview mode and repeat steps 2 to 4 for add_to_cart.

Once everything checks out, publish the container. Standard GA4 reports can take 24 to 48 hours to show new data, so don't panic if they're empty on day one.

What you can report on now

With item data flowing, GA4's ecommerce reports and Explorations can break your products down by brand and category instead of URL. Some questions you can now answer:

| Question | Where to look in GA4 | Dimension | Metrics | | --- | --- | --- | --- | | Which categories get looked at most? | Reports > Monetization > Ecommerce purchases | Item category | Items viewed | | Which categories turn views into carts? | Free-form exploration | Item category | Items viewed, Items added to cart | | Which products get viewed but rarely added? | Reports > Monetization > Ecommerce purchases | Item name | Items viewed, Items added to cart | | Which brands earn the most? | Reports > Monetization > Ecommerce purchases | Item brand | Item revenue, Items purchased |

The last row depends on purchase data from your platform (Step 4). For brand performance, use item revenue rather than average order value: one order often contains products from several brands, so AOV by brand is misleading.

When this approach makes sense

This is a pragmatic quick win, not a replacement for a full ecommerce dataLayer built by your developers. It's a good fit when developer time is scarce, when your platform doesn't output ecommerce data, or when you want product-level insight this week rather than next quarter.

Know its limits before you rely on it:

  • It's only as accurate as your markup. SEO plugins sometimes serve cached markup with out-of-date prices, and GA4 will record whatever the markup says.
  • Variants are a blind spot. Markup usually describes the default variant and doesn't update when a shopper picks a different size or colour. If price or SKU changes by variant, your data will reflect the default.
  • Transactions need platform data. As covered in Step 4, purchase events should come from your ecommerce platform.

There's a useful side effect, though. If GA4 starts showing missing brands or empty prices, your Product markup is probably broken too, and that also costs you rich results in search. Analytics becomes an early warning system for your structured data.

Wrapping up

Your Product markup already describes every item you sell. With two Custom HTML tags and one GA4 Event tag, you can put that description to work in GA4, and see performance by brand and category instead of by URL.

Start small: get view_item working on a handful of product pages, check it in DebugView, then add add-to-cart tracking. Once you trust the numbers, connect your platform's purchase data to complete the picture.

If you try this on your own site, tell me in the comments which platform you're on and what edge cases you hit.

Further reading