Markus Michalski FAQ

Sylius SEO Suite Pro: Shop API

Shop API des Sylius-SEO-Plugins im Dual Mode: Endpunkte, Felder, Beispiele für Produkt und Taxon, Fehlercodes, Zuordnung zu den Head-Tags sowie SEO-Felder über die Sylius Admin API lesen und schreiben.

English Version

Zurück zur Übersicht

Sylius SEO Suite Pro: Shop API

Das Plugin arbeitet im Dual Mode: Ein Twig-Storefront bekommt seine SEO-Tags über einen Twig Hook im <head>, ein Headless-Frontend holt dieselben Daten über die Shop API. Beide Wege fragen denselben Service (SeoDataProviderInterface) und können deshalb nicht auseinanderlaufen. Zusätzlich lassen sich die SEO-Felder pro Produkt und Kategorie über die Sylius Admin API lesen und schreiben.

Inhalt

Dual Mode im Überblick

graph TD
    A["Storefront: Twig Hook metatags in sylius_shop.base"] --> B["Twig-Funktion 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 im head"]
    F --> H["SeoResource: JSON-Antwort"]

Das Plugin liefert die Daten, aber kein Frontend: Ein Headless-Client baut aus der JSON-Antwort selbst seine <head>-Tags. Die Antwort enthält bewusst fertige Werte (Titel, Canonical, Robots-Direktiven, JSON-LD-Knoten), damit der Client keine SEO-Logik nachbauen muss.

Endpunkte

Die Ressource heißt Seo und ist rein lesend. Die Pfade liegen unter dem Präfix der Sylius Shop API (/api/v2).

Methode Pfad Beschreibung
GET /api/v2/shop/seo/products/{code} SEO-Daten eines Produkts, Schlüssel ist der Produkt-Code
GET /api/v2/shop/seo/taxons/{code} SEO-Daten einer Kategorie (Taxon), Schlüssel ist der Taxon-Code

Beim Taxon-Endpunkt gibst Du die Filter- und Sortier-Parameter der Listing-Seite einfach an die Query-String weiter, zum Beispiel ?sorting[name]=asc. Das Plugin wendet darauf dieselbe Facet-Regel an wie im Storefront: Ein gefilterter oder sortierter Listing-Aufruf bekommt noindex, follow und ein Canonical ohne Filter-Parameter. Details dazu stehen auf der Seite Konfiguration.

Es gibt keinen Endpunkt für die Startseite, keine Listen-Endpunkte und keine Schreib-Operationen.

Felder der Antwort

Alle Felder gehören zur Serialisierungsgruppe mmd_seo:read.

Feld Typ Beschreibung
code string Der angefragte Produkt- bzw. Taxon-Code
title string Seitentitel nach der Prioritätskette (manueller Wert, SEO-Template, Fallback)
description string Meta Description nach derselben Kette, kann ein leerer String sein
canonicalUrl string oder null Absolute Canonical-URL der Seite
robots string oder null Fertiger Wert für das Robots-Meta-Tag, zum Beispiel index, follow; null, wenn keine Direktive gesetzt ist
openGraph object oder null Open-Graph-Daten; null, wenn die Social Tags für den Channel abgeschaltet sind
twitterCard object oder null Twitter-Card-Daten; null, wenn die Social Tags für den Channel abgeschaltet sind
structuredData array Liste der JSON-LD-Knoten (schema.org), je Knoten ein Objekt mit @context und @type; leer, wenn nichts ausgegeben wird
alternates array Liste der hreflang-Alternativen als {hreflang, href}; leer, wenn die Seite keine bekommt

Aufbau von openGraph:

