Überblick
Das Problem
Eine Contao-Website liefert ihre Inhalte als HTML aus: Überschriften, Absätze, Bilder, Listen. Ein Mensch erkennt darin sofort, dass die Zahl unter dem Firmennamen eine Telefonnummer ist, dass „12. März, 19:00 Uhr“ ein Termin ist und dass der Kasten am Seitenende die Öffnungszeiten enthält. Eine Suchmaschine und erst recht eine KI-Antwortmaschine sehen zunächst nur Text. Damit sie dieselben Schlüsse ziehen, braucht es eine zweite, maschinenlesbare Fassung derselben Angaben — strukturierte Daten nach schema.org, üblicherweise als JSON-LD im Kopf der Seite.
Wer diese Fassung ohne Werkzeug pflegt, pflegt jede Angabe zweimal:
- Jede Änderung zweimal einpflegen. Die neue Telefonnummer steht im Fußzeilen-Inhaltselement und im JSON-LD-Block. Wer nur das eine ändert, liefert Suchmaschinen wochenlang die alte Nummer aus.
- Handgeschriebene Blöcke veralten unbemerkt. Ein JSON-LD-Schnipsel im Seitenlayout wird beim Redesign mitkopiert, nicht mitgeprüft. Fehler in strukturierten Daten sind unsichtbar: Die Seite sieht weiterhin richtig aus.
- Jede Nachricht und jeder Termin wäre Einzelarbeit. Für Artikel, Veranstaltungen und FAQ-Einträge müsste die Redaktion pro Datensatz von Hand auszeichnen, was Contao längst strukturiert in der Datenbank hält.
- Mehrere lose Blöcke widersprechen sich. Werden Organisation,
Seite und Artikel in getrennten
<script>-Elementen ausgegeben, erkennt keine Maschine, dass der Artikel von genau dieser Organisation stammt. Die Angaben stehen nebeneinander statt in Beziehung. - Vorlagen fressen die Ausgabe. Wer den JSON-LD-Block über
TL_HEADeinhängt, verliert ihn stillschweigend, sobald ein eigenesfe_page-Template den zugehörigen Insert-Tag weglässt — was bei zugekauften Themes und Page-Buildern regelmäßig vorkommt. - Niemand kontrolliert das Ergebnis. Ob die Auszeichnung tatsächlich stimmt, sieht man erst in einem externen Validator, und dafür muss man wissen, welche URL man dort einwerfen soll.
Am härtesten trifft das Redaktionen ohne eigene Entwicklung: Sie sollen für gute Auffindbarkeit sorgen, können aber nicht kontrollieren, ob das, was sie eintragen, überhaupt in maschinenlesbarer Form ankommt.
Die Lösung
Das Grundprinzip in einem Satz: Die Erweiterung erzeugt die strukturierten
Daten bei jedem Seitenaufruf aus dem, was ohnehin schon in Contao gepflegt ist, und
gibt sie als einen einzigen, in sich verlinkten @graph aus.
Es gibt keinen zweiten Pflegeort. Der Organisationsname kommt von der Startseite,
der Seitentitel von der Seite, der Artikel aus tl_news, der Termin aus
tl_calendar_events, die Fragen aus tl_faq. Die einzelnen
Bausteine — sogenannte Knotenlieferanten — schreiben in denselben Graphen und
verweisen über @id aufeinander, statt sich zu wiederholen. Eingefügt wird
das Ergebnis unmittelbar vor </head>, und zwar in der fertigen
Antwort statt über TL_HEAD, damit kein Template es verschlucken kann.
| Aufgabe | Von Hand gepflegt | Mit diesem Paket |
|---|---|---|
| Telefonnummer ändern | Inhaltselement und JSON-LD-Block anfassen | Feld auf der Startseite ändern |
| Neue Nachricht veröffentlichen | Article-Auszeichnung von Hand ergänzen | Geschieht automatisch beim Veröffentlichen |
| Neuer Termin | Event-Block schreiben, Datumsformat nachschlagen | Geschieht automatisch; Status und Teilnahmeart optional |
| FAQ auszeichnen | Jede Frage einzeln als Question notieren | Alle veröffentlichten Fragen der Leseseite automatisch |
| Zusammenhang Artikel ↔ Herausgeber | Nicht vorhanden oder von Hand dupliziert | Verweis über @id im selben Graphen |
| Ergebnis kontrollieren | URL suchen, in externen Validator kopieren | Backend-Modul zeigt das JSON-LD und verlinkt beide Validatoren |
| Sonderfall auszeichnen (z. B. Produkt) | Eigener Block, unverbunden | Feld Eigenes JSON-LD, fließt in denselben Graphen ein |
Den oben genannten Kosten stehen damit konkrete Mechanismen gegenüber:
- Doppelte Pflege entfällt, weil Organisation, WebSite und WebPage aus vorhandenen Feldern gebaut werden.
- Veraltete Blöcke entfallen, weil nichts gespeichert wird — der Graph entsteht pro Aufruf neu.
- Einzelarbeit an Datensätzen entfällt durch die Lieferanten für NewsArticle, Event und FAQPage.
- Widersprüchliche Blöcke entfallen, weil es genau einen
<script type="application/ld+json">pro Seite gibt. - Der Verlust durch eigene Templates entfällt durch die Einfügung in die Antwort.
- Die fehlende Kontrolle beantwortet das Backend-Modul Schema.org mit einer Vorschau je Seite.
Wann das Paket passt
| Situation | Einschätzung |
|---|---|
| Unternehmenswebsite mit Contao 5.3+, Adresse, Kontaktdaten, Nachrichten | Passt. Der Regelfall, für den das Paket gebaut ist. |
| Lokales Geschäft mit Öffnungszeiten und Anfahrt | Passt. Organisationstyp Lokales Unternehmen ergänzt Geokoordinaten, Öffnungszeiten und Preisniveau. |
| Redaktionelle Website mit vielen Artikeln | Passt. Jede Nachrichten-Detailseite bekommt Autor, Daten und Herausgeber ohne Zutun. |
| FAQ-Bereich, der in KI-Antworten auftauchen soll | Passt. Alle veröffentlichten Fragen der Leseseite werden als FAQPage ausgegeben. |
| Sie brauchen Produkt-, Rezept- oder HowTo-Auszeichnung | Teilweise. Dafür gibt es keine eigenen Felder; es geht über Eigenes JSON-LD, also von Hand — dann aber im selben Graphen. |
Intranet, Testsystem, Betrieb unter localhost oder einer IP-Adresse | Passt nicht. Solche Installationen lassen sich nicht lizenzieren und geben keine strukturierten Daten aus. |
| Sie wollen ausgehende Verbindungen der Installation vollständig unterbinden | Passt nicht. Für Aktivierung und Betrieb wird www.v-t.one per HTTPS kontaktiert; das lässt sich nicht abschalten. |
| Contao 4.13 oder älter | Passt nicht. composer.json schließt Versionen unter 5.3 ausdrücklich aus. |
Teil 1 — Einrichtung
Der Weg von der leeren Installation bis zur ersten sichtbaren Ausgabe:
- Voraussetzungen prüfen.
- Vor der Installation: Sicherung, Domain, Schreibrechte.
- Installation über den Contao Manager — der empfohlene Weg.
- Alternativ: Installation über Composer.
- Installation überprüfen.
- Lizenz aktivieren — ohne sie bleibt die Ausgabe aus.
- Websiteweite Angaben einrichten.
- Erste Ausgabe prüfen.
Voraussetzungen
| Komponente | Anforderung | Bemerkung |
|---|---|---|
| PHP | ^8.2 | 8.2, 8.3 und 8.4 |
| Contao | contao/core-bundle ^5.3 | Ältere Versionen sind über conflict ausdrücklich ausgeschlossen |
| Symfony | ^6.4 || ^7.0 | HttpFoundation, HttpKernel, HttpClient, EventDispatcher, SecurityBundle, Security-CSRF |
| Doctrine DBAL | ^3.6 || ^4.0 | Die Knotenlieferanten lesen direkt über DBAL |
| PSR-Log | ^2.0 || ^3.0 | |
PHP-Erweiterung json | zwingend | |
PHP-Erweiterung sodium | zwingend | Ohne sie lässt sich keine Lizenz prüfen; die Erweiterung bleibt dann inaktiv |
PHP-Erweiterung intl | empfohlen | Laut composer.json: „Needed to accept internationalised domain names when activating a licence“ |
| Ausgehendes HTTPS | zwingend | Ziel ist ausschließlich www.v-t.one |
| Erreichbarkeit unter echtem Hostnamen | zwingend | IP-Adressen und einteilige Namen wie localhost werden abgewiesen |
Die Contao-Bundles für Nachrichten, Termine und FAQ sind keine
Voraussetzung. Sie stehen nicht in require; die zugehörigen
Knotenlieferanten prüfen zur Laufzeit, ob die Tabellen vorhanden sind, und
liefern andernfalls einfach nichts.
| Optionales Contao-Bundle | Schaltet frei |
|---|---|
contao/news-bundle | Knoten NewsArticle / Article / BlogPosting und die Schema-Felder je Nachricht |
contao/calendar-bundle | Knoten Event und die Schema-Felder je Termin |
contao/faq-bundle | Knoten FAQPage und das Ausschlussfeld je Frage |
Vor der Installation
- Sicherung anlegen. Datenbank und Dateisystem. Die Installation
legt neue Felder in
tl_pagean und, sofern vorhanden, intl_news,tl_calendar_eventsundtl_faq. - Domain der Startseite klären. Die Lizenz wird exakt je Hostname
ausgestellt. Legen Sie vorher fest, unter welchem Namen die Website läuft —
example.comundwww.example.comsind zwei verschiedene Hosts. - Schreibrechte prüfen. Das Paket legt ein eigenes
Arbeitsverzeichnis unterhalb von
var/an. - Testsystem bevorzugen. Beachten Sie dabei, dass die Lizenz an den Hostnamen gebunden ist: Ein Testsystem unter anderer Domain benötigt eine eigene Lizenzierung oder gibt schlicht keine strukturierten Daten aus.
| Pfad | Zweck | Rechte |
|---|---|---|
var/schema-org/ | Arbeitsverzeichnis der Erweiterung | beschreibbar für Webserver- und CLI-Benutzer |
var/schema-org/state/ | Lizenzdatensatz, Siegel, Sidecar, Sperrdatei | wird mit 0700 angelegt |
var/schema-org/journal/ | Wiederholungsschutz für eingehende Aktualisierungen | wird mit 0700 angelegt |
Nicht über den Webserver erreichbar machen. Das Verzeichnis liegt
bewusst außerhalb des öffentlichen Bereichs und gehört nicht in die
Versionsverwaltung. Restriktive open_basedir- oder
umask-Einstellungen müssen den Zugriff auf var/ erlauben,
sonst schlägt bereits das Anlegen des Verzeichnisses fehl.
Installation über den Contao Manager
- Contao Manager öffnen und anmelden.
- Bereich Entdecken öffnen. Neue Pakete werden immer dort gesucht und hinzugefügt; der Bereich Pakete daneben zeigt unter Installierte Pakete nur, was bereits vorhanden ist. Entdecken ist zugleich die Startansicht des Managers.
- Über Pakete suchen nach
vtinnovations/schema-orgsuchen und beim Treffer Paket hinzufügen wählen. - Änderungen anwenden. Der Contao Manager installiert das Paket und aktualisiert den Autoloader.
- Bereich Systemwartung → Datenbank-Migrationen und -Backups → Datenbank prüfen; die angezeigten Datenbank-Änderungen bestätigen.
- Unter Systemwartung den Anwendungs-Cache leeren (in der Navigation auch als Cache erneuern geführt).
Der Contao Manager duzt in seinen eigenen Meldungen, etwa: „Dieses Paket wird installiert, wenn du die Änderungen anwendest.“ Das ist die Originalformulierung der Oberfläche und keine Abweichung von der Ansprache dieser Dokumentation.
Installation über Composer
Der gleichwertige Weg auf der Kommandozeile:
composer require vtinnovations/schema-org
Anschließend Datenbank aktualisieren und Cache leeren:
vendor/bin/contao-console contao:migrate
vendor/bin/contao-console cache:clear
Bei einer Managed Edition, oder wenn die Verzeichnisstruktur nach der Installation neu aufgebaut werden soll:
vendor/bin/contao-console contao:setup
Aktualisierung auf eine neuere Version:
composer update vtinnovations/schema-org
vendor/bin/contao-console contao:migrate
vendor/bin/contao-console cache:clear
Nie eine ZIP-Datei über ein bestehendes Verzeichnis entpacken. Dabei bleiben Dateien einer Vorversion liegen, die Composer bei einem regulären Update entfernt hätte. Installieren und aktualisieren Sie ausschließlich über Composer oder den Contao Manager.
Installation überprüfen
Dass das Bundle registriert ist, zeigt am schnellsten die Routenliste. Registriert das Paket seine Route, ist auch der Contao-Manager-Plugin-Einstieg gelaufen:
vendor/bin/contao-console debug:router | grep schema-org
Erwartete Zeile:
vtinnovations_schema_org_package_intake ANY ANY ANY /rest/api/v1/schema-org-license-updater
Im Backend erscheinen nach dem Cache-Leeren:
| Ort | Was dort auftaucht |
|---|---|
| Navigationsgruppe Schema.org → Schema.org | Das Vorschaumodul. Beschreibung: „JSON-LD-Vorschau und Validierung (die Lizenz wird in den Einstellungen verwaltet)“ |
| System → Einstellungen | Ganz oben die Gruppe V-T.ONE Licence management mit dem Abschnitt Schema.org |
| Seitenstruktur, Startpunkt einer Webseite | Legende Schema.org / Strukturierte Daten mit den websiteweiten Feldern |
| Seitenstruktur, jede andere Seite | Dieselbe Legende mit den vier Übersteuerungsfeldern |
| Nachricht, Termin, FAQ-Frage | Legende Schema.org je Datensatz |
Die Legenden werden eingeklappt angelegt. Wenn Sie die Felder nicht sehen, klappen Sie Schema.org / Strukturierte Daten beziehungsweise Schema.org im Bearbeitungsformular auf.
Lizenz aktivieren
Das Produkt wird als dauerhaft kostenfreie Ausgabe verteilt, benötigt aber einen ausgestellten und aktivierten Schlüssel. Ohne aktive Lizenz werden keine strukturierten Daten ausgegeben; alles andere — Konfiguration, Backend, Website — verhält sich unverändert.
- Domain auf der Startseite eintragen. Seitenstruktur → Startpunkt einer Webseite → Feld Domainname. Die Erweiterung liest die konfigurierten Hosts aus allen Startseiten. Anschließend Cache leeren.
- System → Einstellungen öffnen. Die Gruppe V-T.ONE Licence management steht oberhalb der Contao-eigenen Legenden; darin trägt dieser Abschnitt die Überschrift Schema.org mit dem Hinweis „Lizenz für diese Installation aktivieren, aktualisieren oder entfernen.“
- Den Schlüssel in das Feld Lizenzschlüssel eintragen
(Platzhalter
XXXXX-XXXXX-XXXXX-XXXXX) und Lizenz prüfen und aktivieren wählen. - Bei Erfolg meldet Contao „Die Lizenz wurde aktiviert.“, und der Abschnitt zeigt „Lifetime-Free-Lizenz aktiv. Alle Funktionen freigeschaltet.“
Die Hilfe am Schlüsselfeld lautet wörtlich: „Schlüssel eingeben und aktivieren. Bereits lizenziert? Nach einer Verlängerung oder einem Domainwechsel genügt ‚Lizenz aktualisieren‘ — der Schlüssel muss nicht erneut eingegeben werden.“
Ist auf keiner Startseite ein Domainname hinterlegt, verwendet die Erweiterung ersatzweise den aktuellen, von Symfony geprüften Hostname der Anfrage. Für ein vorhersagbares Ergebnis sollten Sie den Domainnamen trotzdem setzen — nur dann ist die Menge der Hosts stabil, für die die Lizenz gelten muss.
Details zu den einzelnen Schaltflächen und Zuständen stehen unter Lizenzverwaltung im Detail.
Websiteweite Angaben einrichten
Öffnen Sie Seitenstruktur → Ihre Startseite → Legende Schema.org / Strukturierte Daten. Ein sinnvolles Minimum:
- Organisationstyp festlegen. Voreingestellt ist Organisation; für ein Ladengeschäft, eine Praxis oder ein Restaurant Lokales Unternehmen (mit Adresse/Öffnungszeiten).
- Name der Organisation eintragen — oder leer lassen, dann wird der Titel der Startseite verwendet.
- Logo auswählen. Es wird als
logoundimageder Organisation ausgegeben. - Telefon, E-Mail und die Adressfelder füllen, soweit sie öffentlich sind.
- sameAs (Profil-URLs): je eine Zeile pro Social-Media-, Wikipedia- oder Profil-URL.
- Bei einem lokalen Unternehmen zusätzlich Breitengrad und Längengrad, Öffnungszeiten und Preisniveau.
- Such-URL (SearchAction) eintragen, wenn die Website eine
Suchergebnisseite hat — mit
{search_term_string}als Platzhalter. Der WebSite-Knoten ist bereits standardmäßig aktiv.
Die vollständige Feldliste mit allen Hilfetexten und Voreinstellungen steht unter Organisation und lokales Unternehmen.
Erste Ausgabe prüfen
- Cache leeren.
- Eine beliebige Frontend-Seite aufrufen und den Quelltext ansehen. Kurz vor
</head>steht ein einzelnes Element<script type="application/ld+json">. - Alternativ und bequemer: Backend → Schema.org → JSON-LD-Vorschau, dort die Seite auswählen. Das Modul zeigt das erzeugte JSON formatiert an und verlinkt Google Rich Results Test und schema.org-Validator mit der passenden URL.
Bleibt der Quelltext leer, hilft die Tabelle unter Fehlerbehebung weiter.
Teil 2 — Funktionen im Detail
Der zusammenhängende @graph
Ausgegeben wird pro Seite genau ein <script type="application/ld+json">
mit "@context": "https://schema.org" und einem @graph-Array.
Jeder Knoten trägt eine stabile @id, sodass andere Knoten ihn referenzieren
können, statt seine Angaben zu wiederholen.
| Knoten | @id | Woher |
|---|---|---|
| Organization / LocalBusiness | {basis}/#organization | Startseite |
| ImageObject (Logo) | {basis}/#logo | Feld Logo der Startseite |
| WebSite | {basis}/#website | Startseite |
| WebPage und Untertypen | {seiten-url}#webpage | aktuelle Seite |
| BreadcrumbList | {seiten-url}#breadcrumb | Seitenpfad |
| NewsArticle / Event | {seiten-url}#primaryentity | Datensatz der Detailseite |
| FAQPage | {seiten-url}#faqpage | FAQ-Kategorien mit dieser Leseseite |
| Eigene Knoten | frei | Feld Eigenes JSON-LD |
Die Verweise sind das eigentliche Produkt: WebPage.isPartOf zeigt auf
die WebSite, WebPage.about und
WebPage.publisher auf die Organization,
WebPage.breadcrumb auf die BreadcrumbList, und auf einer
Detailseite zeigt WebPage.mainEntity auf den Artikel, den Termin oder
die FAQ-Seite.
Wer zuerst schreibt, gewinnt. Existiert eine @id
bereits im Graphen, werden von einem späteren Beitrag nur die noch fehlenden Felder
ergänzt; vorhandene Werte bleiben stehen. So kann ein später laufender Lieferant
einen bestehenden Knoten anreichern, ihn aber nicht überschreiben.
Organisation und lokales Unternehmen
Der zentrale Entitätsknoten der Website. Konfiguriert wird er auf der Startseite in der Legende Schema.org / Strukturierte Daten.
| Feld | Hilfetext im Backend | Voreinstellung |
|---|---|---|
| Schema.org komplett deaktivieren | „Gibt für diese gesamte Website kein JSON-LD aus.“ | aus |
| Organisationstyp | „Bestimmt den Typ des zentralen Entitäts-Knotens.“ | Organisation |
| Name der Organisation | „Leer = Titel der Startseite wird verwendet.“ | leer |
| Logo | „Wird als logo/image der Organisation ausgegeben (ImageObject).“ | leer; erlaubt sind jpg, jpeg, png, gif, svg, webp |
| sameAs (Profil-URLs) | „Social-Media-/Wikipedia-/Profil-URLs, je eine pro Zeile.“ | leer |
| Telefon | „Wird als telephone und ContactPoint ausgegeben.“ | leer |
| „Wird als email und ContactPoint ausgegeben.“ | leer | |
| Straße und Hausnummer | „Straße und Hausnummer der Postanschrift.“ | leer |
| PLZ | „Postleitzahl der Postanschrift.“ | leer |
| Ort | „Ort bzw. Stadt der Postanschrift.“ | leer |
| Region/Bundesland | „Region, Bundesland oder Kanton der Postanschrift.“ | leer |
| Ländercode | „Zweistelliger ISO-Code, z. B. DE.“ | leer, Platzhalter DE |
| Breitengrad (Latitude) | „Nur für lokales Unternehmen.“ | leer |
| Längengrad (Longitude) | „Nur für lokales Unternehmen.“ | leer |
| Öffnungszeiten | „Je Zeile ein Eintrag im Format ‚Mo-Fr 09:00-18:00‘.“ | leer |
| Preisniveau | „z. B. €, €€, €€€. Nur für lokales Unternehmen.“ | leer, Platzhalter €€ |
Die Auswahl unter Organisationstyp:
| Option | Verhalten |
|---|---|
| Organisation | Knoten vom Typ Organization. Geokoordinaten, Öffnungszeiten und Preisniveau werden nicht ausgegeben, auch wenn sie gefüllt sind. |
| Lokales Unternehmen (mit Adresse/Öffnungszeiten) | Knoten vom Typ LocalBusiness, zusätzlich mit geo, openingHours und priceRange, soweit gefüllt. |
| Keine Organisation ausgeben | Kein Organisationsknoten. Andere Knoten verzichten dann auch auf publisher, about und organizer. |
Zusätzliche Regeln, die sich nicht aus den Feldern allein ergeben:
- Ist weder ein Name der Organisation gesetzt noch die Startseite betitelt, entfällt der Knoten vollständig.
- Sind Telefon oder E-Mail gefüllt, entsteht zusätzlich ein
ContactPointmitcontactType: customer service. - Aus den Adressfeldern entsteht eine
PostalAddress, sobald mindestens eines davon gefüllt ist. geowird nur geschrieben, wenn beide Koordinaten gefüllt sind.
Schema.org komplett deaktivieren wirkt sofort auf die gesamte Website — jede Seite, jeder Artikel, jeder Termin. Es ist der Notausschalter, nicht der Weg, eine einzelne Seite auszunehmen; dafür gibt es Schema für diese Seite deaktivieren.
WebSite-Knoten und Suchbox
| Feld | Hilfetext im Backend | Voreinstellung |
|---|---|---|
| WebSite-Knoten ausgeben | „Aktiviert den WebSite-Knoten (nötig für die Suchbox).“ | an |
| Such-URL (SearchAction) | „URL der Suchergebnisseite mit {search_term_string} als Platzhalter. Leer = keine Suchbox.“ | leer |
Der Platzhalter des Feldes zeigt die erwartete Form:
https://example.com/suche.html?keywords={search_term_string}
Ist die Such-URL gefüllt, erhält der WebSite-Knoten eine
potentialAction vom Typ SearchAction mit einem
EntryPoint und query-input: required name=search_term_string.
Der Name des WebSite-Knotens folgt dem Organisationsnamen; ist keiner
gesetzt, wird der Titel der Startseite verwendet.
Wird WebSite-Knoten ausgeben abgeschaltet, verlieren die
WebPage-Knoten ihren isPartOf-Verweis. Das ist zulässig,
nimmt der Auszeichnung aber einen Teil ihres Zusammenhangs. Abschalten lohnt sich
praktisch nur, wenn ein anderes Bundle bereits einen WebSite-Knoten
ausgibt.
WebPage und Speakable
Jede Frontend-Seite bekommt einen WebPage-Knoten. Auf jeder Seite
außer den Startseiten steht dafür die Legende Schema.org / Strukturierte
Daten mit vier Feldern zur Verfügung.
| Feld | Hilfetext im Backend | Voreinstellung |
|---|---|---|
| Schema für diese Seite deaktivieren | „Unterdrückt jegliches JSON-LD auf dieser Seite.“ | aus |
| WebPage-Typ | „Genauerer Seitentyp für Suchmaschinen/KI.“ | leer, wirkt wie WebPage |
| Speakable CSS-Selektoren | „Kommagetrennt. Markiert vorlesbare Bereiche für Sprach-/KI-Assistenten.“ | leer, Platzhalter h1, .intro |
| Eigenes JSON-LD | „Wird zusätzlich in den @graph eingefügt (ohne @context). Ein Objekt oder ein Array von Objekten.“ | leer |
Die Auswahl unter WebPage-Typ — leer entspricht Webseite (allgemein):
| Beschriftung | Ausgegebener Typ |
|---|---|
| Webseite (allgemein) | WebPage |
| Über-uns-Seite | AboutPage |
| Kontaktseite | ContactPage |
| Übersichtsseite (Liste/Sammlung) | CollectionPage |
| Profilseite | ProfilePage |
| FAQ-Seite | FAQPage |
| Frage-und-Antwort-Seite | QAPage |
| Detailseite (einzelnes Objekt) | ItemPage |
| Suchergebnisseite | SearchResultsPage |
| Kassenseite (Checkout) | CheckoutPage |
Woher die übrigen Angaben des Knotens stammen:
| Eigenschaft | Quelle |
|---|---|
name | Seitentitel der Seite; ist er leer, der Seitenname |
description | Feld Beschreibung der Seite, sofern gefüllt |
inLanguage | Sprache der Seite, ersatzweise die der Startseite; ist beides leer, entfällt die Angabe |
url | Aufgerufene URL der Seite |
dateModified | Der jüngere Wert aus dem Änderungszeitpunkt der Seite und dem der veröffentlichten Artikel auf ihr |
speakable | Feld Speakable CSS-Selektoren, als SpeakableSpecification mit cssSelector |
dateModified wird bewusst aus dem tatsächlich jüngsten Inhalt
gebildet — Seite oder veröffentlichter Artikel. Antwortmaschinen gewichten diese
Angabe stark, deshalb wird sie nicht künstlich aktuell gehalten.
BreadcrumbList
Die Brotkrumenliste entsteht ohne Konfiguration aus dem Seitenpfad der aktuellen
Seite. Jede Station wird zu einem ListItem mit position,
name (Seitenname) und item (absolute URL).
- Startseiten werden übersprungen — sie sind Wurzelknoten, keine Station.
- Eine Station, deren absolute URL sich nicht bilden lässt, wird ausgelassen.
- Bleiben weniger als zwei Stationen übrig, entfällt der Knoten ganz: Eine einelementige Brotkrume ist keine Information.
Nachrichten (NewsArticle)
Auf einer Nachrichten-Detailseite wird der Artikel zum Hauptgegenstand der Seite. Die Felder stehen im Datensatz unter der Legende Schema.org.
| Feld | Hilfetext im Backend | Voreinstellung |
|---|---|---|
| Schema deaktivieren | „Kein Article-JSON-LD für diese Nachricht.“ | aus |
| Article-Typ | „Leer = NewsArticle.“ | leer |
| Autor (Name) | „Überschreibt den Contao-Autor. Wird als Person ausgegeben.“ | leer |
| Autor (URL) | „Profil-/Über-mich-URL des Autors.“ | leer |
| Speakable CSS-Selektoren | „Kommagetrennt. Markiert vorlesbare Bereiche für Sprach-/KI-Assistenten.“ | leer, Platzhalter h1, .ce_text |
| Eigenes JSON-LD | „Zusätzliche Knoten für den @graph (ohne @context).“ | leer |
| Beschriftung unter Article-Typ | Ausgegebener Typ |
|---|---|
| Nachrichtenartikel | NewsArticle |
| Artikel (allgemein) | Article |
| Blogbeitrag | BlogPosting |
| Reportage | ReportageNewsArticle |
| Kommentar/Meinungsbeitrag | OpinionNewsArticle |
Automatisch übernommen werden:
| Eigenschaft | Quelle |
|---|---|
headline | Schlagzeile der Nachricht |
datePublished | Datum der Nachricht |
dateModified | Änderungszeitpunkt, mindestens jedoch das Veröffentlichungsdatum |
description | Teasertext ohne HTML, sofern vorhanden |
image | Das Bild der Nachricht, wenn Ein Bild hinzufügen aktiv und eine Datei gewählt ist |
author | Autor (Name), sonst der Name des Contao-Benutzers; als Person, mit url wenn Autor (URL) gefüllt ist |
publisher | Verweis auf den Organisationsknoten, sofern dieser ausgegeben wird |
isPartOf / mainEntityOfPage | Verweis auf den WebPage-Knoten |
Zusätzlich wird der WebPage-Knoten um
mainEntity ergänzt, das auf den Artikel zeigt.
Nur veröffentlichte Nachrichten werden ausgezeichnet. Der Datensatz wird über seinen Alias gesucht; eine Vorschau unveröffentlichter Inhalte liefert daher keinen Artikelknoten.
Termine (Event)
| Feld | Hilfetext im Backend | Voreinstellung |
|---|---|---|
| Schema deaktivieren | „Kein Event-JSON-LD für diesen Termin.“ | aus |
| Event-Status | „Leer = findet statt.“ | leer |
| Teilnahmeart | „Wie am Termin teilgenommen werden kann. Leer = vor Ort.“ | leer |
| Veranstaltungsort (Name) | „Überschreibt den Ort des Termins.“ | leer |
| Eigenes JSON-LD | „Zusätzliche Knoten für den @graph (ohne @context).“ | leer |
| Event-Status | Ausgegebener Wert |
|---|---|
| Findet statt | https://schema.org/EventScheduled |
| Verschoben (neuer Termin) | https://schema.org/EventRescheduled |
| Verschoben (offen) | https://schema.org/EventPostponed |
| Nach online verlegt | https://schema.org/EventMovedOnline |
| Abgesagt | https://schema.org/EventCancelled |
| Teilnahmeart | Ausgegebener Wert |
|---|---|
| Vor Ort | https://schema.org/OfflineEventAttendanceMode |
| Online | https://schema.org/OnlineEventAttendanceMode |
| Hybrid (vor Ort + online) | https://schema.org/MixedEventAttendanceMode |
Weitere Regeln:
eventStatuswird immer geschrieben. Bleibt das Feld leer, lautet der WertEventScheduled.eventAttendanceModewird nur bei ausdrücklicher Auswahl geschrieben.- Führt der Termin eine Uhrzeit, entsteht ein vollständiger Zeitstempel, sonst ein reines Datum.
- Ein
endDateentsteht nur, wenn das Ende nach dem Anfang liegt. - Als
locationdient Veranstaltungsort (Name), ersatzweise das Ortsfeld des Termins; ausgegeben wird einPlacemitname. - Wird ein Organisationsknoten ausgegeben, wird er als
organizerreferenziert. - Nur veröffentlichte Termine mit gültigem Startzeitpunkt werden ausgezeichnet.
FAQ (FAQPage)
Der FAQ-Knoten hängt nicht am einzelnen Datensatz, sondern an der Seite: Ausgewertet
werden alle FAQ-Kategorien, deren Leseseite die aufgerufene Seite ist. Jede
veröffentlichte Frage wird zu einem Question mit
acceptedAnswer, sortiert nach der Reihenfolge im Backend.
| Feld je Frage | Hilfetext im Backend | Voreinstellung |
|---|---|---|
| Aus FAQPage ausschließen | „Diese Frage nicht ins FAQPage-JSON-LD aufnehmen.“ | aus |
- Frage und Antwort werden von HTML befreit; nicht aufgelöste Insert-Tags in der
Form
{{…}}werden entfernt, damit keine Steuerzeichen in die strukturierten Daten gelangen. - Fragen ohne Text oder ohne Antworttext werden übersprungen.
- Bleibt keine Frage übrig, entfällt der
FAQPage-Knoten. - Andernfalls zeigt
WebPage.mainEntityauf dieFAQPage.
Der Knoten FAQPage und die Auswahl FAQ-Seite im Feld
WebPage-Typ sind zwei verschiedene Dinge. Ersterer enthält die Fragen,
Letzteres bestimmt nur den Typ des Seitenknotens. Beides zusammen ist möglich und
auf einer echten FAQ-Leseseite auch sinnvoll.
Eigenes JSON-LD
Für alles, was die automatischen Lieferanten nicht abdecken — Product,
HowTo, Recipe, Review und anderes — gibt es
das Feld Eigenes JSON-LD. Es steht auf jeder Seite (außer Startseiten)
sowie an Nachrichten und Terminen zur Verfügung.
Erwartet wird ein Objekt oder ein Array von Objekten, jeweils ohne
@context:
{
"@type": "Product",
"name": "Beispielprodukt",
"offers": {
"@type": "Offer",
"price": "49.00",
"priceCurrency": "EUR"
}
}
Oder mehrere Knoten auf einmal:
[
{ "@type": "Person", "@id": "https://example.com/#chef", "name": "A. Beispiel" },
{ "@type": "Recipe", "name": "Beispielgericht", "author": { "@id": "https://example.com/#chef" } }
]
| Verhalten | Erläuterung |
|---|---|
| Ungültiges JSON | Wird stillschweigend übergangen. Die Seite wird niemals abgebrochen und nichts wird gemeldet. |
Mitgeliefertes @context | Wird entfernt; der Graph liefert seinen eigenen. |
Knoten mit eigener @id | Kann von anderen eigenen Knoten referenziert werden und kann einen bestehenden Knoten um fehlende Felder ergänzen. |
Knoten ohne @id | Wird als eigenständiger Eintrag an den Graphen angehängt. |
| Feld am Datensatz | Wird nur auf der Detailseite ausgewertet, also dann, wenn der Datensatz über seinen Alias in der URL angesprochen wird. |
Es findet keine Validierung gegen schema.org statt. Ein Tippfehler im Typnamen fällt hier nicht auf — die Ausgabe erscheint, ist aber wertlos. Prüfen Sie eigene Knoten nach dem Speichern über die Vorschau und die dort verlinkten Validatoren.
Backend-Modul Schema.org
Das Modul in der Navigationsgruppe Schema.org ist reine Anzeige. Es erzeugt keine Daten, speichert nichts und enthält keine Lizenzfunktionen — den Lizenzschlüssel bekommt es nie zu sehen.
| Element | Beschriftung / Verhalten |
|---|---|
| Untertitel | „Strukturierte Daten (JSON-LD) für Suchmaschinen und KI-Antwortmaschinen.“ |
| Statuszeile bei aktiver Lizenz | „Lizenz aktiv“, dahinter der zugeordnete Hostname und der maskierte Schlüssel |
| Karte | „JSON-LD-Vorschau“ — „Zeigt die für eine Seite generierten strukturierten Daten und verlinkt zu den Validatoren.“ |
| Auswahlfeld | „Seite“, zunächst „— bitte wählen —“. Die Auswahl wird sofort übernommen; ohne JavaScript steht eine Schaltfläche „Anzeigen“ bereit. |
| Ergebnis | Zeile „URL“ mit Verweis auf die Seite, danach das formatierte JSON |
| Schaltflächen | „Google Rich Results Test“ und „schema.org-Validator“, beide mit der URL der gewählten Seite vorbelegt |
Die Auswahlliste enthält alle Seiten außer Startseiten und den Fehlerseiten 401, 403 und 404. Jeder Eintrag zeigt Titel, Seitentyp und ID.
Drei Meldungen können statt des JSON erscheinen:
| Meldung | Bedeutung |
|---|---|
| „Für diese Installation ist keine gültige Lizenz aktiv, daher werden keine strukturierten Daten ausgegeben.“ | Das gesamte Modul ist gesperrt. Daneben steht der Verweis „Lizenz in den Einstellungen verwalten“. |
| „Seite nicht gefunden.“ | Die gewählte Seite existiert nicht mehr. |
| „Für diesen Seitentyp lässt sich keine URL bilden.“ | Für die Seite lässt sich keine absolute URL erzeugen; ohne URL gibt es keine Vorschau. |
| „Für diese Seite wird kein Schema ausgegeben (deaktiviert oder keine Daten konfiguriert).“ | Der Graph ist leer — Website oder Seite deaktiviert, oder es ist nichts konfiguriert, woraus ein Knoten entstehen könnte. |
Lizenzverwaltung im Detail
Der Abschnitt unter System → Einstellungen ist die einzige Stelle, an der Lizenzdaten sichtbar sind oder verändert werden können. Es gibt kein zweites Modul, keine zweite Route und kein Feld an der Startseite dafür.
| Schaltfläche | Verhalten |
|---|---|
| Lizenz prüfen und aktivieren | Prüft den eingegebenen Schlüssel beim Lizenzserver, verifiziert die Antwort und speichert sie erst danach. |
| Lizenz aktualisieren | Holt den aktuellen Stand. Ohne Eingabe wird der gespeicherte Schlüssel verwendet; eine Eingabe ersetzt ihn. Schlägt der Aufruf fehl, bleibt die bisherige Lizenz unverändert bestehen. |
| Lizenz entfernen | Erfordert die Bestätigung der Rückfrage und versetzt die Installation sofort in den unlizenzierten Zustand. |
| Gefundenen Schlüssel der Vorversion aktivieren | Erscheint nur, wenn ein Schlüssel aus einer früheren Ausgabe gefunden wurde und noch nichts gespeichert ist. |
Die Rückfrage beim Entfernen lautet: „Lizenz von dieser Installation entfernen? Die Erweiterung gibt danach keine strukturierten Daten mehr aus.“ Sie ist mehr als eine Höflichkeit — die Bestätigung wird als Formularfeld mitgesendet, und der Server lehnt eine Entfernung ohne dieses Feld ab.
Die drei Zustände, die der Abschnitt anzeigt:
| Meldung | Bedeutung |
|---|---|
| „Lifetime-Free-Lizenz aktiv. Alle Funktionen freigeschaltet.“ | Alles in Ordnung; darunter erscheint die Faktenzeile. |
| „Keine Lizenz aktiviert. Es werden keine strukturierten Daten ausgegeben, bis eine Lizenz aktiviert ist.“ | Es ist nichts gespeichert. |
| „Die gespeicherte Lizenz gilt nicht für diese Installation. Es werden keine strukturierten Daten ausgegeben.“ | Es liegt etwas vor, es passt aber nicht — abgelaufen, verändert oder von einer anderen Installation kopiert. |
| „Diese Lizenz ist für keine der auf dieser Installation konfigurierten Domains ausgestellt.“ | Sonderfall des vorigen Zustands: Der Datensatz ist echt, nennt aber andere Hostnamen als die Startseiten. |
| „Auf keiner Startseite ist eine Domain hinterlegt, daher kann keine Lizenz aktiviert werden. Bitte zuerst die Domain der Startseite setzen.“ | Erscheint zusätzlich, solange keine Startseite eine Domain führt. |
Bei aktiver Lizenz zeigt eine Zeile die Fakten, durch Punkte getrennt: Schlüssel (maskiert), Paket, Gültig ab, Gültig bis — bei dieser Ausgabe stets „unbegrenzt“ — und Zuletzt geprüft.
| Rückmeldung | Anlass |
|---|---|
| „Die Lizenz wurde aktiviert.“ | Aktivierung erfolgreich |
| „Die Lizenz wurde aktualisiert.“ | Aktualisierung erfolgreich |
| „Die Lizenz wurde entfernt. Die Erweiterung gibt keine strukturierten Daten mehr aus.“ | Entfernung erfolgreich |
| „Die Lizenz konnte nicht aktiviert werden. Bitte den Schlüssel prüfen und erneut versuchen.“ | Schlüssel unbrauchbar oder Antwort nicht verifizierbar |
| „Die Lizenz konnte nicht aktualisiert werden. Die bisherige Lizenz bleibt bestehen.“ | Aktualisierung fehlgeschlagen |
| „Die Lizenz konnte nicht entfernt werden.“ | Der gespeicherte Zustand ließ sich nicht löschen |
| „Das Entfernen wurde nicht bestätigt, es wurde nichts geändert.“ | Rückfrage abgebrochen oder Bestätigungsfeld leer |
| „Auf dieser Installation ist noch keine Lizenz gespeichert, es gibt also nichts zu aktualisieren oder zu entfernen. Bitte zuerst einen Schlüssel eingeben und aktivieren.“ | Aktualisieren oder Entfernen ohne gespeicherten Zustand |
| „Der Lizenzserver war nicht erreichbar. Es wurde nichts geändert.“ | Zeitüberschreitung, Netzwerkfehler oder Serverfehler auf der Gegenseite |
Wird beim Öffnen ein Schlüssel aus einer Vorversion gefunden, erscheint der Hinweis: „Es wurde ein Lizenzschlüssel aus einer früheren Version dieser Erweiterung gefunden. Lizenzen sind jetzt signiert, der Schlüssel muss deshalb einmal neu aktiviert werden.“ Bis zur erneuten Aktivierung gibt die Erweiterung keine strukturierten Daten aus.
| Berechtigung | Voraussetzung |
|---|---|
| Modul Schema.org öffnen | Zugriff auf das Backend-Modul in der Benutzergruppe |
| Lizenzabschnitt sehen und bedienen | Zugriff auf das Modul Einstellungen; in Contao standardmäßig Administratoren vorbehalten |
| Schema-Felder an Seiten und Datensätzen bearbeiten | Nicht-Administratoren benötigen die Felder in ihrer Gruppe unter den erlaubten Feldern |
Jede lizenzverändernde Aktion prüft serverseitig zuerst die Modulberechtigung und danach das Contao-Anfrage-Token, bevor der gespeicherte Schlüssel gelesen oder der Lizenzserver kontaktiert wird. Der vollständige Schlüssel wird nach der Aktivierung nicht mehr angezeigt und nicht in das Eingabefeld zurückgeschrieben.
Teil 3 — Für Entwickler
Ausgabeweg und Reihenfolge
Die Einfügung geschieht in einem Listener auf kernel.response mit
Priorität -768, also spät in der Kette. Er schreibt den Block unmittelbar
vor das erste </head> im Antworttext.
Ausgegeben wird nur, wenn alle folgenden Bedingungen zutreffen:
- Es handelt sich um die Hauptanfrage, nicht um eine Unteranfrage.
- Die Anfrage liegt im Frontend-Scope.
- Ein
PageModelist aufgelöst. - Der
Content-Typeist leer oder enthälttext/html. - Der Antworttext enthält
</head>. - Die Installation ist lizenziert.
- Der aufgerufene Hostname ist selbst einer der lizenzierten Hosts.
- Mindestens ein Knoten wurde erzeugt.
Trifft eine Bedingung nicht zu, verhält sich Contao exakt wie ohne dieses Bundle. Es werden keine Inhalte verändert, entfernt oder umgeleitet.
Die Knotenlieferanten laufen nach absteigender Priorität:
| Priorität | Lieferant | Ergebnis |
|---|---|---|
| 100 | OrganizationProvider | Organization / LocalBusiness |
| 90 | WebSiteProvider | WebSite mit optionaler SearchAction |
| 80 | WebPageProvider | WebPage und Untertypen |
| 70 | BreadcrumbProvider | BreadcrumbList, ergänzt WebPage.breadcrumb |
| 50 | NewsArticleProvider | Artikelknoten der Nachrichten-Detailseite |
| 50 | EventProvider | Event der Termin-Detailseite |
| 50 | FaqProvider | FAQPage der FAQ-Leseseite |
| 10 | CustomJsonLdProvider | Frei eingegebene Knoten |
Der Ausnahmefall ist eingeplant: Wirft ein Lieferant eine Ausnahme, wird sie verschluckt und der nächste läuft weiter. Eine Seite wird dadurch nie unvollständig ausgeliefert oder abgebrochen — sie verliert höchstens einen Knoten.
Erweiterungspunkt: eigene Knoten
Es gibt genau einen Erweiterungspunkt: Eine eigene Klasse implementiert
VTinnovations\SchemaOrg\Schema\NodeProviderInterface. Bei aktivem
Autoconfigure wird sie automatisch mit
vtinnovations_schema.node_provider getaggt und vom Builder
eingesammelt.
namespace App\Schema;
use VTinnovations\SchemaOrg\Schema\NodeProviderInterface;
use VTinnovations\SchemaOrg\Schema\SchemaContext;
use VTinnovations\SchemaOrg\Schema\SchemaGraph;
final class MyProvider implements NodeProviderInterface
{
public function getPriority(): int
{
return 40; // niedriger als 50 = laeuft nach den Detailknoten
}
public function contribute(SchemaContext $ctx, SchemaGraph $graph): void
{
$graph->add([
'@type' => 'Service',
'@id' => $ctx->pageUrl . '#service',
'name' => 'Beispielleistung',
'provider' => $graph->ref($ctx->organizationId()),
]);
}
}
Was der Kontext bereitstellt:
| Element | Inhalt |
|---|---|
$ctx->page | Die aufgelöste Seite als PageModel |
$ctx->rootPage | Die zugehörige Startseite |
$ctx->baseUrl | Schema und Host ohne abschließenden Schrägstrich |
$ctx->pageUrl | Basis-URL plus Pfad der Anfrage |
$ctx->autoItem | Der Alias des Detaildatensatzes, sonst null |
$ctx->language() | Sprache der Seite, ersatzweise der Startseite, sonst leerer String |
organizationId(), webSiteId(), webPageId(), breadcrumbId(), primaryEntityId() | Die stabilen Knoten-IDs |
| Methode am Graphen | Wirkung |
|---|---|
add(array $node) | Knoten aufnehmen. Mit @id gewinnt der erste Schreiber; spätere Beiträge ergänzen nur fehlende Schlüssel. |
has(string $id) | Prüft, ob ein Knoten mit dieser ID bereits existiert. |
ref(string $id) | Erzeugt einen Verweis der Form {"@id": "…"}. |
isEmpty() | Ob überhaupt etwas gesammelt wurde. |
Zwei öffentliche Dienste lassen sich zusätzlich aus dem Container beziehen:
VTinnovations\SchemaOrg\Schema\SchemaBuilder und
VTinnovations\SchemaOrg\Site\StatusEvaluator. Der Builder liefert ohne
aktive Lizenz einen leeren Graphen — auch, wenn er direkt aufgerufen wird.
Was kein Erweiterungspunkt ist. Es gibt keinen Contao-Hook,
kein Event, keine Bundle-Konfiguration unter config/config.yaml, keinen
Konsolenbefehl und keine Umgebungsvariable dieses Pakets. Die Lizenzprüfung lässt
sich nicht per Einstellung abschalten, und die Zieladressen der externen
Kommunikation lassen sich nicht umlenken — sie sind fest eingebaut und werden gegen
eine hinterlegte Prüfsumme kontrolliert.
Datenbankfelder
Die Migration legt ausschließlich Felder in vorhandenen Contao-Tabellen an. Eigene Tabellen bringt das Paket nicht mit.
| Tabelle | Feld | Typ und Voreinstellung |
|---|---|---|
tl_page | schema_disable | char(1) NOT NULL default '' |
schema_orgType | varchar(32) NOT NULL default 'Organization' | |
schema_orgName | varchar(255) NOT NULL default '' | |
schema_orgLogo | binary(16) NULL | |
schema_orgSameAs | blob NULL | |
schema_orgPhone | varchar(64) NOT NULL default '' | |
schema_orgEmail | varchar(255) NOT NULL default '' | |
schema_orgStreet | varchar(255) NOT NULL default '' | |
schema_orgPostal | varchar(32) NOT NULL default '' | |
schema_orgCity | varchar(128) NOT NULL default '' | |
schema_orgRegion | varchar(128) NOT NULL default '' | |
schema_orgCountry | varchar(2) NOT NULL default '' | |
schema_geoLat | varchar(32) NOT NULL default '' | |
schema_geoLng | varchar(32) NOT NULL default '' | |
schema_openingHours | blob NULL | |
schema_priceRange | varchar(32) NOT NULL default '' | |
schema_website | char(1) NOT NULL default '1' | |
schema_searchUrl | varchar(255) NOT NULL default '' | |
tl_page (Nicht-Startseiten) | schema_pageDisable | char(1) NOT NULL default '' |
schema_webPageType | varchar(32) NOT NULL default '' | |
schema_speakable | varchar(255) NOT NULL default '' | |
schema_customJsonLd | text NULL | |
tl_news | schema_disable | char(1) NOT NULL default '' |
schema_articleType | varchar(32) NOT NULL default '' | |
schema_authorName | varchar(128) NOT NULL default '' | |
schema_authorUrl | varchar(255) NOT NULL default '' | |
schema_speakable | varchar(255) NOT NULL default '' | |
schema_customJsonLd | text NULL | |
tl_calendar_events | schema_disable | char(1) NOT NULL default '' |
schema_eventStatus | varchar(32) NOT NULL default '' | |
schema_eventAttendance | varchar(16) NOT NULL default '' | |
schema_location | varchar(255) NOT NULL default '' | |
schema_customJsonLd | text NULL | |
tl_faq | schema_disable | char(1) NOT NULL default '' |
Die Felder an tl_news, tl_calendar_events und
tl_faq werden nur angelegt, wenn die jeweilige DCA vorhanden ist, das
zugehörige Contao-Bundle also installiert ist. Das Bundle wird nach den optionalen
Inhalts-Bundles geladen, damit seine Palettenergänzungen greifen.
Gelesen wird zur Laufzeit außerdem direkt über DBAL: tl_article für
dateModified, tl_news im Verbund mit tl_user
für den Autor, tl_calendar_events für den Termin sowie
tl_faq im Verbund mit tl_faq_category für die Fragen.
Route und Endpunkt
| Eigenschaft | Wert |
|---|---|
| Routenname | vtinnovations_schema_org_package_intake |
| Pfad | /rest/api/v1/schema-org-license-updater |
| Methode | Nur POST wird bearbeitet. Andere Methoden werden mit 405 und Allow: POST beantwortet — bewusst statt eines 404. |
| Medientyp | application/json, sonst 415 |
| Größenbegrenzung | 262 144 Byte, sonst 413 |
| Contao-Token | abgeschaltet (_token_check: false) |
| Wartungsmodus | umgangen (_bypass_maintenance: true) |
Der Endpunkt ist absichtlich öffentlich: Der Aufrufer ist ein Server, keine
angemeldete Person, es gibt also weder Sitzung noch Browser-Token. Authentifiziert
wird stattdessen kryptografisch — über eine Signatur über Methode, exakten Pfad,
Anfragemetadaten und einen Hash des Körpers, geprüft gegen einen fest hinterlegten
Schlüssel. Herkunftsangaben wie Origin, Referer oder
Absender-IP gelten nicht als Nachweis.
Abgewiesene Anfragen erhalten 401 bei fehlgeschlagener
Authentifizierung und 403 in den übrigen Fällen. Der Antwortkörper nennt
nur rejected; der genaue Grund wird ausschließlich intern protokolliert.
Wiederholt eingespielte oder veraltete Aktualisierungen werden abgewiesen — eine
ältere Lizenz kann eine neuere nicht ersetzen.
Cron und Hintergrundaktualisierung
Ein täglicher Cron-Job hält den Lizenzzustand aktuell, ohne dass jemand etwas
eingeben muss. Er ist über das Contao-Cron-Attribut mit dem Intervall
daily registriert und läuft über die Weboberfläche oder:
vendor/bin/contao-console contao:cron
| Regel | Verhalten |
|---|---|
| Nichts gespeichert | Der Job endet ohne Aufruf. |
| Gespeicherter Datensatz nicht lesbar | Der Job endet; hier muss eine Person handeln. |
| Vor weniger als 12 Stunden geprüft | Kein Aufruf — der Lizenzserver wird nicht ohne Anlass kontaktiert. |
| Aufruf schlägt fehl | Der bisherige Zustand bleibt unverändert. |
Da es im Cron keine Anfrage gibt, wird der Hostname aus dem gespeicherten Datensatz genommen und nicht aus dem Umfeld.
Laufzeitverzeichnisse
| Pfad | Inhalt |
|---|---|
var/schema-org/ | Arbeitsverzeichnis; wird bei Bedarf mit 0700 angelegt |
var/schema-org/state/record.json | Der Lizenzdatensatz, byteweise wie geliefert |
var/schema-org/state/seal.json | Das zugehörige authentifizierte Siegel |
var/schema-org/state/sidecar.json | Betriebsnotizen: ein übernommener Schlüssel und die letzte Ergebniskategorie. Begründet für sich genommen keine Berechtigung. |
var/schema-org/state/.lock | Sperrdatei für Zustandsänderungen |
var/schema-org/journal/exchange.json | Wiederholungsschutz für eingehende Aktualisierungen |
var/schema-org/license.json | Ablage aus Ausgaben vor 2.0. Wird einmal gelesen, der Schlüssel übernommen, dann entfernt. |
Datensatz und Siegel sind zwei Dateien, aber eine Einheit: Sie werden zusammen geschrieben, geprüft und getauscht, und bei einem Fehler nach dem Tausch gemeinsam auf das vorherige Paar zurückgesetzt.
Bei mehreren Anwendungsservern muss var/schema-org/ auf
gemeinsam genutztem Speicher liegen. Sonst wirken Sperre und
Wiederholungsschutz nur je Server, und die Installationen können unterschiedliche
Lizenzstände halten.
Externe Kommunikation
Das Bundle kommuniziert ausschließlich mit dem Lizenzdienst von V&T Innovations
unter www.v-t.one über HTTPS. Die Adressen sind fest im Code hinterlegt,
werden zur Laufzeit gegen eine Prüfsumme kontrolliert und lassen sich nicht per
Konfiguration umlenken.
| Anlass | Richtung | Übertragene Angaben |
|---|---|---|
| Lizenz aktivieren oder aktualisieren | Installation → Lizenzdienst | Produktkennung, Hostname, Lizenzschlüssel |
| Nutzungsmeldung | Installation → Lizenzdienst | Produktname und Hostname — je Seitenaufruf mit ausgegebenen Daten, höchstens einmal pro Anfrage, nie ein Schlüssel |
| Sitzungsmeldung | Installation → Lizenzdienst | Hostname und Lizenzschlüssel, einmal je angemeldeter Backend-Sitzung beim ersten Öffnen des Lizenzabschnitts |
| Ausgelöste Aktualisierung | Lizenzdienst → Installation | Aktualisierte Lizenzdaten an den öffentlichen Endpunkt |
Beide Meldungen werden erst nach dem Senden der Antwort ausgeführt — technisch im
kernel.terminate-Listener — und beeinflussen weder die Auslieferung noch
die Lizenzgültigkeit. Die Zeitlimits liegen bei 2 Sekunden für den Verbindungsaufbau
und 5 Sekunden für die Gesamtdauer, Weiterleitungen sind ausgeschlossen, und
Zertifikate werden geprüft. Ein Fehlschlag bleibt folgenlos und wird innerhalb
derselben Sitzung nicht wiederholt; die Antwort wird nicht gelesen.
Für Firewalls: Ausgehend muss www.v-t.one über HTTPS erreichbar sein.
Eingehend muss /rest/api/v1/schema-org-license-updater erreichbar bleiben.
Protokollierung
Geschrieben wird über den Standard-Logger der Anwendung, also in das übliche
Symfony-Protokoll unter var/logs/. Das Paket richtet keinen eigenen
Monolog-Kanal und keine eigene Protokolldatei ein.
| Ereignis | Stufe | Meldung |
|---|---|---|
| Datensatz übernommen | info | schema-org package applied mit operation, result und license_version |
| Datensatz entfernt | info | schema-org package removed |
| Aktion abgelehnt | info | schema-org licence action refused mit operation und result |
| Eingehende Aktualisierung abgewiesen | warning | schema-org package push refused |
| Eingehende Aktualisierung fehlgeschlagen | warning | schema-org package push failed |
Protokolliert werden ausschließlich betriebliche Angaben: Vorgang, Ergebniskategorie und die übernommene Lizenzversion. Nicht protokolliert werden Lizenzschlüssel, Prüfsummen, Signaturen, übertragene Daten oder Antwortinhalte. Der Lizenzschlüssel erscheint weder in der Browserausgabe noch in Fehlermeldungen.
Deployment
composer install --no-dev --optimize-autoloader
vendor/bin/contao-console contao:migrate --no-interaction
vendor/bin/contao-console cache:clear --env=prod
vendor/bin/contao-console cache:warmup --env=prod
Zusätzlich sicherstellen:
var/schema-org/ist beschreibbar und wird nicht zwischen Installationen kopiert.- Der Contao-Cron läuft.
- Ausgehende HTTPS-Verbindungen sind erlaubt.
- Bei Reverse-Proxy-Betrieb sind
trusted_proxiesundtrusted_hostsin Symfony korrekt gesetzt — der Hostname der Installation wird daraus abgeleitet.
Beim Klonen einer Installation auf eine andere Domain wird die kopierte Lizenz dort nicht anerkannt. Der Lizenzzustand ist an den Hostnamen gebunden und muss für den neuen Hostnamen ausgestellt werden. Ein Staging-System unter eigener Domain gibt bis dahin keine strukturierten Daten aus — die Konfiguration bleibt aber vollständig erhalten.
Tests
Das Paket bringt eine Testsuite mit. Mit installierten Entwicklungsabhängigkeiten läuft sie über PHPUnit:
composer install
vendor/bin/phpunit
Für Umgebungen ohne Entwicklungsabhängigkeiten steht ein eigenständiger Testlauf bereit, der nur PHP benötigt:
composer test-standalone
Ein auslieferbares Paket samt Prüfungen und SHA-256-Manifest entsteht mit:
composer build-release
Der Erstellungslauf bricht ab, wenn die Testsuite fehlschlägt oder das Paket keine gültigen Prüfdaten enthält.
Fehlerbehebung
| Symptom | Ursache und Prüfung |
|---|---|
Kein <script type="application/ld+json"> im Quelltext |
Die häufigste Ursache ist eine fehlende Lizenz. Prüfen Sie System → Einstellungen; steht dort „Keine Lizenz aktiviert…“, folgen Sie Lizenz aktivieren. Sonst der Reihe nach die acht Bedingungen unter Ausgabeweg und Reihenfolge durchgehen. |
| „Auf keiner Startseite ist eine Domain hinterlegt, daher kann keine Lizenz aktiviert werden. Bitte zuerst die Domain der Startseite setzen.“ | Feld Domainname auf der Startseite füllen und Cache leeren. |
| „Diese Lizenz ist für keine der auf dieser Installation konfigurierten Domains ausgestellt.“ | Hostname der Lizenz und Domainname der Startseite stimmen nicht exakt
überein — typisch example.com gegenüber www.example.com.
Die Zuordnung erfolgt byteweise; es gibt keine Wildcards und keine
Eltern-Kind-Beziehung zwischen Hosts. |
| „Der Lizenzserver war nicht erreichbar. Es wurde nichts geändert.“ | Ausgehende HTTPS-Verbindungen zu www.v-t.one freigeben. Die
bisherige Lizenz bleibt bestehen. |
| „Die Lizenz konnte nicht aktiviert werden. Bitte den Schlüssel prüfen und erneut versuchen.“ trotz korrektem Schlüssel | sodium prüfen, Systemzeit prüfen, Schreibrechte auf
var/ prüfen. Der Schlüssel muss zudem 8 bis 190 druckbare
ASCII-Zeichen ohne Leerzeichen umfassen — ein beim Kopieren mitgenommener
Zeilenumbruch reicht zur Ablehnung. |
| „Auf dieser Installation ist noch keine Lizenz gespeichert…“ beim Aktualisieren | Lizenz aktualisieren und Lizenz entfernen werden immer angeboten, auch wenn nichts gespeichert ist. Zuerst einen Schlüssel eingeben und aktivieren. |
| „Das Entfernen wurde nicht bestätigt, es wurde nichts geändert.“ | Die Rückfrage wurde abgebrochen — oder der Browser führt kein JavaScript aus, sodass das Bestätigungsfeld leer bleibt. Ohne dieses Feld verweigert der Server die Entfernung. |
| Ausgabe verschwindet nach Umzug oder Domainwechsel | Die Lizenz ist hostgebunden und muss für den neuen Hostnamen ausgestellt werden. Siehe Deployment. |
| Legende Schema.org / Strukturierte Daten fehlt im Seitenformular | Die Legende wird eingeklappt angelegt — erst aufklappen. Fehlt sie ganz: Cache leeren. Als Nicht-Administrator brauchen Sie die Felder in Ihrer Benutzergruppe unter den erlaubten Feldern. |
| Abschnitt Schema.org in den Einstellungen nicht sichtbar | Er sitzt in der Gruppe V-T.ONE Licence management oberhalb der Contao-eigenen Legenden und setzt Zugriff auf das Modul Einstellungen voraus — standardmäßig Administratoren. |
| Nachricht, Termin oder FAQ ohne eigenen Knoten | Das jeweilige Contao-Bundle fehlt, der Datensatz ist nicht veröffentlicht, oder Schema deaktivieren ist gesetzt. Bei Terminen zusätzlich: ohne gültigen Startzeitpunkt entfällt der Knoten. |
| FAQ-Knoten bleibt leer, obwohl Fragen veröffentlicht sind | Ausgewertet werden nur Kategorien, deren Leseseite genau die aufgerufene Seite ist. Prüfen Sie die Leseseite der FAQ-Kategorie. Fragen ohne Antworttext werden übersprungen. |
| Eigenes JSON-LD erscheint nicht | Ungültiges JSON wird stillschweigend übergangen. Eingabe ohne
@context prüfen und gegen einen JSON-Validator halten. Am Datensatz
wirkt das Feld nur auf der Detailseite. Siehe
Eigenes JSON-LD. |
| Ein eigener Knoten überschreibt einen erzeugten nicht | Beabsichtigt: Bei gleicher @id gewinnt der erste Schreiber, und
spätere Beiträge ergänzen nur fehlende Schlüssel. Vergeben Sie eine eigene
@id. |
| Brotkrumenliste fehlt | Nach dem Ausschluss der Startseite bleiben weniger als zwei Stationen übrig. Auf einer Seite direkt unterhalb der Wurzel ist das der Normalfall. |
| Öffnungszeiten oder Koordinaten erscheinen nicht | Sie werden nur bei Organisationstyp Lokales Unternehmen ausgegeben.
geo zusätzlich nur, wenn beide Koordinaten gefüllt sind. |
| „Für diesen Seitentyp lässt sich keine URL bilden.“ in der Vorschau | Für diese Seite lässt sich keine absolute URL erzeugen. Das betrifft nur die Vorschau; die Ausgabe im Frontend ist davon nicht betroffen. |
| Änderung an der Startseite wirkt nicht | Cache leeren. Das gilt besonders für den Domainnamen, der in die Lizenzprüfung eingeht. |
| Zwei JSON-LD-Blöcke auf der Seite | Ein anderes Bundle oder das Template gibt ebenfalls strukturierte Daten aus. Diese Erweiterung schreibt genau einen Block und entfernt keine fremden. |
Bekannte Einschränkungen
- Installationen unter einer IP-Adresse oder einem einteiligen Hostnamen wie
localhostkönnen nicht lizenziert werden und geben keine strukturierten Daten aus. - Die Zuordnung erfolgt exakt je Hostname. Unterdomains und die Form mit
wwwsind eigenständige Hosts und müssen einzeln lizenziert sein. - Ist auf keiner Startseite ein Domainname hinterlegt, wird der aktuelle, von Symfony geprüfte Hostname der Anfrage verwendet. Für vorhersagbares Verhalten sollte der Domainname gesetzt sein.
- Bei mehreren Anwendungsservern ohne gemeinsam genutztes
var/wirken Sperre und Wiederholungsschutz nur je Server. - Auf lizenzierten Seiten wird nach dem Ausliefern der Antwort je Aufruf eine Nutzungsmeldung gesendet. Auf stark frequentierten Websites ist das bei der Kapazitätsplanung zu berücksichtigen.
- Eigenes JSON-LD wird nicht gegen schema.org validiert.
- Die Backend-Vorschau erzeugt für einzelne Seitentypen keine absolute URL und kann diese daher nicht darstellen.
- Es gibt genau eine Lizenzstufe. Eine Unterscheidung zwischen Free und Pro existiert in diesem Produkt nicht; ein befristeter Datensatz wird abgelehnt, auch wenn seine Signatur echt ist.
- Für Produkte, Rezepte, Bewertungen und ähnliche Typen gibt es keine eigenen Felder — nur den Weg über Eigenes JSON-LD.
- Die Erweiterung entfernt keine strukturierten Daten, die von anderer Stelle ausgegeben werden.
Deinstallation
Wenn die Ausgabe nur vorübergehend aufhören soll, genügt Schema.org komplett deaktivieren auf der Startseite oder Lizenz entfernen in den Einstellungen. In beiden Fällen bleibt die gesamte Konfiguration erhalten und lässt sich jederzeit wieder aktivieren.
Für die vollständige Entfernung:
- Datenbank und Dateisystem sichern.
- Optional Lizenz entfernen wählen, solange das Backend-Formular noch da ist.
- Paket entfernen — im Contao Manager unter Pakete →
Installierte Pakete, oder auf der Kommandozeile:
composer remove vtinnovations/schema-org - Datenbank aktualisieren und Cache leeren:
vendor/bin/contao-console contao:migrate vendor/bin/contao-console cache:clear - Arbeitsverzeichnis löschen:
rm -rf var/schema-org
Die Migration nach der Deinstallation entfernt die Schema-Felder aus
tl_page, tl_news, tl_calendar_events und
tl_faq — samt Inhalt. Damit gehen auch alle selbst
geschriebenen JSON-LD-Blöcke verloren. Contao listet die Löschungen vorher zur
Bestätigung auf; lesen Sie diese Liste, bevor Sie sie bestätigen, und sichern Sie
vorher die Datenbank.
Die Erweiterung löscht, verschiebt und überschreibt darüber hinaus keine Inhalte, Dateien oder Einstellungen der Installation.
