Markus Michalski FAQ

Sylius SEO Suite Pro: OG-Bilder

Generierte OG-Bilder (1200x630) mit Name, Preis und Logo: Einstellungen pro Channel, Auslieferung über Hash-URL, Cache, WebP und Schriften, Bildquellen lokal und per CDN, Console-Commands und Grenzen.

English Version

Zurück zur Übersicht

Sylius SEO Suite Pro: OG-Bilder

Standardmäßig ist das og:image einer Produktseite das Produktbild und das einer Kategorieseite das Kategoriebild. Das Feature OG-Bilder ist optional und standardmäßig aus: Schaltest Du es für einen Channel ein, ist das og:image (und twitter:image) seiner Seiten stattdessen eine generierte Karte im Format 1200x630 mit Name, Preis, Bild und dem Logo des Channels. Das lohnt sich für Shops, deren Produktfotos schlechte Vorschaubilder beim Teilen abgeben (weiße Hintergründe, ungünstige Proportionen, kein Name auf dem Bild).

Du erreichst die Einstellungen über den Button "OG-Bilder" oben auf der Seite "SEO Suite Pro". Das Plugin hat weiterhin nur einen Sidebar-Eintrag, der Bildschirm hat einen Button zurück zu "SEO Suite Pro".

Die Karte wird mit PHP-GD und FreeType gezeichnet, mehr nicht: kein intervention/image, kein Headless-Browser, keine neue Composer-Abhängigkeit. GD muss also mit FreeType-Unterstützung installiert sein, sonst kann keine Karte entstehen.

Einstellungen pro Channel

Pro Channel gibt es eine Zeile (Admin-Bildschirm "OG-Bilder").

Einstellung Standard Beschreibung
Social-Karte erzeugen (enabled) aus Ohne Zeile oder ausgeschaltet ist jede Seite byte-identisch zu vorher
Karten für Produkte (products_enabled) an Ob Produkte eine Karte bekommen
Karten für Kategorien (taxons_enabled) an Ob Kategorien eine Karte bekommen
Hintergrundfarbe #FFFFFF Format #RRGGBB
Textfarbe #111827 Format #RRGGBB
Akzentfarbe #2563EB Format #RRGGBB; der Balken links und der Preis
Logo leer Pfad unterhalb des Public-Verzeichnisses (/media/logo.png) oder absolute https://-URL, höchstens 1024 Zeichen; PNG, JPEG, GIF oder WebP; optional
Slogan (tagline) leer Eine kurze Zeile (höchstens 120 Zeichen) neben dem Logo; optional
Preis anzeigen (show_price) an Der niedrigste Preis der aktiven Varianten im Channel, formatiert in der Standardsprache und Basiswährung des Channels. Ein Produkt ohne Preis bekommt eine Karte ohne Preis
Template-Version (template_version) 1 Teil des Inhalts-Hashes jeder Karte. Die Checkbox "Karten neu erzeugen" erhöht sie beim Speichern

Eine Karte wird in der Standardsprache des Channels geschrieben: Es gibt eine Karte pro Produkt und Channel, nicht eine pro Seitensprache. Der Bildschirm hat einen Button "Vorschau": eine Beispielkarte des ersten aktiven Produkts des Channels (oder einer Kategorie, wenn es kein Produkt gibt) mit den gespeicherten Einstellungen. Sie wird direkt gerendert und nie gecacht.

Einstellungen im Projekt (Format, Quellen, Limits)

Alle Schlüssel sind optional und stehen unter mmd_sylius_seo_suite_pro.og_image:

Einstellung Standard Beschreibung
og_image.format auto auto, webp, jpeg oder png
og_image.quality 85 Qualität für WebP und JPEG, 1 bis 100
og_image.cache_dir %kernel.project_dir%/var/mmd_seo_og Ablageort der Karten, außerhalb des Public-Verzeichnisses
og_image.images_path %kernel.project_dir%/public/media/image Verzeichnis, aus dem lokale Produktbilder gelesen werden
og_image.public_dir %kernel.project_dir%/public Public-Verzeichnis, aus dem ein Logo-Pfad gelesen wird
og_image.font_regular mitgeliefertes DejaVu Sans TrueType-Schrift für die Texte
og_image.font_bold mitgeliefertes DejaVu Sans Bold TrueType-Schrift für Name und Preis
og_image.remote_hosts [] Allow-List für CDN-Hosts; leer bedeutet jeder Host mit öffentlicher Adresse
og_image.fetch_timeout 5 Sekunden, die ein Remote-Bild zum Antworten haben darf (zum Herunterladen die doppelte Zeit)
og_image.render_budget 15 Sekunden, die eine Karte für das Laden von Bild und Logo zusammen hat
og_image.max_source_bytes 8388608 Größtes Quellbild in Byte, lokal oder remote
og_image.max_source_pixels 25000000 Größtes Quellbild in Pixeln; zusätzlich höchstens 8000 Pixel pro Seite