Feld Typ Beschreibung
title string og:title
description string og:description
url string og:url, entspricht der Canonical-URL
type string og:type, bei Produkten product, sonst website
image string oder null og:image, absolute URL
imageAlt string oder null og:image:alt
siteName string oder null og:site_name (Einstellung des Channels, sonst der Channel-Name)
locale string oder null og:locale, der Locale-Code der Anfrage
imageWidth int og:image:width, nur vorhanden, wenn ein generiertes OG-Bild verwendet wird
imageHeight int og:image:height, nur bei generiertem OG-Bild
imageType string og:image:type, nur bei generiertem OG-Bild

Aufbau von twitterCard:

Feld Typ Beschreibung
card string summary oder summary_large_image (sobald ein Bild vorhanden ist)
title string twitter:title
description string twitter:description
image string oder null twitter:image
imageAlt string oder null twitter:image:alt
site string oder null twitter:site (Channel-Einstellung)
creator string oder null twitter:creator (Channel-Einstellung)

Größe und Typ des Bildes (imageWidth, imageHeight, imageType) sind nur bei einem generierten OG-Bild bekannt. Ohne generierte Karte fehlen diese drei Schlüssel komplett, sie stehen nicht als null in der Antwort. Mehr zu den Karten auf der Seite OG-Bilder.

Die Seiten Strukturierte Daten und hreflang beschreiben, welche Knoten bzw. Alternativen wann entstehen.

Beispiel: Produkt

Die Beispiele verwenden Accept: application/ld+json. Das ist das Format, in dem Sylius die API standardmäßig ausliefert, und die Schlüssel @context, @id und @type kommen von API Platform. Die Werte sind frei gewählt, die Struktur entspricht der echten Antwort.

curl -s "https://shop.example/api/v2/shop/seo/products/BLUE-SOFA" \
  -H "Accept: application/ld+json" \
  -H "Accept-Language: de_DE"
{
  "@context": "/api/v2/contexts/Seo",
  "@id": "/api/v2/shop/seo/products/BLUE-SOFA",
  "@type": "Seo",
  "code": "BLUE-SOFA",
  "title": "Blaues Sofa kaufen | Beispiel-Shop",
  "description": "Das blaue Sofa mit Stoffbezug, direkt ab Lager lieferbar.",
  "canonicalUrl": "https://shop.example/de_DE/products/blaues-sofa",
  "robots": "index, follow",
  "openGraph": {
    "title": "Blaues Sofa kaufen | Beispiel-Shop",
    "description": "Das blaue Sofa mit Stoffbezug, direkt ab Lager lieferbar.",
    "url": "https://shop.example/de_DE/products/blaues-sofa",
    "type": "product",
    "image": "https://shop.example/media/image/blue-sofa.jpg",
    "imageAlt": "Blaues Sofa",
    "siteName": "Beispiel-Shop",
    "locale": "de_DE"
  },
  "twitterCard": {
    "card": "summary_large_image",
    "title": "Blaues Sofa kaufen | Beispiel-Shop",
    "description": "Das blaue Sofa mit Stoffbezug, direkt ab Lager lieferbar.",
    "image": "https://shop.example/media/image/blue-sofa.jpg",
    "imageAlt": "Blaues Sofa",
    "site": "@beispielshop",
    "creator": null
  },
  "structuredData": [
    {
      "@context": "https://schema.org",
      "@type": "Product",
      "name": "Blaues Sofa kaufen | Beispiel-Shop",
      "description": "Das blaue Sofa mit Stoffbezug, direkt ab Lager lieferbar.",
      "sku": "BLUE-SOFA",
      "image": ["https://shop.example/media/image/blue-sofa.jpg"],
      "offers": {
        "@type": "Offer",
        "url": "https://shop.example/de_DE/products/blaues-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/de_DE/taxons/sofas" },
        { "@type": "ListItem", "position": 2, "name": "Blaues Sofa", "item": "https://shop.example/de_DE/products/blaues-sofa" }
      ]
    }
  ],
  "alternates": [
    { "hreflang": "de-DE", "href": "https://shop.example/de_DE/products/blaues-sofa" },
    { "hreflang": "en-US", "href": "https://shop.example/en_US/products/blue-sofa" },
    { "hreflang": "x-default", "href": "https://shop.example/en_US/products/blue-sofa" }
  ]
}

