Sylius SEO Suite Pro: SEO-Templates
Meta-Titel und Meta-Beschreibung per Template erzeugen: Prioritätenkette, Platzhalter, leere Platzhalter, Kürzen an der Wortgrenze, Sprache und Channel, Startseite, Shop API und eigene Platzhalter.
Sylius SEO Suite Pro: SEO-Templates
Ein SEO-Template erzeugt den Meta-Titel und/oder die Meta-Beschreibung aller Produkte oder Kategorien eines Channels, ohne dass Du pro Seite etwas eintippen musst. Eine Zeile im Bildschirm SEO-Templates enthält ein Titel-Template und ein Beschreibungs-Template für einen Channel, einen Typ (Produkt, Kategorie oder Sonstiges) und optional eine Sprache.
Den Bildschirm erreichst Du über MMKreativ Plugins > SEO Suite Pro (einziger Menüeintrag des Plugins) und dort über den Button SEO-Templates. Der Button Zurück zu SEO Suite Pro führt zurück.
Der Typ Sonstiges steht für jede Seite, die weder Produkt noch Kategorie ist. Heute ist das die Startseite, siehe Startseiten-Beschreibung.
Prioritätenkette
Für jedes Feld gewinnt die erste Stufe, die einen verwertbaren Wert liefert: erst der handgeschriebene Wert, dann das Template, dann der Fallback.
graph TD
A["Seite wird aufgebaut"] --> B{"Handgeschriebener Wert vorhanden?"}
B -->|ja| C["Handgeschriebenen Wert verwenden, nie gekürzt"]
B -->|nein| D{"Template vorhanden und vollständig auflösbar?"}
D -->|ja| E["Generierten Wert verwenden, an Wortgrenze gekürzt"]
D -->|nein| F["Fallback - Name beim Titel, kein Tag bei der Beschreibung"]
| Feld | 1. Handgeschrieben | 2. Template | 3. Fallback |
|---|---|---|---|
| Titel (Produkt, Kategorie) | Meta-Titel der Übersetzung | Titel-Template | der Name |
| Beschreibung, Produkt | Meta-Beschreibung aus dem Sylius-Core | Beschreibungs-Template | keiner (kein Tag) |
| Beschreibung, Kategorie | Meta-Beschreibung-Feld des Plugins | Beschreibungs-Template | keiner (kein Tag) |
| Beschreibung, Startseite | Beschreibung des Channels | Beschreibungs-Template vom Typ Sonstiges | keiner (kein Tag) |
Ein leerer oder nur aus Leerzeichen bestehender Wert zählt in jeder Stufe als nicht gesetzt. Ein handgeschriebener Wert wird nie gekürzt. Ohne Template-Zeile verhält sich alles wie ohne dieses Feature.
Der handgeschriebene Titel (Meta-Titel) und die Kategorie-Beschreibung existieren nur, wenn Du die Felder pro Entität in Deinen Übersetzungsklassen freigeschaltet hast, siehe Installation. Ohne diese Freischaltung gibt es keinen handgeschriebenen Titel, und das Titel-Template greift direkt. Ohne das Kategorie-Feld greift das Beschreibungs-Template direkt.
Startseiten-Beschreibung
Die Startseite hat im Sylius-Admin keinen eigenen Eintrag. Ihre Beschreibung kommt deshalb aus zwei Quellen, in dieser Reihenfolge:
- Die Beschreibung des Channels (Konfiguration > Channels > Channel > Allgemein), ohne Leerzeichen am Rand. Leer zählt als nicht gesetzt.
- Das Beschreibungs-Template vom Typ Sonstiges für den Channel (und die Sprache der Seite, sonst die Zeile für alle Sprachen). Verfügbar sind nur
{channel.name}und{locale}, zum Beispiel{channel.name}: Sofas, Sessel und Tische mit kostenloser Lieferung.
Sind beide leer, hat die Startseite gar keine Beschreibungs-Tags. Was gewinnt, landet im description-Meta-Tag, in og:description und in twitter:description. Der Titel der Startseite wird nicht per Template erzeugt: Der eigene <title> von Sylius bleibt bestehen, ein Titel-Template vom Typ Sonstiges wird nicht verwendet.
Platzhalter
Es gibt reine {token}-Ersetzung aus einer festen Whitelist. Twig oder Ausdrücke werden nicht ausgewertet, ein Template kann also nichts ausführen.
| Platzhalter | Gilt für | Beschreibung |
|---|---|---|
{product.name} |
Produkte | Name in der Sprache der Seite |
{product.code} |
Produkte | Produktcode |
{product.short_description} |
Produkte | Kurzbeschreibung ohne HTML, auf 150 Zeichen gekürzt |
{attribute.<code>} |
Produkte | Wert des Produktattributs mit diesem Code in der Sprache der Seite (sprachunabhängiger Wert als Fallback). Bei einer Auswahl die Labels der gewählten Optionen, mehrere durch ", " getrennt; eine Checkbox zeigt Yes/No (lokalisiert), ein Datum seine ISO-Form |
{taxon.main} |
Produkte | Name der Hauptkategorie des Produkts (in der Sprache der Seite, sonst in der Standardsprache des Channels) |
{taxon.name} |
Kategorien | Name |
{taxon.parent} |
Kategorien | Name der übergeordneten Kategorie (Sprache der Seite, sonst Standardsprache des Channels); leer bei der Wurzel. Kategorien der ersten Ebene haben die Wurzelkategorie des Shops als Parent |
{taxon.description} |
Kategorien | Beschreibung ohne HTML, auf 150 Zeichen gekürzt |
{channel.name} |
alle Typen | Name des Channels |
{locale} |
alle Typen | Locale-Code der Seite, zum Beispiel de_DE |
Markup, das Du ins Template tippst, wird entfernt. Text, den ein Platzhalter liefert, wird so übernommen, wie er ist: Ein maskiertes <b> in einer Beschreibung bleibt der Text <b>.
Unbekannte Tokens, Tokens eines anderen Typs ({product.name} in einem Kategorie- oder Sonstiges-Template) und unausgeglichene Klammern werden beim Speichern mit einem Formularfehler abgelehnt. Ein Tippfehler geht so nicht unbemerkt live.
| Meldung | Auslöser |
|---|---|
| Gib ein Titel-Template, ein Beschreibungs-Template oder beide an. | Beide Felder leer |
| Die geschweiften Klammern sind nicht ausgeglichen. Ein Platzhalter wird als {product.name} geschrieben, mit genau einer öffnenden und einer schließenden Klammer. | Offene, überzählige oder verschachtelte Klammern |
| Der Platzhalter {{ token }} ist für diesen Typ unbekannt. | Tippfehler oder Token eines anderen Typs |
| Der Channel bietet die Sprache {{ locale }} nicht an. | Sprache gehört nicht zum gewählten Channel |
| Für diesen Channel, diese Sprache und diesen Typ gibt es bereits ein Template. | Doppelte Kombination aus Channel, Typ und Sprache |
Leere Platzhalter
Hat irgendein Platzhalter eines Templates für eine Seite keinen Wert (ein Produkt ohne das Attribut brand, das {taxon.parent} einer Wurzelkategorie), wird das gesamte Template für diese Seite übersprungen. Es gilt dann die nächste Stufe der Prioritätenkette. Etwas Halbgefülltes wie – | Shop wird nie ausgegeben. Die Regel ist für jede Seite gleich: Ein Template ist entweder vollständig auflösbar oder wird nicht benutzt.
Auch ein Template, das nach dem Ersetzen keinen Buchstaben und keine Ziffer enthält (nur Trennzeichen), wird übersprungen.
Soll ein Template für jedes Produkt greifen, nimm nur Platzhalter, die jedes Produkt hat. Für die übrigen Produkte ist der Fallback (der Name) die sichere Lösung.
Beispiel: Titel-Template mit Marke
Template (Typ Produkt, alle Sprachen) im Channel Wohnwelt:
{product.name} – {attribute.brand} | {channel.name}
| Produkt | Attribut brand |
Ergebnis |
|---|---|---|
| Ledersofa Milano | Casa Nova | Ledersofa Milano – Casa Nova \| Wohnwelt |
| Ecksofa Bergamo mit Schlaffunktion und Bettkasten | Casa Nova | Ecksofa Bergamo mit Schlaffunktion und Bettkasten – Casa (auf 60 Zeichen an der Wortgrenze gekürzt, \| Wohnwelt fällt weg) |
| Beistelltisch Lino | (nicht gepflegt) | Beistelltisch Lino (Template übersprungen, Fallback auf den Namen) |
Das dritte Produkt hat keinen Wert für {attribute.brand}. Statt eines halbleeren Titels greift der Fallback, nämlich der Produktname.
Ausgabe und Kürzen
Die Ausgabe ist Klartext: HTML-Tags werden entfernt, Entities aus Rich-Text-Quellen werden dekodiert, und mehrere Leerzeichen am Stück werden zu einem. Sie wird nicht maskiert: Das Autoescaping von Twig (Tags), der Austausch des <title> und der JSON-Encoder (API) maskieren jeweils genau einmal, damit nichts doppelt maskiert wird.
Generierte Werte werden an einer Wortgrenze gekürzt, nie mitten im Wort. Trennzeichen, die am Schnitt übrig bleiben (Name – Marke |), werden entfernt.
| Limit | Ellipse | |
|---|---|---|
| Titel | 60 Zeichen | nein |
| Beschreibung | 155 Zeichen (die Ellipse zählt mit) | ja, … |
Ist schon das erste Wort länger als das Limit, gilt das Template als nicht auflösbar, statt mitten im Wort abgeschnitten zu werden. Beide Limits sind konfigurierbar (mmd_sylius_seo_suite_pro.seo_templates.title_max_length, Standard 60, Minimum 10, und description_max_length, Standard 155, Minimum 20), siehe Konfiguration.
Gekürzt werden nur generierte Werte. Handgeschriebene Meta-Titel und Meta-Beschreibungen bleiben immer unverändert. Auch die Platzhalter
{product.short_description}und{taxon.description}kürzen ihren Text für sich auf 150 Zeichen (mit…), bevor das Template das Gesamt-Limit anwendet.
Sprache und Channel
Die Sprachliste des Formulars bietet die Sprachen aller Channels an. Beim Speichern wird eine Sprache abgelehnt, die der gewählte Channel nicht anbietet. Eine Zeile mit Sprache (de_DE) gilt genau für diese Locale, eine Zeile ohne Sprache gilt für jede Sprache des Channels. Die exakte Sprache gewinnt.
Titel und Beschreibung werden unabhängig voneinander gewählt: Eine de_DE-Zeile, die nur einen Titel definiert, lässt die Beschreibung trotzdem aus der Zeile für alle Sprachen kommen. Ein leeres Template verdeckt das der anderen Zeile nie. Es gibt keinen Fallback von de_DE auf de. Pro Channel, Typ und Sprache gibt es höchstens eine Zeile (wird beim Speichern geprüft).
Beispiel: Zeilen für einen Channel
Zeile für alle Sprachen
| Feld | Wert |
|---|---|
| Channel | Wohnwelt (WOHNWELT) |
| Gilt für | Produkt |
| Sprache | Alle Sprachen |
| Meta-Titel-Template | {product.name} \| {channel.name} |
| Meta-Beschreibungs-Template | {product.short_description} Jetzt bei {channel.name} bestellen. |
Zeile für de_DE
| Feld | Wert |
|---|---|
| Channel | Wohnwelt (WOHNWELT) |
| Gilt für | Produkt |
| Sprache | Deutsch (Deutschland) |
| Meta-Titel-Template | {product.name} – {attribute.brand} \| {channel.name} |
| Meta-Beschreibungs-Template | (leer) |
Ergebnis auf einer de_DE-Seite
Der Titel kommt aus der de_DE-Zeile, die Beschreibung aus der Zeile für alle Sprachen, weil das Beschreibungs-Template der de_DE-Zeile leer ist. Auf einer en_US-Seite kommen beide Werte aus der Zeile für alle Sprachen.
Storefront und API
Die generierten Werte laufen durch dieselben MetaData wie die handgeschriebenen. Sie landen deshalb im <title>, im description-Meta-Tag, in og:title/og:description und twitter:title/twitter:description der Storefront. In der Shop API stehen sie in title, description, openGraph und twitterCard von GET /api/v2/shop/seo/products/{code} und GET /api/v2/shop/seo/taxons/{code}. Es gibt keine Logik in Twig oder in einem Controller. Die Startseite übernimmt nur ihre Beschreibung aus einem Template (Typ Sonstiges).
Shop API / Headless: Die Felder der Endpunkte und das Antwortformat beschreibt die Seite Shop API. Ein Headless-Frontend bekommt den fertig aufgelösten Titel und die fertige Beschreibung, es muss weder Templates noch Prioritäten selbst auswerten.
Eigene Platzhalter
Implementiere Mmd\SyliusSeoSuiteProPlugin\Seo\Template\PlaceholderProviderInterface. Jeder Service, der es implementiert, wird automatisch mit mmd_sylius_seo_suite_pro.placeholder_provider getaggt. Der erste Provider, der ein Token unterstützt, gewinnt; über die priority des Tags läuft Deiner vor einem eingebauten. Ein Token wird im Admin genau dann akzeptiert, wenn ein Provider es unterstützt, die Validierung entspricht also immer dem, was gerendert wird.
final class WarrantyPlaceholderProvider implements PlaceholderProviderInterface
{
public function supports(string $token, string $subjectType): bool
{
return 'warranty' === $token && SeoTemplateInterface::SUBJECT_PRODUCT === $subjectType;
}
public function resolve(string $token, PlaceholderContext $context): ?string
{
// $context->subject, ->channel, ->locale; remember() merkt sich Teures einmal pro Render.
// Klartext zurückgeben. Null oder '' heißt "kein Wert": das ganze Template wird übersprungen.
return $context->subject instanceof ProductInterface ? '2 Jahre Garantie' : null;
}
}
Provider müssen Klartext liefern (für Rich-Text PlainText::fromHtml() verwenden) und dürfen nichts maskieren. Der Hilfetext im Formular nennt nur die eingebauten Platzhalter. Der Renderer selbst ist SeoTemplateRendererInterface (Service mmd_sylius_seo_suite_pro.seo_template_renderer) und lässt sich dekorieren oder ersetzen. Details zu allen Erweiterungspunkten stehen auf der Seite Erweiterungen.
Kosten pro Request
Der Template-Lookup liest alle Zeilen des aktuellen Channels mit einer Query pro Request und hält sie im Speicher (zwischen Requests zurückgesetzt). Titel und Beschreibung beliebig vieler Anfragen werden daraus bedient. Ein Template mit Attribut-Platzhaltern holt alle Attributwerte des Produkts samt Definitionen mit einer weiteren Query, und nur dann, wenn tatsächlich ein {attribute.*}-Token benutzt wird.
Titel und Beschreibung einer Seite teilen sich diesen Lookup (der Platzhalter-Kontext wird pro Subjekt, Channel und Locale gehalten). Er läuft also einmal pro Seite, egal wie viele Attribut-Platzhalter die Templates benutzen. Dieselbe Query nutzt auch der Product-Knoten der Strukturierten Daten.
Fehlt die Tabelle der Templates (Plugin aktiv, Migrationen noch nicht gelaufen), gilt das als "keine Templates": Der Shop verhält sich wie bisher und antwortet nicht mit einem Fehler 500. Im Log steht eine Warnung. Die Migrationen führst Du mit
mmd:seo-suite-pro:setupaus, siehe Installation.
Siehe auch
- Konfiguration: Längenlimits der Templates
- SEO-Audit: prüft den Titel und die Beschreibung, die Besucher tatsächlich bekommen (handgeschrieben, Template, Fallback)
- Strukturierte Daten: der Product-Knoten übernimmt den aufgelösten Titel und die Beschreibung
- Shop API
- Erweiterungen
- Fehlerbehebung