Eine realistische Konfiguration für einen Shop, dessen Bilder von einem CDN kommen:

# config/packages/mmd_sylius_seo_suite_pro.yaml
mmd_sylius_seo_suite_pro:
    og_image:
        format: auto
        quality: 85
        cache_dir: '%kernel.project_dir%/var/mmd_seo_og'
        remote_hosts: ['cdn.example.com', '*.example.org']
        fetch_timeout: 5
        render_budget: 15
        max_source_bytes: 8388608
        max_source_pixels: 25000000

Die Einträge von remote_hosts sind reine Hostnamen (cdn.example.com oder *.example.org), ohne Schema und Pfad. Alles andere lehnt die Konfiguration ab.

Wie eine Karte ausgeliefert wird

Beim Aufbau einer Seite wird nie gerendert. Eine Seite braucht nur die URL der Karte, und die wird aus Metadaten berechnet, ohne eine Datei zu lesen, ein Bild abzurufen oder etwas zu zeichnen:

/seo/og/<channel>/<type>/<code>-<hash>.<ext>      z. B. /seo/og/shop/product/SOFA-1-3f9c...e1.webp

Der Hash ist ein sha256 (auf 32 Hex-Zeichen gekürzt) über alles, was das Aussehen der Karte verändert: die Layout-Version des Plugins, die Template-Version des Channels, den Typ, den Namen, den Preistext, die Identität des Bildes (gespeicherter Pfad oder URL), das Logo, die drei Farben, den Slogan, das Format, die Qualität und den Inhalt der Schriftdateien.

graph TD
    A["Seitenaufbau: nur Hash berechnen, nichts zeichnen"] --> B["Seite nennt Karten-URL mit Hash"]
    B --> C["Crawler fragt die Karten-URL an"]
    C --> D{"Hash und Format aktuell?"}
    D -->|"nein, Subjekt hat eine Karte"| E["301 auf die aktuelle URL, Cache 300 Sekunden"]
    D -->|"Subjekt oder Channel unbekannt oder aus"| F["404"]
    D -->|"ja"| G{"Karte im Cache vorhanden?"}
    G -->|"ja"| H["Datei senden: immutable, ETag, 304 bei Revalidierung"]
    G -->|"nein"| I{"Render-Lock frei innerhalb von 2 Sekunden?"}
    I -->|"nein"| J["503 mit Retry-After 5"]
    I -->|"ja"| K["Rendern, atomar speichern, senden"]
    K -->|"Bild oder Logo vorübergehend nicht ladbar"| L["Degradierte Karte: 300 Sekunden Cache, nicht gespeichert"]
    K -->|"Rendern scheitert endgültig"| M["302 auf das normale Produktbild, sonst 404"]

Die Route berechnet den aktuellen Hash des Subjekts, niemand kann den Shop also dazu bringen, Karten für Hashes zu rendern, die er nie veröffentlicht hat. Im Einzelnen:

Eine Karte wird nur auf einer Seite angekündigt, wenn die Route sie auch ausliefern würde: Ein Produkt muss aktiv und im Channel verkauft sein, eine Kategorie muss dort eine Seite haben. Die Route gehört zu den Shop-Routen und wird auf oberster Ebene importiert (siehe Installation). Eine Seite mit Karte bekommt zusätzlich og:image:width, og:image:height und og:image:type.

Ändern sich Name, Preis oder Bild eines Produkts oder eine Einstellung, ändert sich der Hash: Die neue URL erscheint auf der Seite, die alte leitet dorthin um, und die alte Datei des Produkts wird beim Schreiben der neuen gelöscht.

Ein Bild, das unter demselben Pfad oder derselben URL ausgetauscht wurde, lässt sich nicht bemerken, ohne es zu lesen, und das darf ein Seitenaufruf nicht. Nutze "Karten neu erzeugen" nach dem Austausch eines Logos oder eines CDN-Bildes hinter unveränderter URL. (Sylius legt ein neu hochgeladenes Produktbild unter einem neuen Pfad ab, dieser Fall wird automatisch erkannt.)

Degradierte Karten

