Ü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_HEAD einhängt, verliert ihn stillschweigend, sobald ein eigenes fe_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.

AufgabeVon Hand gepflegtMit diesem Paket
Telefonnummer ändernInhaltselement und JSON-LD-Block anfassenFeld auf der Startseite ändern
Neue Nachricht veröffentlichenArticle-Auszeichnung von Hand ergänzenGeschieht automatisch beim Veröffentlichen
Neuer TerminEvent-Block schreiben, Datumsformat nachschlagenGeschieht automatisch; Status und Teilnahmeart optional
FAQ auszeichnenJede Frage einzeln als Question notierenAlle veröffentlichten Fragen der Leseseite automatisch
Zusammenhang Artikel ↔ HerausgeberNicht vorhanden oder von Hand dupliziertVerweis über @id im selben Graphen
Ergebnis kontrollierenURL suchen, in externen Validator kopierenBackend-Modul zeigt das JSON-LD und verlinkt beide Validatoren
Sonderfall auszeichnen (z. B. Produkt)Eigener Block, unverbundenFeld 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

SituationEinschätzung
Unternehmenswebsite mit Contao 5.3+, Adresse, Kontaktdaten, NachrichtenPasst. Der Regelfall, für den das Paket gebaut ist.
Lokales Geschäft mit Öffnungszeiten und AnfahrtPasst. Organisationstyp Lokales Unternehmen ergänzt Geokoordinaten, Öffnungszeiten und Preisniveau.
Redaktionelle Website mit vielen ArtikelnPasst. Jede Nachrichten-Detailseite bekommt Autor, Daten und Herausgeber ohne Zutun.
FAQ-Bereich, der in KI-Antworten auftauchen sollPasst. Alle veröffentlichten Fragen der Leseseite werden als FAQPage ausgegeben.
Sie brauchen Produkt-, Rezept- oder HowTo-AuszeichnungTeilweise. 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-AdressePasst nicht. Solche Installationen lassen sich nicht lizenzieren und geben keine strukturierten Daten aus.
Sie wollen ausgehende Verbindungen der Installation vollständig unterbindenPasst nicht. Für Aktivierung und Betrieb wird www.v-t.one per HTTPS kontaktiert; das lässt sich nicht abschalten.
Contao 4.13 oder älterPasst 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:

  1. Voraussetzungen prüfen.
  2. Vor der Installation: Sicherung, Domain, Schreibrechte.
  3. Installation über den Contao Manager — der empfohlene Weg.
  4. Alternativ: Installation über Composer.
  5. Installation überprüfen.
  6. Lizenz aktivieren — ohne sie bleibt die Ausgabe aus.
  7. Websiteweite Angaben einrichten.
  8. Erste Ausgabe prüfen.

Voraussetzungen

KomponenteAnforderungBemerkung
PHP^8.28.2, 8.3 und 8.4
Contaocontao/core-bundle ^5.3Ältere Versionen sind über conflict ausdrücklich ausgeschlossen
Symfony^6.4 || ^7.0HttpFoundation, HttpKernel, HttpClient, EventDispatcher, SecurityBundle, Security-CSRF
Doctrine DBAL^3.6 || ^4.0Die Knotenlieferanten lesen direkt über DBAL
PSR-Log^2.0 || ^3.0
PHP-Erweiterung jsonzwingend
PHP-Erweiterung sodiumzwingendOhne sie lässt sich keine Lizenz prüfen; die Erweiterung bleibt dann inaktiv
PHP-Erweiterung intlempfohlenLaut composer.json: „Needed to accept internationalised domain names when activating a licence“
Ausgehendes HTTPSzwingendZiel ist ausschließlich www.v-t.one
Erreichbarkeit unter echtem HostnamenzwingendIP-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-BundleSchaltet frei
contao/news-bundleKnoten NewsArticle / Article / BlogPosting und die Schema-Felder je Nachricht
contao/calendar-bundleKnoten Event und die Schema-Felder je Termin
contao/faq-bundleKnoten FAQPage und das Ausschlussfeld je Frage

