Markus Michalski FAQ

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.

Deutsche Version

Back to overview

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 text is accepted.

GTIN, MPN and brand

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:

"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 structuredData of GET /api/v2/shop/seo/products/{code} and GET /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 a script tag 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. Run mmd: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.

See also