Lässt sich ein Quellbild nicht laden, kommt trotzdem eine Karte, nur ohne das Bild; der Fehler wird mit Karte und Quelle geloggt. Kann der Fehler beim nächsten Versuch weg sein oder hängt er an Umgebung und Konfiguration statt an der Karte, ist die Karte degradiert: Sie wird mit Cache-Control: public, max-age=300 (nicht immutable) ausgeliefert und nicht gespeichert, sodass die nächste Anfrage die vollständige Karte zeichnet und nur diese ein Jahr lang gehalten wird. Dazu zählen ein Timeout, ein Verbindungsfehler, eine HTTP-Antwort 5xx, 408 oder 429, ein aufgebrauchtes Zeitbudget, ein Host, der nicht auflöst (DNS), und ein Host oder eine Adresse, die die Adress-Policy ablehnt (nicht auf remote_hosts, oder eine Loopback-, private, Link-Local- oder reservierte Adresse).

Ein endgültiger Fehler (das Bild existiert nicht, ist kein Bild, ist zu groß, ein lokaler Pfad außerhalb der erlaubten Verzeichnisse, eine gespeicherte URL, die nicht http(s) ist oder Zugangsdaten enthält) ergibt dagegen eine normale, gecachte Karte ohne Bild.

Lokale CDN-Setups (ein Media-Server auf Loopback oder im Docker-Netz, etwa in einem Dev-Shop): Solche Adressen werden absichtlich abgelehnt, die Karte bleibt ohne Bild und wird bei jedem Request neu gezeichnet, statt ein Jahr gehalten zu werden. Abhilfe: Lass die Bilder von einem Host mit öffentlicher Adresse kommen (und, falls Du die Allow-List nutzt, auf og_image.remote_hosts). Die Allow-List schränkt die Hosts nur ein, sie erlaubt nie eine private oder Loopback-Adresse.

Format, WebP und Schriften

Erzwingst Du format: webp auf einem Server, dessen GD kein WebP schreiben kann, wird das Format nicht stillschweigend geändert: Das Rendern scheitert mit einer Meldung, die das sagt. Der Besucher bekommt einen 302 oder ein 404, die Meldung steht im Log, im Console-Command und in der Admin-Vorschau. Setze dann format auf auto oder jpeg.

Console und Webserver sollten dieselbe GD-Konfiguration haben. Mit auto und unterschiedlichem WebP-Support schreiben beide Karten in verschiedenen Formaten (und damit mit verschiedenen Hashes). prune lässt eine Karte im anderen Format in Ruhe, und das nächste Schreiben der Karte des Subjekts entfernt sie. Setze og_image.format explizit, wenn beide Seiten abweichen.

Quellen für Bilder: lokale Dateien und CDN-URLs

Ein Produkt- oder Kategoriebild ist entweder ein Pfad relativ zu images_path oder eine absolute URL (ein CDN). Beides wird beim Rendern einer Karte geladen.

Lokale Dateien oder CDN

Lokale Dateien

Ein lokaler Pfad wird mit realpath aufgelöst und muss innerhalb von images_path landen (ein Logo innerhalb von public_dir): kein .., kein Symlink aus dem Verzeichnis heraus, kein absoluter Pfad. Mehr Konfiguration brauchst Du nicht.

CDN-URLs

Eine URL muss http(s) sein, ohne Zugangsdaten, und auf der remote_hosts-Allow-List stehen, wenn es eine gibt.

SSRF-Schutz: Der Host wird aufgelöst und jede Adresse muss öffentlich sein (kein Loopback, kein privater, Link-Local-, Carrier-Grade-NAT-, Dokumentations- oder reservierter Bereich, IPv4-gemappte IPv6-Adressen werden nach der IPv4-Adresse beurteilt). Die geprüfte Adresse wird für den Request festgenagelt, Redirects folgt der Loader von Hand (höchstens 3) und prüft jeden Sprung erneut. Weder eine zweite DNS-Antwort noch ein Redirect kann ins Netz des Shops führen.

Der Loader hat einen eigenen HTTP-Client (Service mmd_sylius_seo_suite_pro.og_image.http_client), nicht den gemeinsamen des Frameworks. Er nutzt nie einen Proxy aus der Umgebung (HTTP_PROXY, HTTPS_PROXY), denn ein Proxy löst den Hostnamen selbst auf, und die geprüfte, festgenagelte Adresse wäre bedeutungslos. Ein Shop, der das CDN nur über einen Proxy erreicht, muss diesen Service bewusst ersetzen.

Grenzen beim Laden eines Bildes

Console-Commands