Vor der Installation

  1. Sicherung anlegen. Datenbank und Dateisystem. Die Installation legt neue Felder in tl_page an und, sofern vorhanden, in tl_news, tl_calendar_events und tl_faq.
  2. Domain der Startseite klären. Die Lizenz wird exakt je Hostname ausgestellt. Legen Sie vorher fest, unter welchem Namen die Website läuft — example.com und www.example.com sind zwei verschiedene Hosts.
  3. Schreibrechte prüfen. Das Paket legt ein eigenes Arbeitsverzeichnis unterhalb von var/ an.
  4. 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.
PfadZweckRechte
var/schema-org/Arbeitsverzeichnis der Erweiterungbeschreibbar für Webserver- und CLI-Benutzer
var/schema-org/state/Lizenzdatensatz, Siegel, Sidecar, Sperrdateiwird mit 0700 angelegt
var/schema-org/journal/Wiederholungsschutz für eingehende Aktualisierungenwird 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

  1. Contao Manager öffnen und anmelden.
  2. 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.
  3. Über Pakete suchen nach vtinnovations/schema-org suchen und beim Treffer Paket hinzufügen wählen.
  4. Änderungen anwenden. Der Contao Manager installiert das Paket und aktualisiert den Autoloader.
  5. Bereich SystemwartungDatenbank-Migrationen und -BackupsDatenbank prüfen; die angezeigten Datenbank-Änderungen bestätigen.
  6. 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:

OrtWas dort auftaucht
Navigationsgruppe Schema.orgSchema.orgDas Vorschaumodul. Beschreibung: „JSON-LD-Vorschau und Validierung (die Lizenz wird in den Einstellungen verwaltet)“
System → EinstellungenGanz oben die Gruppe V-T.ONE Licence management mit dem Abschnitt Schema.org
Seitenstruktur, Startpunkt einer WebseiteLegende Schema.org / Strukturierte Daten mit den websiteweiten Feldern
Seitenstruktur, jede andere SeiteDieselbe Legende mit den vier Übersteuerungsfeldern
Nachricht, Termin, FAQ-FrageLegende 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.

  1. Domain auf der Startseite eintragen. Seitenstruktur → Startpunkt einer Webseite → Feld Domainname. Die Erweiterung liest die konfigurierten Hosts aus allen Startseiten. Anschließend Cache leeren.
  2. 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.“
  3. Den Schlüssel in das Feld Lizenzschlüssel eintragen (Platzhalter XXXXX-XXXXX-XXXXX-XXXXX) und Lizenz prüfen und aktivieren wählen.
  4. 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:

  1. Organisationstyp festlegen. Voreingestellt ist Organisation; für ein Ladengeschäft, eine Praxis oder ein Restaurant Lokales Unternehmen (mit Adresse/Öffnungszeiten).
  2. Name der Organisation eintragen — oder leer lassen, dann wird der Titel der Startseite verwendet.
  3. Logo auswählen. Es wird als logo und image der Organisation ausgegeben.
  4. Telefon, E-Mail und die Adressfelder füllen, soweit sie öffentlich sind.
  5. sameAs (Profil-URLs): je eine Zeile pro Social-Media-, Wikipedia- oder Profil-URL.
  6. Bei einem lokalen Unternehmen zusätzlich Breitengrad und Längengrad, Öffnungszeiten und Preisniveau.
  7. 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

  1. Cache leeren.
  2. Eine beliebige Frontend-Seite aufrufen und den Quelltext ansehen. Kurz vor </head> steht ein einzelnes Element <script type="application/ld+json">.
  3. Alternativ und bequemer: Backend → Schema.orgJSON-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@idWoher
Organization / LocalBusiness{basis}/#organizationStartseite
ImageObject (Logo){basis}/#logoFeld Logo der Startseite
WebSite{basis}/#websiteStartseite
WebPage und Untertypen{seiten-url}#webpageaktuelle Seite
BreadcrumbList{seiten-url}#breadcrumbSeitenpfad
NewsArticle / Event{seiten-url}#primaryentityDatensatz der Detailseite
FAQPage{seiten-url}#faqpageFAQ-Kategorien mit dieser Leseseite
Eigene KnotenfreiFeld 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.

