angeo / module-rich-data
Magento 2 JSON-LD Rich Data for AI Engine Optimization. Injects spec-compliant Product, Organization, BreadcrumbList, FAQPage, and WebSite schema. Fixes the 'Product schema — JSON-LD structured data' signal in angeo/module-aeo-audit.
Angeo Rich Data — Magento 2
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.
What this module fixes
| 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 types injected
| 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.
Installation
composer require angeo/module-rich-data
bin/magento setup:upgrade
bin/magento cache:flush
Upgrading from 1.x? Read UPGRADE.md — the markup shape changes and prices become tax-inclusive on VAT-display stores.
Output
{
"@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.
Configuration
Stores → Configuration → Angeo → Rich Data (JSON-LD)
General
- Enabled
- Output mode —
@graph(default) or legacy one-tag-per-schema
Product 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.
Breadcrumb schema
- Enable — runs on product, category and CMS pages
Merchant policies (return & shipping)
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.
Organization schema
Name, description, logo URL, sameAs URLs, contact telephone and type.
WebSite schema
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.
CollectionPage schema
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.
FAQPage schema
- 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.
FAQ page markup
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>.
Extending with custom schemas
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.
CLI validation
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.
Tests
composer install
vendor/bin/phpunit
The Angeo AI Suite
| 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 |
Security
Found a vulnerability? See SECURITY.md. 2.0.0 fixes a script-breakout issue present in every 1.x release.
License
MIT — see LICENSE
Changelog
All notable changes to angeo/module-rich-data are documented here.
This project adheres to Semantic Versioning.
[2.0.0] - 2026-09-04
Security
- JSON-LD could break out of its own
<script>element. The renderer encoded
withJSON_UNESCAPED_SLASHESand the template printed the result with
@noEscape, so any</script>sequence in a product name, description,
category name or configuration field ended the element early and turned the
rest of the value into markup — a stored XSS vector reachable by anyone who can
edit catalog text. Encoding now happens in one place
(Model\JsonLd\JsonEncoder) with slashes escaped again and
JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOTon top, so<,
>,&,'and"cannot survive as raw characters. Every store running
1.0.x–1.2.0 should upgrade.
Added
- Linked
@graphoutput. All schemas are now written as one
{"@context": ..., "@graph": [...]}document, and every node carries a stable
@id(…#product,…#organization,…#breadcrumb, …). Offers reference the
Organization node as seller, WebSite references it as publisher, CollectionPage
references the WebSite. In 1.x each schema was an isolated block, so nothing
told an AI engine that the three of them described one page. Output modesetting under Angeo → Rich Data → General.graphis the new
default;legacyreproduces the 1.x one-script-per-schema output for anyone who
needs to roll back without downgrading the package.- ProductGroup for configurable products. With
Publish configurable products as ProductGroupenabled, a configurable emits
ProductGroupwithhasVariant,variesBy,productGroupIDand
inProductGroupWithID, and every variant carries its own price, stock and
variant attributes. Supported by Google since February 2024 and used by AI
shopping surfaces to tell one product in six sizes apart from six products.
Capped byMaximum variants per product(default 20, hard limit 50). AggregateOfferfor price ranges. Configurable, grouped and bundle products
now publishlowPrice/highPriceinstead of one arbitrary number.- BreadcrumbList on category and CMS pages, not only product pages, with its
own configuration group. priceValidUntilwindow setting in days, and the option to omit the field.- FAQ page allow-list:
CMS page identifiersunder the FAQPage group. - CLI:
--category-id,--cms-identifierand--jsonoptions on
angeo:rich-data:validate, plus store emulation, duplicate-@iddetection and
ProductGroup checks. i18n/en_US.csv, a GitHub Actions matrix on PHP 8.2–8.5,require-devwith
PHPUnit and PHPStan, and unit tests for the encoder, renderer, product builder,
breadcrumbs, FAQ builder and FAQ matcher.
Fixed
- Published prices no longer ignore tax.
Product::getFinalPrice()returns the
raw catalog price with no tax adjustment, so on a store that displays prices
including VAT the marked-up price was visibly lower than the page. Prices now
come from the pricing framework'sfinal_priceamount, which carries the same
tax adjustment the storefront shows. - Organization and WebSite no longer vanish on incomplete pages. The ViewModel
returned an empty string as soon ascurrent_productorcurrent_categorywas
missing, which suppressed every other schema on that page too. Magento_CatalogInventoryis now declared.StockRegistryInterfacewas used
without the module appearing incomposer.jsonor thesequence.- Out-of-stock products on MSI stores. Salability is now read through
Product::isSalable(), which MSI plugs into; the legacy stock registry remains
a fallback. Multi-source stores previously published sold-out items as
InStock. - N+1 queries removed. Category breadcrumbs loaded one category per repository
call — a dozen loads for a product in eight categories — and the category
ItemList resolved fifty product URLs one query at a time. Both now use a single
collection query. - Category ItemList follows the page. Paging and sort parameters are applied,
so page 3 of a category no longer publishes the products from page 1. - FAQPage is no longer published on unrelated pages. 1.x emitted it on any CMS
page containing an<h2>followed by a<p>, and on the homepage through a
content fallback. A page must now be named in the allow-list, or have an
identifier containing "faq". The homepage is never treated as an FAQ page. - FAQ heading length is measured in characters, not bytes, so short Cyrillic
and Greek headings are filtered the same way as Latin ones. priceValidUntilno longer relies on an accidental date format. 1.x used
date('Y-12-31', …), which produced the right string only because1,2and
3happen not to be format characters.
Changed
SearchActionis off by default. Google removed the sitelinks search box
from search results on 21 November 2024, so the markup no longer produces a
Google feature. It is harmless and the option remains for other consumers, but
the documentation no longer advertises it as a Google feature.- Registry access is confined to
Model\Page\CurrentEntityinstead of being
called from the ViewModel, with a repository fallback when the registry is empty. - Rendering logic was split out of the ViewModel into
Model\Page\*services, so
the ViewModel only assembles context. - Output is compact by default; set
prettyPrintonJsonEncoderviadi.xmlto
get readable JSON while developing.
Migration notes
See UPGRADE.md. No schema or data migration is required:
bin/magento setup:upgrade
bin/magento cache:flush
[1.2.0] - 2026-06-13
Added
- CollectionPage + ItemList on category pages (
CollectionPageBuilder). - Organization
description.
Changed
ViewModel\JsonLdbuilds context forcatalog_category_view.
[1.1.0] - 2026-06-08
Never tagged on Packagist; its changes shipped inside 1.2.0.
Added
- Merchant return policy (
offers.hasMerchantReturnPolicy). - Shipping details (
offers.shippingDetails). - GTIN / MPN identifiers on Product schema. Off by default.
- New admin config group Merchant policies (return & shipping).
Fixed
- BreadcrumbList now actually renders on product pages. The builder expected a
breadcrumbscontext key the ViewModel never supplied. availability_in_stock/availability_out_of_stockexposed in the admin.
[1.0.2] - 2026-04-18
Added
- Initial public release: Product, Organization, WebSite, BreadcrumbList and
FAQPage JSON-LD builders, admin configuration,angeo:rich-data:validate
CLI command, and unit tests.
| Version | Stability | QA Status | Compatibility | Released |
|---|---|---|---|---|
| 2.0.0 | stable | Fail | Magento 2.4.7-2.4.9 Details | 2026-09-06 16:52:35 |
| 1.2.0 | stable | Fail | Magento 2.4.7-2.4.9 Details | 2026-06-14 18:54:30 |
| 1.0.2 | stable | Not tested | Not yet tested Details | 2026-04-24 20:02:53 |
| 1.0.1 | stable | Not tested | Not yet tested Details | 2026-04-18 20:45:32 |
| 1.0.0 | stable | Not tested | Not yet tested Details | 2026-04-18 19:58:31 |
Requires 10
| Package | Constraint |
|---|---|
| magento/framework | >=103.0.0 |
| magento/module-backend | >=102.0.0 |
| magento/module-catalog | >=104.0.0 |
| magento/module-catalog-inventory | >=100.4.0 |
| magento/module-cms | >=104.0.0 |
| magento/module-config | >=101.2.0 |
| magento/module-configurable-product | >=100.4.0 |
| magento/module-review | >=100.4.0 |
| magento/module-store | >=101.0.0 |
| php | ~8.2.0||~8.3.0||~8.4.0||~8.5.0 |
Requires-dev 2
| Package | Constraint |
|---|---|
| phpstan/phpstan | ^1.10 |
| phpunit/phpunit | ^10.5 |
Suggests 1
| Package | Reason |
|---|---|
| angeo/module-aeo-audit | Verify JSON-LD injection with the AEO audit tool. |
Compatibility
Each Magento release line is installed on its supported PHP versions, then the module is built (DI compilation + static-content deploy) and its unit and integration suites are run. The matrix shows the lines and PHP versions the module is confirmed to install and run on. Code-quality results further down (phpstan, phpcs, …) are reported separately and never affect compatibility.
Code Quality
Advisory checks against the module's source. Static analysis runs once across the whole module; PHPStan re-runs per Magento + PHP version because resolvable symbols differ between releases. These NEVER affect the Compatibility badge. A phpcs finding can't make a module incompatible.
Static analysis
Coding standards (phpcs), mess detection (phpmd), copy-pasted code (cpd), PHP cross-version compatibility, composer.json validity. Each runs once for the whole module.
| Tool | Status | Findings | Summary |
|---|---|---|---|
| PHPCS | Warning | 29 | 29 warnings (ruleset: Magento2) |
| PHPMD | Warning | 14 | 14 rule violations (NPathComplexity:5, CyclomaticComplexity:5, MissingImport:2, ExcessiveClassComplexity:1, UnusedFormalParameter:1) |
| Cpd | Pass | 0 | |
| Composer validate | Info | 10 | valid; 10 advisory notes (composer validate --strict) |
Tests
Unit and integration suites, run for each applicable Magento and PHP version. A test failure speaks to the module's behaviour, not its compatibility with a Magento line, so it is reported here separately and never reddens the compatibility matrix.
Unit tests
Integration tests
| Magento | PHP 8.2 | PHP 8.3 | PHP 8.4 | PHP 8.5 |
|---|---|---|---|---|
| 2.4.7 | N/A | N/A | ||
| 2.4.8 | N/A | N/A | ||
| 2.4.9 | N/A | N/A |
Security
Security checks run directly against the module: an audit of its declared dependencies for known vulnerabilities (composer audit) and a scan of its source for malware and web-shell signatures. Each runs once. A malware detection fails the version outright.
More from angeo
View vendorMagento 2 module for AI Engine Optimization (AEO). Generates spec-compliant llms.txt and llms-full.txt per llmstxt.org standard, plus streaming JSONL for vector indexing. Multi-store, multi-website, CLI, cron, async admin UI, Page Builder-aware sanitization, customer-group pricing, atomic writes, ETag/Cache-Control, .md mirrors.
Magento 2 module for AI Engine Optimization (AEO). Injects AI crawler rules (OAI-SearchBot, GPTBot, ChatGPT-User, PerplexityBot, Perplexity-User, Google-Extended, ClaudeBot, anthropic-ai, Claude-User, Applebot, cohere-ai, Amazonbot, Meta-ExternalAgent) into robots.txt — without overwriting your existing configuration. Supports per-bot Allow/Disallow lists, Crawl-delay, Sitemap directives, multi-store, and a public Api\RobotsStatusInterface for cross-module integration with angeo/module-aeo-audit.
Live AI brand visibility audit for Magento 2. Queries ChatGPT, Claude, Perplexity, Gemini and Groq with brand-probing prompts and scores real-world AI recall, citation rate and recommendation presence. Extends angeo/module-aeo-audit v3 via CheckerInterface as the 16th signal, alongside the 15 built-in technical checks.
Magento 2 AEO (AI Engine Optimization) Audit. v3 covers 15 signals — robots.txt AI bots, llms.txt + llms.jsonl, Product / Organization / FAQ schema, merchant return + shipping policies, sitemap.xml, UCP profile, AI product feed, OG tags, canonical + hreflang, JSON-LD quality, well-known endpoint matrix, Core Web Vitals via CrUX. Score Trend dashboard, Admin UI, cron, dynamic fix commands, dependency-injected extension point for custom checkers.
Turn an existing module into recurring revenue.
If you already maintain a Magento 2 module on GitHub or GitLab, listing it on Packagento takes about five minutes. We mirror your tags, handle distribution signing, and route paid licenses through Stripe Connect, so you can keep shipping the way you already do.