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.
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
- Endpunkte
- Felder der Antwort
- Beispiel: Produkt
- Beispiel: Kategorie
- Zuordnung: Storefront-Tag zu API-Feld
- Fehler und Statuscodes
- Channel, Sprache und Zugriff
- Admin API: SEO-Felder lesen und schreiben
- Nicht über die API verfügbar
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 alsnullin 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
structuredDatastehen und welche Wertetitle,descriptionundcanonicalUrlbekommen, 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
}
}
}
- Eine bestehende Übersetzung braucht ihre
@id(aus dem GET), genau wie Sylius' eigene übersetzte Felder. Ohne sie antwortet Sylius mit der Meldung, dass für den Locale-Code bereits eine Übersetzung existiert. - Felder, die Du weglässt, behalten ihren Wert. Ein PUT ohne
robotsIndexoderrobotsFollowsetzt sie nicht zurück. canonicalPathOverridewird wie im Admin validiert: ein Pfad mit genau einem führenden Slash. Eine vollständige URL oder//host/...ergibt ein422. Höchstens 255 Zeichen.metaTitleist auf 255 Zeichen begrenzt,metaDescription(Kategorie) auf 512.- Die
metaDescriptionder Kategorie gibt es nur, wenn DeineTaxonTranslationdenMetaDescriptionAwareTranslationTraitverwendet. Die Meta Description des Produkts ist Sylius' eigenes Feld. - Der Zugriff folgt der Admin-API-Absicherung von Sylius (Token, Rolle
ROLE_API_ACCESS).
Nach dem Update des Plugins oder dem Einbau der Traits einmal
bin/console cache:clearausfü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.