Beispiel: Kategorie

Ein sortierter Listing-Aufruf wird als Facet erkannt: robots ist dann noindex, follow, das Canonical bleibt die parameterfreie Listing-URL, und es gibt keine 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 | Beispiel-Shop",
  "description": "Alle Sofas im Überblick.",
  "canonicalUrl": "https://shop.example/de_DE/taxons/sofas",
  "robots": "noindex, follow",
  "openGraph": {
    "title": "Sofas | Beispiel-Shop",
    "description": "Alle Sofas im Überblick.",
    "url": "https://shop.example/de_DE/taxons/sofas",
    "type": "website",
    "image": null,
    "imageAlt": null,
    "siteName": "Beispiel-Shop",
    "locale": "de_DE"
  },
  "twitterCard": {
    "card": "summary",
    "title": "Sofas | Beispiel-Shop",
    "description": "Alle Sofas im Überblick.",
    "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/de_DE/taxons/sofas" }
      ]
    }
  ],
  "alternates": []
}

Die Beispiele zeigen die Struktur der Antwort. Welche Knoten in structuredData stehen und welche Werte title, description und canonicalUrl bekommen, hängt von Deinen SEO-Templates, Channel-Einstellungen und Produktdaten ab.

Zuordnung: Storefront-Tag zu API-Feld

Das Template metatags.html.twig rendert dieselben SeoData, aus denen die API ihre Felder füllt. Der <title> ist die Ausnahme: Er wird im Storefront nicht über das Template ausgegeben, sondern per Response-Listener im fertigen HTML ersetzt. Die API liefert den Wert trotzdem in title.

Storefront <head>-Element API-Feld
<title> title
<meta name="description"> description
<link rel="canonical"> canonicalUrl
<meta name="robots"> robots
<link rel="alternate" hreflang="..."> alternates[].hreflang und 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"> (eines je Knoten) structuredData[] (ein Objekt je Knoten)

Ein Headless-Client schreibt die JSON-LD-Knoten als JSON.stringify(node) in ein <script type="application/ld+json">. Das Storefront maskiert dabei Zeichenfolgen, die das Script-Element vorzeitig beenden würden (Twig-Funktion mmd_seo_suite_pro_json_ld). Wenn Du die Knoten selbst serialisierst, übernimmst Du dieses Escaping.

Fehler und Statuscodes

Status Wann
200 Die Seite existiert und ist im aktuellen Channel sichtbar
404 Unbekannter Code
404 Produkt deaktiviert
404 Produkt wird im aktuellen Channel nicht verkauft, oder der Channel lässt sich nicht ermitteln
404 Kategorie deaktiviert
404 Der Seite lassen sich keine SEO-Daten zuordnen

Alle Fälle antworten mit derselben Meldung Not found.. Das Format des Fehlerkörpers bestimmt API Platform anhand des angefragten Formats. Ein Client, der 404 bekommt, rendert einfach keine zusätzlichen SEO-Tags.

Das Plugin selbst löst keine weiteren Statuscodes aus, auch keine 401 oder 403.

Channel, Sprache und Zugriff

Das Plugin ermittelt Channel und Sprache nicht selbst. Es fragt den ChannelContextInterface und den LocaleContextInterface von Sylius und verwendet das Ergebnis. Welche Merkmale der Anfrage (Hostname, Header) dort den Ausschlag geben, hängt von der Channel- und Locale-Konfiguration Deiner Sylius-Installation ab. Die API-Pfade selbst enthalten weder Channel noch Locale.

Ein Produkt wird nur im aktuellen Channel ausgeliefert, in dem es verkauft wird. Prüfe bei einem Headless-Setup, dass die Anfrage an die Shop API den richtigen Channel trifft, sonst bekommst Du für gültige Produkte ein 404.

