Sylius SEO Suite Pro: Structured Data
JSON-LD for Product, Breadcrumb, Organization and WebSite. GTIN, MPN, brand and additionalProperty from product attributes per channel, Twig function mmd_seo_suite_pro_json_ld, settings and Shop API.
Sylius SEO Suite Pro: Structured Data
The plugin outputs schema.org data as JSON-LD on product, category and home pages. Every schema.org type is its own SchemaProviderInterface implementation. In addition you can decide per channel which product attributes feed the Product node: GTIN, MPN, brand and further properties (additionalProperty).
Everything a provider emits must have a visible counterpart on the page. Otherwise search engines ignore it, and a rating nobody can see is what gets a shop penalised.
Built-in providers
Every service implementing SchemaProviderInterface is tagged mmd_sylius_seo_suite_pro.schema_provider automatically. Give the tag a priority to control the order; higher values come first.
| Provider | Page | Priority | Notes |
|---|---|---|---|
ProductSchemaProvider |
product | 30 | Offer (one priced variant) or AggregateOffer with price, currency, SKU and availability from inventory; brand from the configured attribute; image. AggregateRating and Review only from reviews a moderator accepted |
BreadcrumbListSchemaProvider |
product, category | 20 | Same path as the shop's visible breadcrumb. Entries the shop shows without a link (root category, disabled categories) are left out because a list item needs a URL |
OrganizationSchemaProvider |
home page | 10 | Channel name, URL, contact e-mail and phone number if set |
WebSiteSchemaProvider |
home page | 5 | Name, URL, language (inLanguage, for example en-US) |
A provider that throws is logged and skipped. It cannot take the page down. If a provider returns [], nothing is emitted.
graph TD
A["SchemaContext with page, channel, locale and meta data"] --> B["StructuredDataBuilder"]
B --> C["ProductSchemaProvider, priority 30"]
C --> D["Product enrichers by priority"]
D --> E["List of JSON-LD nodes"]
B --> F["BreadcrumbList, priority 20"]
B --> G["Organization, priority 10, home page only"]
B --> H["WebSite, priority 5, home page only"]
F --> E
G --> E
H --> E
E --> I["Storefront - one script tag per node"]
E --> J["Shop API - field structuredData"]
The name of the Product node is the resolved meta title, its description the resolved meta description (or, if that is empty, the product's short description). An SEO template therefore takes effect here as well.
Prices are Sylius' minor units divided by 100 (two decimals), the currency is the channel's base currency. A currency with another number of fraction digits needs its own product provider.
The prices and availability in the Product node come from the channel of the page. Only variants with a price in that channel appear in the node.
Product attributes in the Product node
Product attributes can feed the Product node, so the identifiers and specifications a shop already maintains reach search engines and AI shopping assistants. This is configured per channel: the Structured data button on the SEO Suite Pro screen opens the settings. There is one row per channel, a back button leads to SEO Suite Pro, and there is no menu entry of its own.
Settings
| Setting | Meaning |
|---|---|
| Channel | The channel the row applies to. There is exactly one row per channel; on an existing row the channel cannot be changed |
| GTIN attribute | A text attribute holding the GTIN |
| MPN attribute | A text attribute holding the manufacturer part number |
| Brand attribute | A text or select attribute holding the brand. A mapped value wins over the plugin setting brand_attribute_code. If nothing is mapped ("Default"), that result stays untouched |
| Additional properties | Off (default), All attributes or Selected attributes only |
| Attributes to list | Only in the mode Selected attributes only: at least one attribute |
| Attributes never listed | In the modes All attributes and Selected attributes only, for example for internal attributes |
The list of rows shows channel, mode, and the GTIN, MPN and brand attributes, and offers create, edit and delete. The data lives in the table mmd_sylius_seo_suite_pro_schema_settings.
Map only attributes that the product page shows too (Sylius lists product attributes on the product page by default). Search engines ignore structured data nobody can see. The form says so, the plugin does not enforce it.
The form offers only suitable attributes and validates on save:
| Message | Trigger |
|---|---|
| The attribute {{ code }} does not exist. | An attribute code does not (or no longer) exist |
| The attribute {{ code }} is of the type {{ type }}; only {{ allowed }} can be used here. | GTIN or MPN not of the type text; brand not text or select |
| Choose at least one attribute, or switch the mode. | Mode Selected attributes only without an attribute |
| This channel already has SEO Suite Pro settings. | Second row for the same channel |
Only text attributes are allowed for GTIN and MPN. Sylius stores integer attributes in a 32 bit column, which cannot hold a GTIN-12/13/14 and drops leading zeros. The help text in the form still mentions "text or number", but the validation is what counts: only
textis accepted.
GTIN, MPN and brand
- GTIN. Only a valid GTIN is emitted: digits only (spaces and hyphens are removed), length 8, 12, 13 or 14 and a correct GS1 check digit. It goes into the property that matches its length (
gtin8,gtin12,gtin13orgtin14), the most specific form. An invalid value is left out and logged once per process and product code; a blank value is ignored without a log entry. - MPN. Trimmed, and left out (logged once) when longer than 70 characters.
- Brand. The value of the mapped attribute replaces
brandin the node. Without a mapping the brand comes from the plugin valuebrand_attribute_code(defaultbrand); if the product does not have that attribute, there is no brand in the node. - Language. An attribute value is taken in the page language, then the language-independent one, then the one in the channel's default language.
additionalProperty
One PropertyValue is created per attribute. The name is the attribute name in the page language (then the channel's default language, then any translation, then the code). The value is formatted by attribute type:
| Attribute type | Value |
|---|---|
| Text | trimmed |
| Textarea (rich text) | tags (script and style content included) removed, entities decoded |
| Integer | as stored |
| Percent, date | in the language's format (a date format configured on the attribute is honoured) |
| Checkbox | localized Yes or No (a false checkbox is kept) |
| Select | the labels joined by , |
Values are cut at 500 characters and empty values are skipped. An attribute already used for GTIN, MPN or brand (also the default brand attribute, when it supplies the brand) is not repeated. At most schema.max_additional_properties entries are emitted (default 25, in the attributes' display order).
Mode for additional properties
Off
The default. No additionalProperty is created. If neither GTIN, MPN nor brand is mapped either, the JSON-LD stays byte for byte what it is without these settings, and the attribute values are not even loaded.
All attributes
All attributes of the product that have a value are candidates, minus the list Attributes never listed and the attributes already used for GTIN, MPN or brand. Capped at 25 entries.
Selected attributes only
Candidates are only the attributes from Attributes to list, also minus the exclusion list. The order in the node follows the attributes' display order, not the order of the selection.
Variants
Sylius attributes belong to the product, not to a variant. GTIN, MPN, brand and the properties therefore sit on the Product node only. The offers of the variants are not touched. A GTIN per variant needs an own enricher, see Extending.
Example: a complete Product node
Settings of the channel WOHNWELT: GTIN attribute ean, MPN attribute mpn, mode Selected attributes only with material, breite and schlaffunktion. The product has two accepted reviews (5 and 4 stars). Shown formatted here; in the HTML every node stands compact in a script tag of its own:
{
"@context": "https://schema.org",
"@type": "Product",
"name": "Ledersofa Milano – Casa Nova | Wohnwelt",
"description": "Dreisitzer aus pflanzlich gegerbtem Leder mit Federkern. Jetzt bei Wohnwelt bestellen.",
"sku": "MILANO-3S",
"image": ["https://www.wohnwelt.example/media/image/ab/cd/ledersofa-milano.jpg"],
"brand": {"@type": "Brand", "name": "Casa Nova"},
"offers": {
"@type": "Offer",
"url": "https://www.wohnwelt.example/de_DE/products/ledersofa-milano",
"sku": "MILANO-3S",
"price": "1299.00",
"priceCurrency": "EUR",
"availability": "https://schema.org/InStock"
},
"aggregateRating": {"@type": "AggregateRating", "ratingValue": "4.5", "reviewCount": 2},
"review": [
{
"@type": "Review",
"name": "Sehr bequem",
"reviewBody": "Hochwertig verarbeitet und schnell geliefert.",
"author": {"@type": "Person", "name": "Anna"},
"reviewRating": {"@type": "Rating", "ratingValue": 5}
},
{
"@type": "Review",
"author": {"@type": "Person", "name": "Anonymous"},
"reviewRating": {"@type": "Rating", "ratingValue": 4}
}
],
"gtin13": "4006381333931",
"mpn": "ML-2200-BR",
"additionalProperty": [
{"@type": "PropertyValue", "name": "Material", "value": "Echtleder"},
{"@type": "PropertyValue", "name": "Breite", "value": "220"},
{"@type": "PropertyValue", "name": "Schlaffunktion", "value": "Nein"}
]
}
Notes on the details:
aggregateRatingis calculated over all accepted reviews.reviewholds at most 10 entries.- If a review has no author with a first name,
Anonymousis used. Empty fields of a review (title, text) are left out. - If the product has several priced variants,
offersbecomes anAggregateOffer:
"offers": {
"@type": "AggregateOffer",
"lowPrice": "1299.00",
"highPrice": "1499.00",
"priceCurrency": "EUR",
"offerCount": 2,
"offers": [
{"@type": "Offer", "url": "https://www.wohnwelt.example/de_DE/products/ledersofa-milano", "sku": "MILANO-3S", "price": "1299.00", "priceCurrency": "EUR", "availability": "https://schema.org/InStock"},
{"@type": "Offer", "url": "https://www.wohnwelt.example/de_DE/products/ledersofa-milano", "sku": "MILANO-3S-XL", "price": "1499.00", "priceCurrency": "EUR", "availability": "https://schema.org/OutOfStock"}
]
}
Without a settings row, or with nothing mapped and the mode Off, the Product node stays as the plugin creates it without these settings.
Twig function mmd_seo_suite_pro_json_ld
The function mmd_seo_suite_pro_json_ld(array $node) turns a schema.org node into the content of a <script type="application/ld+json"> tag. The output is HTML-safe: <, > and & are emitted as Unicode escapes (\u003C, \u003E, \u0026), so no value can close the script element. Slashes and Unicode characters stay readable.
The bundled template shop/metatags.html.twig is attached to the hook sylius_shop.base#metatags and prints one script tag per node. mmd_seo_suite_pro_seo() returns the data of the current page (null on pages that are neither product, category nor home page):
{% set seo = mmd_seo_suite_pro_seo() %}
{% if seo %}
{% for node in seo.structuredData %}
<script type="application/ld+json">{{ mmd_seo_suite_pro_json_ld(node) }}</script>
{% endfor %}
{% endif %}
Shop API / Headless: The same nodes are in the field
structuredDataofGET /api/v2/shop/seo/products/{code}andGET /api/v2/shop/seo/taxons/{code}, because Storefront and API use the same service. The field is a list of JSON-LD nodes, see Shop API. A headless frontend serializes them into ascripttag itself and must escape<,>and&as well.
Configuration
| Key | Default | Meaning |
|---|---|---|
mmd_sylius_seo_suite_pro.brand_attribute_code |
brand |
Attribute code of the brand when no brand attribute is mapped for the channel |
mmd_sylius_seo_suite_pro.schema.max_additional_properties |
25 |
Upper limit for additionalProperty (minimum 1) |
mmd_sylius_seo_suite_pro.image_base_path |
/media/image/ |
Where local product images are served; basis for image |
All keys are listed on the Configuration page.
Cost
The attribute values of a product are loaded with one fetch-join query per request, shared with the placeholders of the SEO templates, and the settings are read once per request. The number of queries does not depend on the number of attributes.
The settings live in the table
mmd_sylius_seo_suite_pro_schema_settings. Runmmd:seo-suite-pro:setup, see Installation. Until then the Storefront and the API keep the previous output (one warning in the log). Only the Structured data screen needs the table.
Own types and fields
For a new schema.org type, implement Mmd\SyliusSeoSuiteProPlugin\Schema\SchemaProviderInterface (supports(SchemaContext) and provide(SchemaContext): array). For additional fields on the Product node, for example a GTIN per variant, implement ProductSchemaEnricherInterface (enrich(array $node, ProductInterface $product, SchemaContext $context): array). Both are tagged automatically (mmd_sylius_seo_suite_pro.schema_provider and mmd_sylius_seo_suite_pro.product_schema_enricher). Higher priority runs first, an enricher sees the result of the earlier ones, and a provider or enricher that throws is logged and skipped, so the Product node survives. The built-in attribute enricher has priority 0, so you can run before or after it. Examples and details are on the Extending page.