Adding Product JSON-LD on OroCommerce — and keeping it in sync
How to add schema.org Product JSON-LD to OroCommerce PDPs via a layout update, map GTIN/SKU/brand/offers correctly, and keep the markup in sync with the page.

Once product data is enriched — real names, brand, identifiers, specs, availability — the remaining job is getting that data onto the rendered page in a form buyers and AI/search crawlers can both parse. OroCommerce ships its own Schema.org markup out of the box, but it's HTML Microdata, not JSON-LD, and it's driven by a fixed set of SEO settings rather than the full Product vocabulary distributors usually need (GTIN, MPN, aggregateRating). This guide covers how to add a proper JSON-LD Product block to the OroCommerce storefront PDP, which fields actually matter, and how to keep the markup from drifting away from what's rendered on the page.
What OroCommerce already renders
OroSEOBundle extends the Product entity with localized SEO Title, SEO Description, and SEO Keywords fields (stored as LocalizedFallbackValue collections), and it emits Schema.org Microdata — itemprop/itemscope attributes — directly in the storefront HTML. Two related settings live under System → Configuration → Commerce → Guests → SEO (also configurable per organization or per website):
- Disable Product Microdata Without Price — turns off the microdata block for products with no assigned price, since crawlers can flag priceless product markup as invalid.
- Used Product Description Field — controls whether the Microdata
descriptionpulls from the long description, the SEO Meta Description, or the short description.
This native markup is useful but limited: no gtin, mpn, aggregateRating, or explicit availability/priceCurrency out of the box, and Google's own guidance is that JSON-LD is the preferred format for structured data even though Microdata is still parsed. Adding a JSON-LD block alongside (or in place of) the Microdata is the standard move — just make sure the two never disagree, since Google treats contradictory markup as low quality.
Fields that matter, and where they live in Oro
| JSON-LD property | OroCommerce source |
|---|---|
name | Product::names (localized ProductName collection) — entity.defaultName.string in Twig |
sku | Product::sku (native field, always present) |
brand | Product::brand — the Brand entity relation |
gtin / gtin13 / mpn | Not native. Create as custom Product Attributes |
description | Product::descriptions / shortDescriptions, or the SEO Meta Description field |
image | Product::images (ProductImage collection, filtered by image type) |
offers.price, priceCurrency | Resolved price from the active price list / price rule for the visitor's price list (PricingBundle) |
offers.availability | Inventory status (In Stock / Out of Stock / Backorder) from InventoryBundle |
aggregateRating | Only if you have a reviews/ratings source actually rendered on the page |
GTIN, UPC, EAN, and MPN are not system fields on Product — OroCommerce's EAV-based Product Attributes framework is where you add them. In the back-office, go to Products → Product Attributes → Create Attribute, choose the String type, set the field name (e.g., gtin), and on the following step enable Show On View under Storefront options so the value is exposed to the storefront rather than kept back-office-only. Then assign it to the relevant Attribute Family/Group so it appears on the products that need it. Once saved, the value is reachable in Twig the same way native fields are — entity.gtin — because Oro's extended-entity layer generates the accessor for you.
Rendering the JSON-LD through a layout update
OroCommerce storefront pages are composed through the Layout component, not by editing controller templates directly. The product view page route is oro_product_frontend_product_view. Layout updates for a single route live under a theme folder matching that route name:
src/Acme/Bundle/ThemeBundle/Resources/views/layouts/default/oro_product_frontend_product_view/layout_update.yml
Add a block (parented to head, alongside where Oro's own meta blocks for title/description already attach) and point it at a Twig file:
layout:
actions:
- '@add':
id: product_json_ld
parentId: head
blockType: container
options:
attr:
class: 'product-json-ld'
- '@setBlockTheme':
themes: '@AcmeThemeBundle/layouts/default/oro_product_frontend_product_view/product_json_ld.html.twig'
Then define the widget block in the matching Twig file, reusing the exact same entity accessors the core PDP template uses (entity.sku, entity.defaultName.string, entity.brand, entity.images) so the JSON-LD is always sourced from the same data the visible page renders from, not a hand-copied duplicate:
{% block _product_json_ld_widget %}
<script type="application/ld+json">
{{ {
"@context": "https://schema.org",
"@type": "Product",
"name": entity.defaultName.string,
"sku": entity.sku,
"gtin13": entity.gtin,
"mpn": entity.mpn,
"brand": { "@type": "Brand", "name": entity.brand ? entity.brand|oro_format_name(null, 'full') : null },
"description": entity.defaultShortDescription.text,
"image": product_image_url,
"offers": {
"@type": "Offer",
"url": product_url,
"priceCurrency": price_currency,
"price": price_value,
"availability": "https://schema.org/" ~ availability_schema
}
}|json_encode(constant('JSON_UNESCAPED_SLASHES') b-or constant('JSON_UNESCAPED_UNICODE'))|raw }}
</script>
{% endblock %}
The exact variable names available inside a layout block template (whether the price/availability come through a dedicated data provider or need a small PHP block type) vary by Oro version — check the Symfony profiler's Layout tab on a live page to confirm what's already in context before wiring these up, rather than assuming a name.
Two OroCommerce-specific gotchas for distributors:
- Guest pricing. Many B2B storefronts hide price until login (see Guests settings above). If an anonymous visitor — which is what Googlebot is — can't see a price, don't emit a placeholder
offers.price; omitoffersor the price fields rather than showing a number that won't match what a logged-in buyer sees. - Availability must track the same inventory flag the "Add to Cart" button uses, not a separately maintained value, or the two will eventually disagree.
Keeping it in sync
The single biggest risk with JSON-LD blocks is that they get written once and then drift from the page. Concretely:
- Never hardcode strings into the Twig block — always pull from
entity.*or the same price/inventory services the rest of the PDP layout uses. - If the page is behind full-page or CDN caching, the JSON-LD block must invalidate on the same cache tags/events as the rest of the product page — don't give it a separate TTL.
- If aggregateRating isn't visibly rendered on the page for a given product, don't emit it for that product either; Google requires the rating to be user-visible.
- Add a lightweight scheduled check (or a Behat/functional test) that pulls a sample of PDPs and diffs the JSON-LD
name/sku/priceagainst the rendered DOM — cheap insurance against a future template change silently breaking one but not the other.
How to validate
- View-source vs. rendered DOM:
curl -s https://yourstore.example.com/product/123 | grep -A 30 'application/ld+json'shows what crawlers that don't execute JS will see; compare it against the browser DevTools Elements panel (which shows the rendered DOM) to confirm they match. - Rich Results Test: paste the live URL (or the raw HTML) and confirm the Product type is detected with no missing-field errors.
- Schema Markup Validator: a stricter, non-Google check of the raw JSON-LD syntax and vocabulary.
- Spot-check that the emitted
priceandavailabilitymatch what an anonymous visitor actually sees on that URL — not a logged-in, customer-specific price.
Verified as of July 2026 against the current OroCommerce/OroSEOBundle documentation and Google Search Central's Product structured data guidelines; menu paths and layout syntax should be confirmed against your specific Oro version before shipping, since layout internals do shift between minor releases.
This is the half of the problem page-side tooling was built to solve — but it only has something worth marking up if the underlying data is actually there. Anglera enriches product attributes, identifiers, and specs continuously in the PIM or platform your team already uses, so the gtin, mpn, brand, and description fields this JSON-LD block references are populated and current rather than blank placeholders you're stitching together by hand.