bin/console mmd:seo-suite-pro:og-image:generate [--channel=CODE] [--type=product|taxon] [--subject=CODE] [--force]
bin/console mmd:seo-suite-pro:og-image:prune [--days=0] [--no-orphans] [--dry-run]

generate rendert die Karten vorab, damit der erste Besucher (oder Crawler) nicht warten muss. Produkte und Kategorien werden seitenweise (100 pro Seite) gelesen und der Entity Manager wird nach jeder Seite geleert. Pro Channel und Typ erscheint ein Fortschritt mit der Zahl gerenderter, unveränderter, übersprungener und fehlgeschlagener Karten. Eine vorhandene Karte bleibt unberührt, außer Du gibst --force an. --type akzeptiert neben product und taxon auch den Typ eines eigenen Providers, --subject braucht --type. Channels mit ausgeschalteten Karten werden übersprungen. Der Exit-Code ist 1, wenn eine Karte fehlschlug (auch eine degradierte zählt als fehlgeschlagen und wird nicht gespeichert; der Command nennt den Grund: vorübergehender Fehler, Host löst nicht auf oder von der Adress-Policy abgelehnt), und 2 bei unbekanntem Channel oder Typ.

prune löscht, außer mit --no-orphans, Karten, auf die kein Produkt und keine Kategorie mehr zeigt (Subjekt gelöscht oder deaktiviert, Karte ausgeschaltet, inzwischen ein anderer Hash im aktuellen Format), sowie übrig gebliebene temporäre und Lock-Dateien abgestürzter Prozesse. Standardmäßig sieht es nicht aufs Alter. --days=N löscht zusätzlich Karten, die seit N Tagen nicht geschrieben wurden, aktuelle eingeschlossen (sie werden bei Bedarf neu gezeichnet). Mit --dry-run wird nur gezählt, was gelöscht würde.

Zuerst ein Probelauf, dann der echte Lauf:

bin/console mmd:seo-suite-pro:og-image:prune --dry-run
bin/console mmd:seo-suite-pro:og-image:prune

Karten nach einem Katalog-Import vorab erzeugen und wöchentlich aufräumen, als Cronjob:

15 4 * * * cd /var/www/shop && bin/console mmd:seo-suite-pro:og-image:generate --no-interaction
30 4 * * 0 cd /var/www/shop && bin/console mmd:seo-suite-pro:og-image:prune --no-interaction

Läuft Dein Import zu einer anderen Zeit, hänge og-image:generate direkt hinter den Import-Job.

Eigene Typen

Implementiere Mmd\SyliusSeoSuiteProPlugin\OgImage\OgImageProviderInterface (oder erweitere AbstractOgImageProvider). Jeder Service, der es implementiert, wird automatisch mit mmd_sylius_seo_suite_pro.og_image_provider getaggt. Ein Provider nennt den Typ, der in der URL landet (getType()), findet ein Subjekt anhand seines Codes (find()) und beschreibt es (describe()) als OgImageSpec: Titel, Preistext, Bildquelle und eine optionale Zusatzzeile. describe() läuft beim Seitenaufbau und darf keine Datei lesen und das Netz nicht nutzen. Das Beispiel und die Details stehen unter Erweitern.

Schritte im Host-Projekt

  1. Nach einem Update des Plugins mmd:seo-suite-pro:setup ausführen (siehe Installation): Es legt mmd_sylius_seo_suite_pro_og_image_settings an. Ohne die Tabelle ist das Feature aus und alles andere unverändert (einmal pro Prozess wird eine Warnung geloggt). Nur der Admin-Bildschirm für die OG-Bilder braucht die Tabelle.
  2. Sicherstellen, dass die Shop-Routen auf oberster Ebene importiert sind: Die Karten-Route gehört dazu.
  3. Das Cache-Verzeichnis schreibbar und dauerhaft machen, das Feature pro Channel einschalten und, wenn Du magst, mmd:seo-suite-pro:og-image:generate laufen lassen.

Bei Problemen hilft Fehlerbehebung.

Shop API / Headless

Die Karten-URL ist Teil derselben SEO-Daten, die der Twig-Hook rendert und die Shop API ausliefert. Die API gibt die URL in openGraph.image und twitterCard.image zurück und ergänzt imageWidth, imageHeight und imageType in openGraph und twitterCard. Diese drei Schlüssel existieren nur, wenn eine Karte verwendet wird, ein Shop ohne Karten bekommt also die Antwort, die er schon immer bekam. Die Bild-Route selbst ist ein HTTP-Endpunkt und hat keine API-Resource.

Grenzen