Publishes one linked JSON-LD @graph per page so ChatGPT, Gemini, Perplexity and Google read your catalog as connected data instead of loose fragments. Fixes the schema signals in angeo/module-aeo-audit.
| AEO Audit signal | Before | After |
|---|---|---|
| Product schema — JSON-LD structured data | FAIL / WARN | PASS |
| Merchant policies — return & shipping schema | FAIL | PASS (when enabled & configured) |
| Product schema — AggregateRating | WARN | PASS (when reviews exist) |
| JSON-LD quality — BreadcrumbList | WARN | PASS (product, category and CMS pages) |
| FAQPage schema — AI answer eligibility | WARN | PASS (on pages you nominate) |
| Schema | Pages | Key fields |
|---|---|---|
Product |
Product pages | name, description, image, sku, gtin/mpn, brand, offers, aggregateRating |
ProductGroup |
Configurable product pages | hasVariant, variesBy, productGroupID, per-variant offers |
Organization |
All pages | name, description, url, logo, sameAs, contactPoint |
WebSite |
Homepage | name, url, publisher |
BreadcrumbList |
Product, category, CMS pages | full trail |
CollectionPage |
Category pages | ItemList of the products on the current page |
FAQPage |
Nominated CMS pages | detected Q&A pairs |
Every node has a stable @id, so offers.seller points at the same Organization on every page of the store rather than repeating a name string.
composer require angeo/module-rich-data
bin/magento setup:upgrade
bin/magento cache:flushUpgrading from 1.x? Read UPGRADE.md — the markup shape changes and prices become tax-inclusive on VAT-display stores.
{
"@context": "https://schema.org",
"@graph": [
{ "@type": "Organization", "@id": "https://shop.example/#organization", "name": "Example Store" },
{
"@type": "Product",
"@id": "https://shop.example/alpine-jacket.html#product",
"name": "Alpine Jacket",
"offers": {
"@type": "Offer",
"price": "121.00",
"priceCurrency": "EUR",
"availability": "https://schema.org/InStock",
"seller": { "@type": "Organization", "@id": "https://shop.example/#organization" }
}
},
{ "@type": "BreadcrumbList", "@id": "https://shop.example/alpine-jacket.html#breadcrumb" }
]
}Prefer the 1.x layout of one script tag per schema? General → Output mode → One script tag per schema.
Stores → Configuration → Angeo → Rich Data (JSON-LD)
- Enabled
- Output mode —
@graph(default) or legacy one-tag-per-schema
- Enable, AggregateRating, SKU, Brand (attribute-driven)
- GTIN / MPN (attribute-driven, off by default)
- Item condition
- Publish configurable products as ProductGroup, and a variant cap
priceValidUntilwindow in days (0 to omit)- In-stock / out-of-stock availability URLs
Prices follow the store's tax display setting, so the marked-up price matches the price on the page. Configurable, grouped and bundle products publish an AggregateOffer with lowPrice and highPrice.
- Enable — runs on product, category and CMS pages
offers.hasMerchantReturnPolicy and offers.shippingDetails, required by Google and ChatGPT Shopping since January 2026. Off by default — enable only once your real return window, shipping rate and delivery times are known, because publishing wrong policy values can hurt eligibility.
Name, description, logo URL, sameAs URLs, contact telephone and type.
Enable, plus an optional SearchAction. Google removed the sitelinks search box on 21 November 2024, so SearchAction is off by default and produces no Google feature; the option stays for other consumers.
ItemList of the products on the current category page. Paging and sort order are honoured. Layered-navigation filters are not reflected, because the layer is not resolved yet when the page head renders.
- Enable
- CMS page identifiers — comma-separated allow-list. Leave empty to allow identifiers containing "faq". The homepage is never treated as an FAQ page.
Google restricts FAQ rich results to government and health sites, so treat this as machine-readable content for AI answer engines rather than a Google rich result.
Explicit attributes are the reliable option:
<div data-faq-question="What is your return policy?"
data-faq-answer="We offer 30-day returns on all items in original condition.">
</div>Without them the module falls back to a heuristic: <h2>/<h3> followed by <p>.
Implement Angeo\RichData\Api\Data\SchemaInterface and register via di.xml:
<type name="Angeo\RichData\Model\JsonLd\Renderer\SchemaRenderer">
<arguments>
<argument name="builders" xsi:type="array">
<item name="my_schema" xsi:type="object">Vendor\Module\Model\JsonLd\Builder\MySchemaBuilder</item>
</argument>
</arguments>
</type>Set an @id on your root node and it joins the graph like any built-in schema.
bin/magento angeo:rich-data:validate --store=default
bin/magento angeo:rich-data:validate --store=default --product-id=42
bin/magento angeo:rich-data:validate --category-id=11
bin/magento angeo:rich-data:validate --cms-identifier=faq
bin/magento angeo:rich-data:validate --json | jq .Store: default
Page URL: https://shop.example/alpine-jacket.html
Output mode: graph
Found 3 node(s):
Node 1: Organization
Node 2: ProductGroup
PASS 4 variant(s)
Node 3: BreadcrumbList
All nodes are merged into one @graph document.
All JSON-LD nodes look valid.
Exit code is non-zero when a node is missing an @id, an offer is incomplete or two nodes collide — usable in CI.
composer install
vendor/bin/phpunit| Module | Purpose |
|---|---|
angeo/module-aeo-audit |
AEO audit — detects missing schema |
angeo/module-rich-data |
This module — fixes missing schema |
angeo/module-llms-txt |
Generates /llms.txt |
angeo/module-robots-txt-aeo |
AI crawler rules in robots.txt |
angeo/module-openai-product-feed-api |
ACP REST API for ChatGPT Shopping |
Found a vulnerability? See SECURITY.md. 2.0.0 fixes a script-breakout issue present in every 1.x release.
MIT — see LICENSE