FeldHilfetext im BackendVoreinstellung
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
E-Mail„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:

OptionVerhalten
OrganisationKnoten 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 ausgebenKein 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 ContactPoint mit contactType: customer service.
  • Aus den Adressfeldern entsteht eine PostalAddress, sobald mindestens eines davon gefüllt ist.
  • geo wird 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

FeldHilfetext im BackendVoreinstellung
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.

FeldHilfetext im BackendVoreinstellung
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):

BeschriftungAusgegebener Typ
Webseite (allgemein)WebPage
Über-uns-SeiteAboutPage
KontaktseiteContactPage
Übersichtsseite (Liste/Sammlung)CollectionPage
ProfilseiteProfilePage
FAQ-SeiteFAQPage
Frage-und-Antwort-SeiteQAPage
Detailseite (einzelnes Objekt)ItemPage
SuchergebnisseiteSearchResultsPage
Kassenseite (Checkout)CheckoutPage

Woher die übrigen Angaben des Knotens stammen:

EigenschaftQuelle
nameSeitentitel der Seite; ist er leer, der Seitenname
descriptionFeld Beschreibung der Seite, sofern gefüllt
inLanguageSprache der Seite, ersatzweise die der Startseite; ist beides leer, entfällt die Angabe
urlAufgerufene URL der Seite
dateModifiedDer jüngere Wert aus dem Änderungszeitpunkt der Seite und dem der veröffentlichten Artikel auf ihr
speakableFeld 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.

FeldHilfetext im BackendVoreinstellung
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-TypAusgegebener Typ
NachrichtenartikelNewsArticle
Artikel (allgemein)Article
BlogbeitragBlogPosting
ReportageReportageNewsArticle
Kommentar/MeinungsbeitragOpinionNewsArticle

Automatisch übernommen werden:

EigenschaftQuelle
headlineSchlagzeile der Nachricht
datePublishedDatum der Nachricht
dateModifiedÄnderungszeitpunkt, mindestens jedoch das Veröffentlichungsdatum
descriptionTeasertext ohne HTML, sofern vorhanden
imageDas Bild der Nachricht, wenn Ein Bild hinzufügen aktiv und eine Datei gewählt ist
authorAutor (Name), sonst der Name des Contao-Benutzers; als Person, mit url wenn Autor (URL) gefüllt ist
publisherVerweis auf den Organisationsknoten, sofern dieser ausgegeben wird
isPartOf / mainEntityOfPageVerweis 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)

FeldHilfetext im BackendVoreinstellung
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-StatusAusgegebener Wert
Findet statthttps://schema.org/EventScheduled
Verschoben (neuer Termin)https://schema.org/EventRescheduled
Verschoben (offen)https://schema.org/EventPostponed
Nach online verlegthttps://schema.org/EventMovedOnline
Abgesagthttps://schema.org/EventCancelled
TeilnahmeartAusgegebener Wert
Vor Orthttps://schema.org/OfflineEventAttendanceMode
Onlinehttps://schema.org/OnlineEventAttendanceMode
Hybrid (vor Ort + online)https://schema.org/MixedEventAttendanceMode

Weitere Regeln:

  • eventStatus wird immer geschrieben. Bleibt das Feld leer, lautet der Wert EventScheduled.
  • eventAttendanceMode wird nur bei ausdrücklicher Auswahl geschrieben.
  • Führt der Termin eine Uhrzeit, entsteht ein vollständiger Zeitstempel, sonst ein reines Datum.
  • Ein endDate entsteht nur, wenn das Ende nach dem Anfang liegt.
  • Als location dient Veranstaltungsort (Name), ersatzweise das Ortsfeld des Termins; ausgegeben wird ein Place mit name.
  • Wird ein Organisationsknoten ausgegeben, wird er als organizer referenziert.
  • 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 FrageHilfetext im BackendVoreinstellung
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.mainEntity auf die FAQPage.

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" } }
]
VerhaltenErläuterung
Ungültiges JSONWird stillschweigend übergangen. Die Seite wird niemals abgebrochen und nichts wird gemeldet.
Mitgeliefertes @contextWird entfernt; der Graph liefert seinen eigenen.
Knoten mit eigener @idKann von anderen eigenen Knoten referenziert werden und kann einen bestehenden Knoten um fehlende Felder ergänzen.
Knoten ohne @idWird als eigenständiger Eintrag an den Graphen angehängt.
Feld am DatensatzWird 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.

