Markus Michalski FAQ

Sylius SEO Suite Pro: Shop API

Shop API of the Sylius SEO plugin in dual mode: endpoints, fields, product and taxon examples, error codes, mapping to head tags, and reading and writing SEO fields through the Sylius Admin API.

Deutsche Version

Back to overview

Sylius SEO Suite Pro: Shop API

The plugin works in dual mode: a Twig storefront gets its SEO tags through a Twig hook in the <head>, a headless frontend fetches the same data through the Shop API. Both paths call the same service (SeoDataProviderInterface), so they cannot drift apart. In addition, the per-product and per-category SEO fields can be read and written through the Sylius Admin API.

Contents

Dual mode at a glance

graph TD
    A["Storefront: Twig hook metatags in sylius_shop.base"] --> B["Twig function mmd_seo_suite_pro_seo"]
    C["Headless: GET /api/v2/shop/seo/products/code"] --> D["SeoResourceProvider"]
    B --> E["SeoDataProviderInterface"]
    D --> E
    E --> F["SeoData"]
    F --> G["Template metatags.html.twig: meta tags, canonical, hreflang, JSON-LD in the head"]
    F --> H["SeoResource: JSON response"]

The plugin delivers the data, not a frontend: a headless client builds its own <head> tags from the JSON response. The response deliberately contains finished values (title, canonical, robots directives, JSON-LD nodes), so the client does not have to rebuild any SEO logic.

Endpoints

The resource is called Seo and is read-only. The paths sit below the Sylius Shop API prefix (/api/v2).

Method Path Description
GET /api/v2/shop/seo/products/{code} SEO data of a product, keyed by the product code
GET /api/v2/shop/seo/taxons/{code} SEO data of a category (taxon), keyed by the taxon code

For the taxon endpoint, pass the filter and sort parameters of the listing page on in the query string, for example ?sorting[name]=asc. The plugin applies the same facet rule as in the storefront: a filtered or sorted listing request gets noindex, follow and a canonical without filter parameters. Details are on the Configuration page.

There is no endpoint for the home page, no list endpoints and no write operations.

Response fields

All fields belong to the serialization group mmd_seo:read.

Field Type Description
code string The requested product or taxon code
title string Page title after the priority chain (manual value, SEO template, fallback)
description string Meta description after the same chain, may be an empty string
canonicalUrl string or null Absolute canonical URL of the page
robots string or null Finished value for the robots meta tag, for example index, follow; null when no directive is set
openGraph object or null Open Graph data; null when social tags are switched off for the channel
twitterCard object or null Twitter card data; null when social tags are switched off for the channel
structuredData array List of JSON-LD (schema.org) nodes, each an object with @context and @type; empty when nothing is emitted
alternates array List of hreflang alternates as {hreflang, href}; empty when the page gets none

Structure of openGraph:

Field Type Description
title string og:title
description string og:description
url string og:url, equal to the canonical URL
type string og:type, product for products, website otherwise
image string or null og:image, absolute URL
imageAlt string or null og:image:alt
siteName string or null og:site_name (channel setting, otherwise the channel name)
locale string or null og:locale, the locale code of the request
imageWidth int og:image:width, only present when a generated OG image is used
imageHeight int og:image:height, only with a generated OG image
imageType string og:image:type, only with a generated OG image

Structure of twitterCard:

Field Type Description
card string summary or summary_large_image (as soon as an image exists)
title string twitter:title
description string twitter:description
image string or null twitter:image
imageAlt string or null twitter:image:alt
site string or null twitter:site (channel setting)
creator string or null twitter:creator (channel setting)

Size and type of the image (imageWidth, imageHeight, imageType) are only known for a generated OG image. Without a generated card these three keys are missing entirely, they do not appear as null. More about the cards on the OG images page.

The Structured data and hreflang pages describe which nodes and alternates are produced and when.

Example: product

The examples use Accept: application/ld+json. That is the format Sylius serves the API in by default, and the keys @context, @id and @type come from API Platform. The values are made up, the structure matches the real response.

