# Magento 2 Product JSON-LD Schema for AI Search  -  2026 Guide

> Default Magento 2 product schema fails AI search. Fix offers.availability, the Hyvä gap and the GTM pitfall  -  with copy-paste JSON-LD and a checklist.

Source: https://angeo.dev/magento-2-product-schema-json-ld-ai-search/
Updated: 2026-09-26

TL;DR  -  2 minute version

- Default Magento 2 outputs microdata  -  not JSON-LD. AI engines prefer JSON-LD.

- The single most common failure: `offers.availability` is missing  -  ChatGPT Shopping skips your product entirely.

- AI crawlers do not execute JavaScript  -  schema via GTM is invisible to OAI-SearchBot and PerplexityBot.

- Hyvä Theme emits Product microdata (name, image, price, availability) but no JSON-LD and no sku, brand, GTIN, shipping or return fields.

- This guide gives you the copy-paste fix, Magento-specific pitfalls, Hyvä solution, and a validation checklist.

[image: How to implement Product JSON-LD schema in Magento 2 for AI search engines, ChatGPT visibility and structured data optimization (2026 guide)]

Your Magento store might rank on page one in Google  -  and still be invisible to ChatGPT, Gemini, and Perplexity. In most cases, the reason is not robots.txt or llms.txt. It is Product schema.

AI search engines need structured product data to answer purchase queries. When they cannot parse your product data from JSON-LD, they skip your store and recommend a competitor whose schema is correct. This guide covers exactly what is missing, why, and how to fix it.

## Before vs After: What Schema Visibility Looks Like

| Setup | ChatGPT Shopping | Gemini product results | Perplexity citations |

| No schema | [no] Invisible | [no] Invisible | [no] Invisible |

| Microdata only (Luma default) | [!]️ Partially parsed | [!]️ Partially parsed | [!]️ Inconsistent |

| JSON-LD, no `offers.availability` | [no] Feed validation fails | [!]️ May appear | [!]️ May appear |

| Full JSON-LD + availability + rating | [yes] Eligible for Shopping | [yes] Product results | [yes] Cited with price |

## Why Magento 2 Fails AI Schema by Default

Default Magento 2 (Luma theme) outputs microdata  -  the older `itemscope`/`itemprop` HTML attribute format:

```
<!-- Default Magento Luma  -  NOT sufficient for AI search -->
<div itemscope itemtype="http://schema.org/Product">
  <span itemprop="name">Product Name</span>
  <div itemprop="offers" itemscope itemtype="http://schema.org/Offer">
    <span itemprop="price">49.99</span>
    <!-- offers.availability missing  -  AI cannot confirm purchasability -->
  </div>
</div>
```

Three problems with this for AI search:

**1. It is microdata, not JSON-LD.** Google recommends JSON-LD, and it is the format AI systems parse most reliably, including OAI-SearchBot (ChatGPT) and Googlebot, which feeds AI Overviews and Gemini. Microdata is embedded in HTML that themes and extensions modify  -  it breaks easily and silently.

**2. `offers.availability` is missing.** This is the single most common failure. Without explicit availability set to a schema.org URI, ChatGPT Shopping feed validation fails automatically.

**3. AI crawlers do not execute JavaScript.** Schema injected via Google Tag Manager or any JavaScript that runs after initial page load is completely invisible to AI crawlers. Schema must be server-side rendered in the HTML source.

> **Critical:** If you use GTM to inject schema, AI engines cannot see it. This is confirmed by how OAI-SearchBot and PerplexityBot work  -  they parse the raw HTML response, not the rendered DOM. This is the most overlooked Magento AEO mistake.

## The Complete Copy-Paste Product JSON-LD

This is the spec-compliant JSON-LD that passes all AEO audit checks. Copy and adapt with your product data:

```
<script type="application/ld+json">
{
  "@context": "https://schema.org/",
  "@type": "Product",
  "name": "Alpine Hiking Jacket",
  "description": "Waterproof 3-layer shell for alpine conditions. 300g weight, packed size 18x10cm.",
  "sku": "WB-004",
  "image": [
    "https://yourstore.com/media/catalog/product/jacket-front.jpg",
    "https://yourstore.com/media/catalog/product/jacket-back.jpg"
  ],
  "brand": {
    "@type": "Brand",
    "name": "TrailMaster"
  },
  "offers": {
    "@type": "Offer",
    "priceCurrency": "EUR",
    "price": "189.99",
    "priceValidUntil": "2026-12-31",
    "availability": "https://schema.org/InStock",
    "itemCondition": "https://schema.org/NewCondition",
    "url": "https://yourstore.com/alpine-hiking-jacket.html",
    "seller": {
      "@type": "Organization",
      "name": "Your Store Name"
    }
  },
  "aggregateRating": {
    "@type": "AggregateRating",
    "ratingValue": "4.6",
    "reviewCount": "38",
    "bestRating": "5",
    "worstRating": "1"
  }
}
</script>
```

## Required Fields  -  What AI Engines Actually Need

| Field | Required for AI? | Common mistake |

| `@type: Product` | [yes] Required | Using `IndividualProduct`  -  both work, `Product` is preferred |

| `name` | [yes] Required | Including HTML tags in the name string |

| `offers.availability` | [yes] Required | Text string instead of URI (see table below) |

| `offers.price` | [yes] Required | Including currency symbol: `"€189"` instead of `"189.99"` |

| `offers.priceCurrency` | [yes] Required | Using symbol `€` instead of ISO code `EUR` |

| `image` | [!]️ Strongly recommended | Relative URL instead of absolute |

| `sku` | [!]️ Strongly recommended | Often missing entirely |

| `aggregateRating` | [!]️ Strongly recommended | Including when `reviewCount` is 0  -  this causes validation error |

| `brand` | [!]️ Recommended | Plain string instead of `{"@type": "Brand", "name": "..."}` |

| `description` | [!]️ Recommended | Duplicate of page title instead of unique description |

### offers.availability  -  Correct vs Wrong Format

| Stock status | [yes] Correct value | [no] Wrong (silently rejected) |

| In stock | `https://schema.org/InStock` | `"In Stock"` / `"instock"` / `"available"` |

| Out of stock | `https://schema.org/OutOfStock` | `"Out of Stock"` / `"unavailable"` |

| Pre-order | `https://schema.org/PreOrder` | `"preorder"` / `"coming soon"` |

| Back order | `https://schema.org/BackOrder` | `"backorder"` |

AI parsers require the full schema.org URI. Text strings are parsed as unknown values and treated as missing. No error is shown  -  your product just doesn't appear.

## Configurable Products  -  Correct Structure

Default Magento flattens configurable product schema. If 6 of 8 size variants are in stock and 2 are not, AI engines may see "availability unclear" and suppress the listing. Correct structure uses `AggregateOffer` with per-variant `Offer` objects:

```
{
  "@type": "Product",
  "name": "Running Shoe",
  "offers": {
    "@type": "AggregateOffer",
    "offerCount": 8,
    "lowPrice": "89.99",
    "highPrice": "89.99",
    "priceCurrency": "EUR",
    "availability": "https://schema.org/InStock",
    "offers": [
      {
        "@type": "Offer",
        "sku": "RS-42-BLK",
        "name": "Size 42 / Black",
        "availability": "https://schema.org/InStock",
        "price": "89.99",
        "priceCurrency": "EUR"
      },
      {
        "@type": "Offer",
        "sku": "RS-43-BLK",
        "name": "Size 43 / Black",
        "availability": "https://schema.org/OutOfStock",
        "price": "89.99",
        "priceCurrency": "EUR"
      }
    ]
  }
}
```

## Hyvä Theme  -  What the Microdata Covers and What It Misses