ElementBeschriftung / 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.
ErgebnisZeile „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:

MeldungBedeutung
„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ächeVerhalten
Lizenz prüfen und aktivierenPrüft den eingegebenen Schlüssel beim Lizenzserver, verifiziert die Antwort und speichert sie erst danach.
Lizenz aktualisierenHolt 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 entfernenErfordert die Bestätigung der Rückfrage und versetzt die Installation sofort in den unlizenzierten Zustand.
Gefundenen Schlüssel der Vorversion aktivierenErscheint 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:

MeldungBedeutung
„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ückmeldungAnlass
„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.

BerechtigungVoraussetzung
Modul Schema.org öffnenZugriff auf das Backend-Modul in der Benutzergruppe
Lizenzabschnitt sehen und bedienenZugriff auf das Modul Einstellungen; in Contao standardmäßig Administratoren vorbehalten
Schema-Felder an Seiten und Datensätzen bearbeitenNicht-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:

  1. Es handelt sich um die Hauptanfrage, nicht um eine Unteranfrage.
  2. Die Anfrage liegt im Frontend-Scope.
  3. Ein PageModel ist aufgelöst.
  4. Der Content-Type ist leer oder enthält text/html.
  5. Der Antworttext enthält </head>.
  6. Die Installation ist lizenziert.
  7. Der aufgerufene Hostname ist selbst einer der lizenzierten Hosts.
  8. 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ätLieferantErgebnis
100OrganizationProviderOrganization / LocalBusiness
90WebSiteProviderWebSite mit optionaler SearchAction
80WebPageProviderWebPage und Untertypen
70BreadcrumbProviderBreadcrumbList, ergänzt WebPage.breadcrumb
50NewsArticleProviderArtikelknoten der Nachrichten-Detailseite
50EventProviderEvent der Termin-Detailseite
50FaqProviderFAQPage der FAQ-Leseseite
10CustomJsonLdProviderFrei 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:

ElementInhalt
$ctx->pageDie aufgelöste Seite als PageModel
$ctx->rootPageDie zugehörige Startseite
$ctx->baseUrlSchema und Host ohne abschließenden Schrägstrich
$ctx->pageUrlBasis-URL plus Pfad der Anfrage
$ctx->autoItemDer 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 GraphenWirkung
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.