curl -s "https://shop.example/api/v2/shop/seo/products/BLUE-SOFA" \
  -H "Accept: application/ld+json" \
  -H "Accept-Language: en_US"
{
  "@context": "/api/v2/contexts/Seo",
  "@id": "/api/v2/shop/seo/products/BLUE-SOFA",
  "@type": "Seo",
  "code": "BLUE-SOFA",
  "title": "Buy the blue sofa | Example Shop",
  "description": "The blue fabric sofa, in stock and ready to ship.",
  "canonicalUrl": "https://shop.example/en_US/products/blue-sofa",
  "robots": "index, follow",
  "openGraph": {
    "title": "Buy the blue sofa | Example Shop",
    "description": "The blue fabric sofa, in stock and ready to ship.",
    "url": "https://shop.example/en_US/products/blue-sofa",
    "type": "product",
    "image": "https://shop.example/media/image/blue-sofa.jpg",
    "imageAlt": "Blue sofa",
    "siteName": "Example Shop",
    "locale": "en_US"
  },
  "twitterCard": {
    "card": "summary_large_image",
    "title": "Buy the blue sofa | Example Shop",
    "description": "The blue fabric sofa, in stock and ready to ship.",
    "image": "https://shop.example/media/image/blue-sofa.jpg",
    "imageAlt": "Blue sofa",
    "site": "@exampleshop",
    "creator": null
  },
  "structuredData": [
    {
      "@context": "https://schema.org",
      "@type": "Product",
      "name": "Buy the blue sofa | Example Shop",
      "description": "The blue fabric sofa, in stock and ready to ship.",
      "sku": "BLUE-SOFA",
      "image": ["https://shop.example/media/image/blue-sofa.jpg"],
      "offers": {
        "@type": "Offer",
        "url": "https://shop.example/en_US/products/blue-sofa",
        "sku": "BLUE-SOFA",
        "price": "799.00",
        "priceCurrency": "EUR",
        "availability": "https://schema.org/InStock"
      }
    },
    {
      "@context": "https://schema.org",
      "@type": "BreadcrumbList",
      "itemListElement": [
        { "@type": "ListItem", "position": 1, "name": "Sofas", "item": "https://shop.example/en_US/taxons/sofas" },
        { "@type": "ListItem", "position": 2, "name": "Blue sofa", "item": "https://shop.example/en_US/products/blue-sofa" }
      ]
    }
  ],
  "alternates": [
    { "hreflang": "en-US", "href": "https://shop.example/en_US/products/blue-sofa" },
    { "hreflang": "de-DE", "href": "https://shop.example/de_DE/products/blaues-sofa" },
    { "hreflang": "x-default", "href": "https://shop.example/en_US/products/blue-sofa" }
  ]
}

Example: category

A sorted listing request is recognised as a facet: robots is then noindex, follow, the canonical stays the parameter-free listing URL, and there are no alternates.

curl -s "https://shop.example/api/v2/shop/seo/taxons/SOFAS?sorting%5Bname%5D=asc" \
  -H "Accept: application/ld+json"
{
  "@context": "/api/v2/contexts/Seo",
  "@id": "/api/v2/shop/seo/taxons/SOFAS",
  "@type": "Seo",
  "code": "SOFAS",
  "title": "Sofas | Example Shop",
  "description": "All our sofas at a glance.",
  "canonicalUrl": "https://shop.example/en_US/taxons/sofas",
  "robots": "noindex, follow",
  "openGraph": {
    "title": "Sofas | Example Shop",
    "description": "All our sofas at a glance.",
    "url": "https://shop.example/en_US/taxons/sofas",
    "type": "website",
    "image": null,
    "imageAlt": null,
    "siteName": "Example Shop",
    "locale": "en_US"
  },
  "twitterCard": {
    "card": "summary",
    "title": "Sofas | Example Shop",
    "description": "All our sofas at a glance.",
    "image": null,
    "imageAlt": null,
    "site": null,
    "creator": null
  },
  "structuredData": [
    {
      "@context": "https://schema.org",
      "@type": "BreadcrumbList",
      "itemListElement": [
        { "@type": "ListItem", "position": 1, "name": "Sofas", "item": "https://shop.example/en_US/taxons/sofas" }
      ]
    }
  ],
  "alternates": []
}

The examples show the structure of the response. Which nodes appear in structuredData and which values title, description and canonicalUrl get depends on your SEO templates, channel settings and product data.

Mapping: storefront tag to API field

The template metatags.html.twig renders the same SeoData the API fills its fields from. The <title> is the exception: in the storefront it is not output by the template but replaced in the finished HTML by a response listener. The API still delivers the value in title.