If your store runs on Hyvä Theme, the product page `<body>` is a `schema.org/Product` microdata item: the title carries `name`, the gallery `image`, and the price block an `Offer` with price, currency and availability. What Hyvä does not emit is JSON-LD, and the fields shopping surfaces use to match and trust a product: `sku`, `brand`, GTIN, `shippingDetails` and `hasMerchantReturnPolicy`. Details: [what Hyvä emits as product schema](https://angeo.dev/hyva-theme-product-schema-gap/).

Verify:

```
curl -s https://yourstore.com/sample-product.html | grep -c 'application/ld+json'
# Returns 0 -> no JSON-LD at all

curl -s https://yourstore.com/sample-product.html | grep -c 'itemscope'
# Returns 1+ on Hyvä: Product microdata on , Offer inside it
```

The fix requires injecting JSON-LD via layout XML. Create `Vendor_Theme/layout/catalog_product_view.xml`:

```
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <head>
        <block class="Magento\Framework\View\Element\Template"
               name="product.json.ld"
               template="Vendor_Module::product/json-ld.phtml"/>
    </head>
</page>
```

> **Note:** JSON-LD injected via `<head>` in layout XML is server-side rendered and visible to AI crawlers. This is exactly where it needs to be.

Or use [`angeo/module-rich-data`](https://packagist.org/packages/angeo/module-rich-data) which handles both Luma and Hyvä automatically without any template editing.

## How to Diagnose Your Current Schema

Run this one-liner to see exactly what schema your product pages output:

```
# Replace with a real product URL from your store
curl -s https://yourstore.com/sample-product.html | \
  python3 -c "
import sys, json, re
body = sys.stdin.read()
blocks = re.findall(r'<script[^>]+type=\"application/ld\+json\"[^>]*>(.*?)</script>', body, re.DOTALL)
for b in blocks:
    try:
        d = json.loads(b)
        items = d if isinstance(d, list) else [d]
        for item in items:
            nodes = item.get('@graph', [item])
            for n in nodes:
                if n.get('@type') in ('Product', 'IndividualProduct'):
                    print('FOUND Product schema')
                    offers = n.get('offers', {})
                    if isinstance(offers, list): offers = offers[0] if offers else {}
                    print('availability:', offers.get('availability', 'MISSING'))
                    print('price:', offers.get('price', 'MISSING'))
                    print('priceCurrency:', offers.get('priceCurrency', 'MISSING'))
                    print('aggregateRating:', 'present' if n.get('aggregateRating') else 'MISSING')
    except: pass
  "
```

Four possible outputs:

| Output | Meaning | Action |

| No output | No Product schema at all | Install `angeo/module-rich-data` |

| `availability: MISSING` | Schema exists, offers missing | Fix offers block |

| `availability: InStock` | Wrong format (text string) | Change to full URI |

| `availability: https://schema.org/InStock` | Correct | Re-run CLI audit to confirm |

Or use the CLI audit module for the full 20-signal AEO check:

```
composer require angeo/module-aeo-audit
bin/magento setup:upgrade && bin/magento cache:flush
bin/magento angeo:aeo:audit
```

## Implementation Options

### Option 1  -  Module (recommended, 5 minutes)

```
composer require angeo/module-rich-data
bin/magento setup:upgrade && bin/magento cache:flush
```

The [`angeo/module-rich-data`](https://packagist.org/packages/angeo/module-rich-data) module injects server-side JSON-LD for all product pages automatically. Works on Luma and Hyvä. Includes Product, Organization, BreadcrumbList, WebSite, and FAQPage schema. MIT licensed, free.

### Option 2  -  Manual phtml template

Create `app/design/frontend/Vendor/Theme/Magento_Catalog/templates/product/json-ld.phtml`:

```
<?php
$product = $block->getProduct();
$schema = [
    '@context' => 'https://schema.org/',
    '@type'    => 'Product',
    'name'     => $product->getName(),
    'sku'      => $product->getSku(),
    'offers'   => [
        '@type'         => 'Offer',
        'price'         => (string) $product->getFinalPrice(),
        'priceCurrency' => $block->getCurrencyCode(),
        'availability'  => $product->isAvailable()
            ? 'https://schema.org/InStock'
            : 'https://schema.org/OutOfStock',
        'itemCondition' => 'https://schema.org/NewCondition',
    ],
];
?>
<script type="application/ld+json">
<?= json_encode($schema, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?>
</script>
```

### Option 3  -  GTM

Not recommended for AEO. GTM injects schema after page load via JavaScript. Crawlers that run little or no JavaScript, such as OAI-SearchBot and PerplexityBot, are likely to miss it. Googlebot renders JavaScript, so results differ between systems. If you use GTM for schema today, move it into the server-rendered page.

## Validation Checklist

- ☐ JSON-LD format (not microdata or GTM-injected)

- ☐ `offers.availability` set to full schema.org URI

- ☐ `offers.price` is a number string without currency symbol

- ☐ `offers.priceCurrency` is ISO 4217 code (EUR, USD, GBP)

- ☐ `image` contains at least one absolute URL

- ☐ `aggregateRating` omitted if `reviewCount` is 0

- ☐ Schema server-side rendered (visible in `curl` output)

- ☐ Hyvä: schema injected via layout XML `<head>`

- ☐ Configurable products use `AggregateOffer` with per-variant `Offer`

- ☐ Validated at [validator.schema.org](https://validator.schema.org)  -  zero errors

## FAQ

**Does Magento 2 support Product JSON-LD out of the box?**
No. Default Luma outputs microdata (the older `itemscope`/`itemprop` format), not JSON-LD. Additionally, the default microdata is missing `offers.availability`. Hyvä Theme emits Product microdata but no JSON-LD and no brand, GTIN, shipping or return fields.

**Is offers.availability required for ChatGPT Shopping?**
Yes. ChatGPT Shopping feed validation fails automatically when `offers.availability` is missing or set to a text string. The value must be a full schema.org URI: `https://schema.org/InStock`.

**Does ChatGPT actually read JSON-LD from my Magento store?**
Yes, via OAI-SearchBot. It parses the raw HTML response of your product pages. However, it does not execute JavaScript  -  schema injected via GTM or any JS that runs after page load is invisible. Schema must be server-side rendered.

**Does GTM-injected schema work for AI search?**
Not reliably. OAI-SearchBot and PerplexityBot run little or no JavaScript, so they are likely to miss schema added by GTM. Googlebot renders JavaScript, so Google may see it while others do not. This is one of the most common AEO mistakes  -  a store passes Google Rich Results Test but has zero AI-visible schema because it all goes through GTM.

**Why does Hyvä Theme break Product schema?**
Hyvä removes Luma's product templates entirely to rebuild them in Alpine.js. The microdata that Luma injects via phtml templates is gone. You need to add JSON-LD separately via layout XML or a module like `angeo/module-rich-data`.

**Does Google Rich Results Test passing mean my schema is correct for AI?**
Not fully. Google Rich Results validates schema for Google Search features like rich snippets. AI search engines (ChatGPT Shopping specifically) require additional fields  -  particularly `offers.availability` as a full URI  -  that Google does not enforce but OpenAI does. Always verify with the CLI audit tool, not just Google's test.

## What's Next

Product schema is one of the 20 signals the audit checks. After fixing schema, the next highest-impact signals are:

- [robots.txt AI bot access](https://angeo.dev/magento-2-robots-txt-chatgpt-gemini-ai-bots/)  -  weight 20%, blocked by default on most Magento stores

- [llms.txt generation](https://angeo.dev/how-to-generate-llms-txt-for-magento-2-in-5-minutes/)  -  weight 18%, Perplexity's documented crawl signal

- [ChatGPT Shopping registration](https://angeo.dev/magento-2-chatgpt-shopping-registration/)  -  ACP product feed required for Shopping results

Run the full audit to see your current score across all signals:

```
bin/magento angeo:aeo:audit
```

Or check any store URL without installing: [angeo.dev/ai-magento-audit](https://angeo.dev/ai-magento-audit/)

## More on this topic

- [offers.availability - Definition and Correct Values](https://angeo.dev/offers-availability-definition/)
- [FAQPage Schema After May 2026](https://angeo.dev/faqpage-schema-after-2026/)
- [Entity @id in JSON-LD - Definition](https://angeo.dev/entity-id-definition/)
- [The Magento product description that AI can't read](https://angeo.dev/magento-product-description-invisible-ai-chatgpt/)
- [Magento 2 JSON-LD Schema Module  -  Free](https://angeo.dev/modules/rich-data/)
