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.
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 vorhandene Karte wird unverändert gesendet:
Cache-Control: public, max-age=31536000, immutable, dazu einETag(der Hash) und304bei Revalidierung. - Fehlt die Karte, wird sie einmal unter einem Lock gerendert, atomar geschrieben (temporäre Datei plus Rename) und gesendet.
- Ein veralteter Hash eines Subjekts mit Karte (eine Seite, die vor einer Änderung von Name, Preis oder Bild gecacht wurde) wird mit einem 301 auf die aktuelle Karten-URL beantwortet (
Cache-Control: public, max-age=300, serverseitig aus dem Subjekt gebaut, nie aus dem Request). Der Redirect rendert nichts. - Ungültige Parameter, ein unbekanntes Subjekt, ein Subjekt ohne Karte, ein ausgeschalteter Channel und ein ausgeschalteter Kartentyp ergeben ein schlichtes 404.
- Ein Prozess rendert eine Karte nur einmal gleichzeitig. Eine Anfrage, die die Karte gerade in Arbeit findet, wartet höchstens zwei Sekunden und bekommt dann 503 mit
Retry-After: 5, statt sich hinter dem Lock zu stauen. - Kann eine Karte nicht erzeugt werden (kein Speicherplatz, GD-Problem), wird der Besucher der Karten-URL per 302 auf das normale Produktbild geleitet, oder er bekommt ein 404, wenn es keins gibt. Der Grund landet als Warnung im Log, einmal pro Prozess und Problem.
- Verschwindet eine Datei zwischen Prüfung und Senden (Prune, Deployment), wird sie einmal neu gezeichnet.
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
autoschreibt WebP, wo GD es schreiben kann, sonst JPEG. Ein PHP-Build, dessen GD ohne WebP kompiliert wurde (gd_info()['WebP Support']istfalse, manche Docker-Images), liefert also JPEG-Karten. Der Host kann WebP beim Bauen von GD mit--with-webpaktivieren. Das Format ist Teil des Hashes und der URL (.webp,.jpg,.png).- Die Karte hat immer die feste Größe 1200x630.
- Das Plugin bringt eine eigene Schrift mit (DejaVu Sans und DejaVu Sans Bold in
Resources/fonts/, mit der Lizenz inResources/fonts/LICENSE-DejaVu.txt), weil ein typischer Container keine System-Schriften hat. Sie deckt lateinische, kyrillische und griechische Zeichen ab. Richtefont_regularundfont_boldauf eine beliebige andere TrueType-Datei, um das Aussehen zu ändern. Eine fehlende Datei ist ein Konfigurationsfehler mit dem Namen der Option in der Meldung. cache_dirliegt außerhalb des Public-Verzeichnisses, weil die Karten über die Route ausgeliefert werden. Er muss für den Webserver schreibbar sein und sollte Deployments überleben (ein gemeinsamesvar/-Verzeichnis). Geht er verloren, werden die Karten bei Bedarf einfach neu gerendert.
Erzwingst Du
format: webpauf 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 dannformataufautooderjpeg.
Console und Webserver sollten dieselbe GD-Konfiguration haben. Mit
autound unterschiedlichem WebP-Support schreiben beide Karten in verschiedenen Formaten (und damit mit verschiedenen Hashes).prunelässt eine Karte im anderen Format in Ruhe, und das nächste Schreiben der Karte des Subjekts entfernt sie. Setzeog_image.formatexplizit, 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
- Content-Type
image/jpeg,image/png,image/gifoderimage/webp. Ein WebP-Quellbild wird nur akzeptiert, wenn GD WebP lesen kann. fetch_timeoutSekunden zum Antworten, der Download wird abgebrochen, sobald ermax_source_bytesüberschreitet. Bild und Logo einer Karte teilen sich zusätzlich das Zeitbudgetrender_budget.- Die Pixelmaße werden aus dem Dateikopf gelesen, bevor etwas dekodiert wird. Mehr als
max_source_pixelsoder mehr als 8000 Pixel pro Seite wird abgelehnt, ebenso ein Bild, das nicht in den noch freienmemory_limitpassen würde.
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
- Nach einem Update des Plugins
mmd:seo-suite-pro:setupausführen (siehe Installation): Es legtmmd_sylius_seo_suite_pro_og_image_settingsan. 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. - Sicherstellen, dass die Shop-Routen auf oberster Ebene importiert sind: Die Karten-Route gehört dazu.
- Das Cache-Verzeichnis schreibbar und dauerhaft machen, das Feature pro Channel einschalten und, wenn Du magst,
mmd:seo-suite-pro:og-image:generatelaufen 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
- Kein Text von rechts nach links und keine komplexen Schriftsysteme (Arabisch, Hebräisch und indische Schriften brauchen Shaping, das GD nicht kann).
- Die mitgelieferte Schrift hat keine Zeichen für Chinesisch, Japanisch oder Koreanisch. Konfiguriere dafür eine andere Schrift und prüfe das Ergebnis selbst.
- Keine Animation, ein Layout mit Deinen Farben, Deinem Logo und Slogan, keine Template-Sprache.
- Die Karte hat kein eigenes
og:image:alt(es wird der Seitentitel genutzt). - Eine Karte pro Subjekt und Channel in der Standardsprache des Channels.