Storefront <head> element API field
<title> title
<meta name="description"> description
<link rel="canonical"> canonicalUrl
<meta name="robots"> robots
<link rel="alternate" hreflang="..."> alternates[].hreflang and alternates[].href
<meta property="og:type"> openGraph.type
<meta property="og:title"> openGraph.title
<meta property="og:description"> openGraph.description
<meta property="og:url"> openGraph.url
<meta property="og:site_name"> openGraph.siteName
<meta property="og:locale"> openGraph.locale
<meta property="og:image"> openGraph.image
<meta property="og:image:alt"> openGraph.imageAlt
<meta property="og:image:width">, og:image:height, og:image:type openGraph.imageWidth, openGraph.imageHeight, openGraph.imageType
<meta name="twitter:card"> twitterCard.card
<meta name="twitter:title"> twitterCard.title
<meta name="twitter:description"> twitterCard.description
<meta name="twitter:image"> twitterCard.image
<meta name="twitter:image:alt"> twitterCard.imageAlt
<meta name="twitter:site"> twitterCard.site
<meta name="twitter:creator"> twitterCard.creator
<script type="application/ld+json"> (one per node) structuredData[] (one object per node)

A headless client writes each JSON-LD node as JSON.stringify(node) into a <script type="application/ld+json">. The storefront escapes character sequences that would end the script element early (Twig function mmd_seo_suite_pro_json_ld). If you serialize the nodes yourself, you take over that escaping.

Errors and status codes

Status When
200 The page exists and is visible in the current channel
404 Unknown code
404 Product disabled
404 Product is not sold in the current channel, or the channel cannot be determined
404 Category disabled
404 No SEO data can be derived for the subject

All cases answer with the same message Not found.. API Platform decides the format of the error body based on the requested format. A client that gets a 404 simply renders no extra SEO tags.

The plugin itself raises no other status codes, not even 401 or 403.

Channel, language and access

The plugin does not determine channel and language itself. It asks Sylius' ChannelContextInterface and LocaleContextInterface and uses the result. Which properties of the request (hostname, headers) decide there depends on the channel and locale configuration of your Sylius installation. The API paths contain neither channel nor locale.

A product is only served in the current channel if it is sold there. In a headless setup, make sure the request to the Shop API hits the right channel, otherwise valid products answer with a 404.

The endpoints live under /api/v2/shop and are publicly accessible in the standard Sylius setup (PUBLIC_ACCESS). The plugin adds no authentication of its own. If you lock down the Shop API, that applies to these endpoints as well.

The hreflang URLs in alternates use the hostnames of the channels and the scheme of the API request. A headless frontend on another host has to map these URLs to its own addresses.

Admin API: reading and writing SEO fields

The per-product and per-category fields (metaTitle, canonicalPathOverride, robotsIndex, robotsFollow and, for categories, metaDescription) are part of the translation objects of the Sylius Admin API. A catalogue client (automation, MCP server) can read and set them without the admin form. The plugin ships no Admin API endpoints of its own.

Technically this works through serialization groups (sylius:admin:product:*, sylius:admin:taxon:*, sylius:admin:taxon_translation:show) attached directly to the properties of the traits SeoAwareTranslationTrait and MetaDescriptionAwareTranslationTrait. The plugin has no configuration that extends Sylius' Admin API. The fields therefore only appear if your translation classes use the traits (opt-in, see Extending).

Method Path Description
GET /api/v2/admin/products/{code} Product; translations.{locale} contains metaTitle, canonicalPathOverride, robotsIndex, robotsFollow
GET /api/v2/admin/taxons/{code} Category; as for the product, plus translations.{locale}.metaDescription
PUT /api/v2/admin/products/{code} Update a product, SEO fields in translations.{locale}
PUT /api/v2/admin/taxons/{code} Update a category, SEO fields in translations.{locale}
{
  "translations": {
    "en_US": {
      "@id": "/api/v2/admin/products/P1/translations/en_US",
      "metaTitle": "Blue sofa | Shop",
      "canonicalPathOverride": "/sofas/blue",
      "robotsIndex": false
    }
  }
}

After updating the plugin or adding the traits, run bin/console cache:clear once: the serializer keeps its metadata in the cache.

Not available through the API

Topic Why
robots.txt A plain-text document for crawlers, served at /robots.txt on the shop host identically for both frontends. Content per channel in the admin, see Configuration.
Redirects They act at the HTTP level before a page is rendered and only answer paths of the shop host. There is no API resource. A headless frontend on another host handles its own 404 pages. See Redirects.
OG image bytes The API only returns the card URL in openGraph.image. The client fetches the image itself from the shop's card route (/seo/og/...). See OG images.
SEO audit A back office feature that is started and read in the admin. There is no API resource. See Audit.
Home page There is no endpoint for it. Its hreflang alternates and its Organization and WebSite nodes are only produced in the storefront through Twig.
Channel settings, SEO templates They are maintained in the admin and only affect the values of the response. There is no API for them.

Common problems and error patterns are on the Troubleshooting page.