TabelleFeldTyp und Voreinstellung
tl_pageschema_disablechar(1) NOT NULL default ''
schema_orgTypevarchar(32) NOT NULL default 'Organization'
schema_orgNamevarchar(255) NOT NULL default ''
schema_orgLogobinary(16) NULL
schema_orgSameAsblob NULL
schema_orgPhonevarchar(64) NOT NULL default ''
schema_orgEmailvarchar(255) NOT NULL default ''
schema_orgStreetvarchar(255) NOT NULL default ''
schema_orgPostalvarchar(32) NOT NULL default ''
schema_orgCityvarchar(128) NOT NULL default ''
schema_orgRegionvarchar(128) NOT NULL default ''
schema_orgCountryvarchar(2) NOT NULL default ''
schema_geoLatvarchar(32) NOT NULL default ''
schema_geoLngvarchar(32) NOT NULL default ''
schema_openingHoursblob NULL
schema_priceRangevarchar(32) NOT NULL default ''
schema_websitechar(1) NOT NULL default '1'
schema_searchUrlvarchar(255) NOT NULL default ''
tl_page (Nicht-Startseiten)schema_pageDisablechar(1) NOT NULL default ''
schema_webPageTypevarchar(32) NOT NULL default ''
schema_speakablevarchar(255) NOT NULL default ''
schema_customJsonLdtext NULL
tl_newsschema_disablechar(1) NOT NULL default ''
schema_articleTypevarchar(32) NOT NULL default ''
schema_authorNamevarchar(128) NOT NULL default ''
schema_authorUrlvarchar(255) NOT NULL default ''
schema_speakablevarchar(255) NOT NULL default ''
schema_customJsonLdtext NULL
tl_calendar_eventsschema_disablechar(1) NOT NULL default ''
schema_eventStatusvarchar(32) NOT NULL default ''
schema_eventAttendancevarchar(16) NOT NULL default ''
schema_locationvarchar(255) NOT NULL default ''
schema_customJsonLdtext NULL
tl_faqschema_disablechar(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

EigenschaftWert
Routennamevtinnovations_schema_org_package_intake
Pfad/rest/api/v1/schema-org-license-updater
MethodeNur POST wird bearbeitet. Andere Methoden werden mit 405 und Allow: POST beantwortet — bewusst statt eines 404.
Medientypapplication/json, sonst 415
Größenbegrenzung262 144 Byte, sonst 413
Contao-Tokenabgeschaltet (_token_check: false)
Wartungsmodusumgangen (_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
RegelVerhalten
Nichts gespeichertDer Job endet ohne Aufruf.
Gespeicherter Datensatz nicht lesbarDer Job endet; hier muss eine Person handeln.
Vor weniger als 12 Stunden geprüftKein Aufruf — der Lizenzserver wird nicht ohne Anlass kontaktiert.
Aufruf schlägt fehlDer 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

PfadInhalt
var/schema-org/Arbeitsverzeichnis; wird bei Bedarf mit 0700 angelegt
var/schema-org/state/record.jsonDer Lizenzdatensatz, byteweise wie geliefert
var/schema-org/state/seal.jsonDas zugehörige authentifizierte Siegel
var/schema-org/state/sidecar.jsonBetriebsnotizen: ein übernommener Schlüssel und die letzte Ergebniskategorie. Begründet für sich genommen keine Berechtigung.
var/schema-org/state/.lockSperrdatei für Zustandsänderungen
var/schema-org/journal/exchange.jsonWiederholungsschutz für eingehende Aktualisierungen
var/schema-org/license.jsonAblage 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.

AnlassRichtungÜbertragene Angaben
Lizenz aktivieren oder aktualisierenInstallation → LizenzdienstProduktkennung, Hostname, Lizenzschlüssel
NutzungsmeldungInstallation → LizenzdienstProduktname und Hostname — je Seitenaufruf mit ausgegebenen Daten, höchstens einmal pro Anfrage, nie ein Schlüssel
SitzungsmeldungInstallation → LizenzdienstHostname und Lizenzschlüssel, einmal je angemeldeter Backend-Sitzung beim ersten Öffnen des Lizenzabschnitts
Ausgelöste AktualisierungLizenzdienst → InstallationAktualisierte 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.

EreignisStufeMeldung
Datensatz übernommeninfoschema-org package applied mit operation, result und license_version
Datensatz entferntinfoschema-org package removed
Aktion abgelehntinfoschema-org licence action refused mit operation und result
Eingehende Aktualisierung abgewiesenwarningschema-org package push refused
Eingehende Aktualisierung fehlgeschlagenwarningschema-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_proxies und trusted_hosts in 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

SymptomUrsache 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 localhost können nicht lizenziert werden und geben keine strukturierten Daten aus.
  • Die Zuordnung erfolgt exakt je Hostname. Unterdomains und die Form mit www sind 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:

  1. Datenbank und Dateisystem sichern.
  2. Optional Lizenz entfernen wählen, solange das Backend-Formular noch da ist.
  3. Paket entfernen — im Contao Manager unter PaketeInstallierte Pakete, oder auf der Kommandozeile:
    composer remove vtinnovations/schema-org
  4. Datenbank aktualisieren und Cache leeren:
    vendor/bin/contao-console contao:migrate
    vendor/bin/contao-console cache:clear
  5. 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.