Markus Michalski FAQ

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.

English Version

Zurück zur Übersicht

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:

  1. Die Beschreibung des Channels (Konfiguration > Channels > Channel > Allgemein), ohne Leerzeichen am Rand. Leer zählt als nicht gesetzt.
  2. 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 &lt;b&gt; 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:setup aus, siehe Installation.

Siehe auch