Die Endpunkte liegen unter /api/v2/shop und sind im Standard-Setup von Sylius öffentlich zugänglich (PUBLIC_ACCESS). Das Plugin ergänzt keine eigene Authentifizierung. Wenn Du die Shop API absicherst, gilt das auch für diese Endpunkte.

Die hreflang-URLs in alternates verwenden die Hostnamen der Channels und das Schema der API-Anfrage. Ein Headless-Frontend auf einem anderen Host muss diese URLs selbst auf seine Adressen abbilden.

Admin API: SEO-Felder lesen und schreiben

Die Felder pro Produkt und Kategorie (metaTitle, canonicalPathOverride, robotsIndex, robotsFollow und bei Kategorien metaDescription) hängen an den Übersetzungs-Objekten der Sylius Admin API. So kann ein Katalog-Client (Automatisierung, MCP-Server) sie ohne das Admin-Formular lesen und setzen. Das Plugin bringt dafür keine eigenen Admin-API-Endpunkte mit.

Technisch funktioniert das über Serialisierungsgruppen (sylius:admin:product:*, sylius:admin:taxon:*, sylius:admin:taxon_translation:show), die direkt an den Eigenschaften der Traits SeoAwareTranslationTrait und MetaDescriptionAwareTranslationTrait hängen. Es gibt keine Konfiguration im Plugin, die Sylius' Admin API erweitert. Die Felder erscheinen deshalb nur, wenn Deine Übersetzungs-Klassen die Traits verwenden (Opt-in, siehe Erweiterbarkeit).

Methode Pfad Beschreibung
GET /api/v2/admin/products/{code} Produkt; translations.{locale} enthält metaTitle, canonicalPathOverride, robotsIndex, robotsFollow
GET /api/v2/admin/taxons/{code} Kategorie; wie beim Produkt, zusätzlich translations.{locale}.metaDescription
PUT /api/v2/admin/products/{code} Produkt aktualisieren, SEO-Felder in translations.{locale}
PUT /api/v2/admin/taxons/{code} Kategorie aktualisieren, SEO-Felder in translations.{locale}
{
  "translations": {
    "en_US": {
      "@id": "/api/v2/admin/products/P1/translations/en_US",
      "metaTitle": "Blue sofa | Shop",
      "canonicalPathOverride": "/sofas/blue",
      "robotsIndex": false
    }
  }
}

Nach dem Update des Plugins oder dem Einbau der Traits einmal bin/console cache:clear ausführen: Der Serializer hält seine Metadaten im Cache.

Nicht über die API verfügbar

Thema Warum
robots.txt Ein reines Textdokument für Crawler, das unter /robots.txt am Shop-Host für beide Frontends gleich ausgeliefert wird. Inhalt pro Channel im Admin, siehe Konfiguration.
Redirects Sie greifen auf HTTP-Ebene, bevor eine Seite gerendert wird, und beantworten nur Pfade des Shop-Hosts. Es gibt keine API-Ressource. Ein Headless-Frontend auf einem anderen Host behandelt seine 404-Seiten selbst. Siehe Redirects.
OG-Bild-Bytes Die API liefert nur die URL der Karte in openGraph.image. Das Bild selbst holt der Client von der Karten-Route des Shops (/seo/og/...). Siehe OG-Bilder.
SEO-Audit Eine Backend-Funktion, die im Admin gestartet und gelesen wird. Es gibt keine API-Ressource. Siehe Audit.
Startseite Es gibt keinen Endpunkt dafür. Die hreflang-Alternativen, der Organization- und der WebSite-Knoten der Startseite entstehen nur im Storefront über Twig.
Einstellungen der Channels, SEO-Templates Sie werden im Admin gepflegt und wirken sich nur auf die Werte der Antwort aus. Es gibt keine API dafür.

Fragen und typische Fehlerbilder findest Du auf der Seite Fehlerbehebung.