IT security, FreeBSD, Linux, mail server hardening, post-quantum crypto, DNS, retro computing & hands-on hardware hacks. Privater Tech-Blog seit 2003.

Schlagwort: Fediverse

Von SEO zu AEO, der Kassensturz: was eine maschinenlesbare Identität wirklich bringt

Visualisierung einer maschinenlesbaren Online-Identität mit JSON-LD, Knowledge Graph und KI-Antwortsystemen zur Verknüpfung einer Person über mehrere digitale Quellen.

Am 1. Januar habe ich hier einen Beitrag geschrieben, der eine Wette war. Die These: Web-Optimierung verschiebt sich. Weg von SEO, dem Kampf um die beste Platzierung bei Google, hin zu etwas, das ich AEO genannt habe. Answer Engine Optimization. Also nicht mehr „wie komme ich auf Platz eins“, sondern „wie liefere ich die beste maschinenlesbare Antwort“. Ich habe damals llms.txt eingebaut, ein bisschen über JSON-LD geschrieben und ehrlich dazugesagt, dass niemand weiß, ob das langfristig relevant bleibt. Der letzte Satz war: ich bin gespannt, was passiert.

Jetzt ist ein gutes halbes Jahr vergangen. Zeit für einen Kassensturz. Was davon hat sich gehalten, was war naiv, was hat sich differenziert? Und vor allem: ich habe in den letzten Monaten tatsächlich daran gearbeitet, mich für eine Maschine sauber beschreibbar zu machen. Nicht als Theorie, sondern an der eigenen Seite, mit allen Fehlern, die dabei sichtbar wurden. Genau diese Fehler und die Abwägungen dahinter sind der eigentliche Inhalt dieses Beitrags. Wer den Vorgängerpost noch nicht kennt, findet ihn hier: Von SEO zu AEO, warum llms.txt, JSON-LD und Answer Engines das Web verändern.

Die Suche wird zur Antwortmaschine

Fangen wir mit dem an, was sich gerade wirklich verändert, unabhängig von meinem Blog. Wer heute etwas googelt, bekommt immer öfter die Antwort direkt auf der Ergebnisseite. Eine zusammengefasste KI-Antwort, darunter vielleicht ein paar Quellen. Der Klick auf eine Webseite entfällt. Dafür gibt es einen Begriff: Zero-Click-Suche. Die Information erreicht den Menschen, ohne dass er die Seite besucht, von der sie stammt.

Das ist keine Vermutung, das lässt sich messen. Das Pew Research Center hat Daten aus dem Frühjahr 2025 ausgewertet, veröffentlicht im Juli 2025: das Surfverhalten von rund 900 erwachsenen US-Nutzern, knapp 68.900 Google-Suchanfragen. Das Ergebnis: bekamen die Leute eine KI-Zusammenfassung angezeigt, klickten nur noch 8 Prozent auf einen weiterführenden Treffer. Ohne KI-Zusammenfassung waren es 15 Prozent, fast doppelt so viel. Auf die in der KI-Antwort verlinkten Quellen klickte überhaupt nur 1 Prozent. Fairerweise dazugesagt: Google hält die Methodik dieser Studie für nicht repräsentativ, hat aber keine eigenen Gegenzahlen vorgelegt. Zur Einordnung der Größenordnung: schon bei der ganz normalen Google-Suche endet ein großer Teil ohne Klick. Die Zero-Click-Studie von SparkToro (Rand Fishkin, 2024, Datenbasis Datos, das zu Semrush gehört) kommt auf rund 58 Prozent in den USA und knapp 60 Prozent in der EU, neuere Auswertungen für 2026 eher Richtung zwei Drittel. Und ein Hinweis zur Vorsicht, weil die Zahl gern falsch zitiert wird: die oft genannten 93 Prozent Zero-Click gelten ausschließlich für Googles AI Mode, also den dialogorientierten Chat-Modus der Suche (Semrush maß dort 92 bis 94 Prozent), nicht für die normale Suche. Wer diese Schlagzeile unbesehen übernimmt, vergleicht Äpfel mit Birnen.

Wie stark der Effekt kausal ist, hat ein randomisiertes Feldexperiment von Saharsh Agarwal (Indian School of Business) und Ananya Sen (Carnegie Mellon University) untersucht, Feldphase Januar und Februar 2026, 1.065 ausgewertete US-Desktop-Nutzer von Chrome. Auf den Suchanfragen, bei denen tatsächlich eine KI-Übersicht erschien, sanken die organischen Klicks um etwa 38 Prozent, die Zero-Click-Rate stieg von 54 auf 72 Prozent. Wichtig für die Einordnung: das ist ein noch nicht begutachtetes Arbeitspapier, online seit April 2026, und die Stichprobe sind aktive Desktop-Chrome-Nutzer aus einem Panel, nicht alle Google-Nutzer. Das Studiendesign war immerhin vorab registriert, was die Aussagekraft stützt. Trotzdem bleibt es ein Befund mit klaren Grenzen. Die Pointe ist nicht, dass die Suche stirbt, sondern etwas Nüchterneres: Sichtbarkeit entkoppelt sich vom Klick. Man kann als Quelle einer Antwort auftauchen, ohne dass jemand die eigene Seite öffnet.

Vom String zum Ding

Jetzt wird es interessant, denn hier liegt der Mechanismus, der mitentscheidet, ob man in so einer Antwort überhaupt vorkommt. Schon 2012 hat Google den Knowledge Graph eingeführt, unter dem Slogan „Things, not strings“. Übersetzt: Dinge, nicht Zeichenketten. Davor war eine Suchmaschine im Kern ein Textabgleich. Du tippst Buchstaben, sie sucht Seiten mit denselben Buchstaben. Seitdem versucht Google, hinter den Buchstaben das tatsächliche Ding zu erkennen. Eine Entität. Ein eindeutig identifizierbares Etwas mit Beziehungen zu anderen eindeutig identifizierbaren Etwas.

Das klassische Beispiel ist das Wort Jaguar. Tier, Auto oder Betriebssystem? Ein Mensch erkennt aus dem Zusammenhang sofort, was gemeint ist. Eine Maschine muss disambiguieren, also die Mehrdeutigkeit auflösen. Und genau dasselbe Problem gilt für mich. Welcher Sebastian van de Meer? Es gibt mehr als einen Menschen mit diesem Namen. Für eine Maschine ist mein Name erst einmal nur eine Zeichenkette, die zu mehreren Personen passt. Eindeutigkeit wird belohnt. Es gibt dazu einen vielzitierten Datenpunkt, den ich ehrlich einordnen muss: laut einer Auswertung von Kalicube (Jason Barnard, veröffentlicht bei Search Engine Land im August 2025) verschwanden im Juni 2025 über drei Milliarden Einträge aus dem Knowledge Graph, ein Rückgang von rund sechs Prozent, verteilt auf zwei Stichtage. Google hat das nie offiziell bestätigt, und die Deutung, dass hier Klarheit über Masse gewinnt, ist die des Analysten, nicht Googles erklärter Grund. Also ein Indiz, kein Gesetz. Der Knowledge Graph selbst speist sich nach Googles eigenen Angaben unter anderem aus Wikipedia, Branchenquellen nennen ergänzend Wikidata, und er ist das Bindeglied zwischen klassischer Suche und KI-Antworten. Googles KI-Suche, die auf Gemini basiert, greift nach eigener Darstellung auf den Knowledge Graph als Echtzeit-Quelle zurück. Wer dort als saubere Entität existiert, ist für beide Welten greifbar.

Wie man sich einer Maschine als Entität vorstellt

Damit sind wir beim Herzstück. Wie sage ich einer Maschine glaubwürdig, wer ich bin? Das Werkzeug dafür heißt JSON-LD nach dem schema.org-Vokabular. Vereinfacht: ein maschinenlesbarer Steckbrief, der direkt in der Seite liegt und Fakten ausdrücklich beschriftet. Das ist der Autor, das ist sein Beruf, das ist das Erscheinungsdatum. Statt die Maschine alles aus Fließtext erraten zu lassen, legt man ihr die Fakten getypt hin. Eine Klarheits- und Extraktionshilfe, mehr nicht. Keine Garantie auf ein Ranking und keine Garantie, zitiert zu werden. Diese Erwartung muss man sofort dämpfen, sonst baut man Luftschlösser.

Aus der abstrakten Ansage von damals ist bei mir eine ziemlich durchdachte Identitäts-Architektur geworden. Und ehrlich: das Spannende waren nicht die Zeilen, die ich geschrieben habe, sondern das, was ich dabei gelernt habe. Neun Punkte, die ich so vorher nicht auf dem Schirm hatte.

Erstens, eine Identität, eine kanonische Adresse. Für eine Maschine sollte eine Person genau ein Ding sein, mit einer stabilen Kennung, die überall identisch auftaucht, nicht auf jeder Seite neu erfunden. Maschinen lösen Identität über stabile Identifier auf, nicht über Namen. Lose Namensnennungen ohne gemeinsame Kennung werden als verschiedene Menschen gelesen oder gar nicht zusammengeführt. Der Preis ist weniger Flexibilität. Der Gewinn ist ein zusammenhängender Knoten statt vieler Splitter.

Zweitens, eine Wahrheitsquelle statt überall dasselbe reinkippen. Die vollständige Selbstbeschreibung steht bei mir an genau einer Stelle, auf der Über-mich-Seite. Alle anderen Seiten tragen nur eine schlanke Referenz darauf. Der Grund ist unromantisch: dieselbe Definition überall zu duplizieren erzeugt Drift. Man ändert eine Stelle, vergisst die anderen, und am Ende widerspricht sich der eigene Datensatz selbst. Die Abwägung: die schlanke Referenz darf nicht zu dünn sein, sonst findet ein Crawler, der zufällig nur eine Artikelseite erwischt, keinen Anker zurück zum Profil.

Drittens, Privates gehört nicht in den maschinenlesbaren Broadcast. Das war für mich die wichtigste Einsicht. Telefonnummer, Adresse und ähnliches haben für die maschinelle Identifikation exakt null Wert. Disambiguiert wird über verlinkte Profile, nicht über die Handynummer. Auf jeder einzelnen Seite ausgestrahlt wären solche Daten dagegen eine ideale Fläche zum Abgreifen. Also stehen die privaten Angaben jetzt bewusst nur dort, wo sie hingehören, und sind nicht mehr auf rund 470 Seiten als sauber beschriftete Schlüssel-Wert-Paare maschinenlesbar verteilt. Das ist die zentrale Abwägung zwischen Datenschutz und Maschinenlesbarkeit, und sie fällt klar zugunsten Datenschutz aus. Das Schöne: man verliert dabei kein einziges Identitäts-Signal.

Viertens, externe Anker sind die eigentliche Beweiskette. Eine Behauptung über mich wird erst dann prüfbar, wenn sie auf unabhängige Profile verweist und diese zurückverweisen. Bei mir sind das unter anderem GitHub, ein Eintrag im BSI-Bürger-CERT-Netzwerk, Mastodon und die Bluesky-Brücke, dazu Identifier wie ORCID und ein Wikidata-Eintrag. Entscheidend ist die Wechselseitigkeit. Ein Verweis zählt nur, wenn die Gegenseite zurückzeigt. Anfangs lagen diese Anker nur auf der Profilseite. Das war ein Single Point of Failure: wenn ein Crawler genau diese eine Seite nicht erwischt, ist die Identität nicht mehr belegbar. Also gehören die stärksten Anker auf jede Seite. Und die Disziplin dabei: unverifizierbare oder tote Profile lässt man weg, weil sie das Signal nur verwässern.

Fünftens, innere Widerspruchsfreiheit ist selbst ein Qualitätssignal. Ein Beispiel aus der eigenen Seite, das mich erst geärgert und dann überzeugt hat: der Herausgeber des Blogs und der Herausgeber der einzelnen Artikel zeigten auf zwei verschiedene, nirgends sauber definierte Stellen. Für eine Maschine sieht so etwas aus wie ein Datenfehler und untergräbt das Vertrauen in den gesamten Datensatz. Die Lektion war, lieber einen sauber benannten zusätzlichen Knoten einzuführen, hier die Marke „Kernel-Error“ als eigene Herausgeber-Instanz, als zwei sich widersprechende Halbwahrheiten stehen zu lassen. Das ist übrigens keine technische Petitesse, sondern eine echte Identitäts-Entscheidung: gilt „Kernel-Error“ als eigene Marke neben der Person? Ich habe mich dafür entschieden, und plötzlich ergab der ganze Rest Sinn.

Sechstens, einen Wissensgraphen kann man nicht belügen. Das klingt pathetisch, ist aber sehr praktisch gemeint. Alle externen Quellen, auf die ich verweise, sind crawlbar. Jeder Status lässt sich gegen das echte Upstream-Projekt prüfen. Also habe ich offene Beiträge ehrlich als offen gekennzeichnet, statt sie als erledigt zu verkaufen. Zwei meiner Patches für eine Fingerabdruckleser-Bibliothek sind eingereicht, aber noch nicht gemerged, und genau so steht es da. Behauptungen, die ich nicht belegen kann, etwa angebliche CVEs, die sich öffentlich nicht auffinden lassen, habe ich komplett weggelassen. Ein als „erledigt“ deklarierter, in Wahrheit offener Beitrag ist ein sofort widerlegbarer Fehler, und der beschädigt die Glaubwürdigkeit des gesamten Profils. Die Abwägung ist unbequem: das Profil sieht weniger beeindruckend aus. Aber ein einziger entlarvter Fake-Claim ist teurer als zehn ehrliche kleine. Vertrauen entsteht aus Prüfbarkeit, nicht aus Behauptung.

Siebtens, Expertise belegt man mit Artefakten, nicht mit Adjektiven. Niemand muss mir glauben, dass ich etwas kann. Sie können es nachsehen. Konkrete, von Dritten kontrollierbare Arbeiten sind der stärkste maschinenlesbare Beleg. Ein in ein fremdes Projekt aufgenommener Patch verankert mich im Linkgraph dieses fremden, autoritativen Projekts. Ein eigenes Repository ist überprüfbarer Code, kein Selbstlob. Die Disziplin dahinter: nur real Existierendes, korrekt zugeschrieben. Fremde Maintainer-Arbeit führe ich nicht als meine. Bei einem Rezensions-Artikel über ein Tool, das mir nicht gehört, bleibt die Urheberschaft beim Upstream. Und Füllmaterial wie Trivia oder Verzeichnis-Einträge bleibt bewusst draußen, um das Signal nicht zu verwässern.

Achtens, wer alles kennt, löst auf nichts auf. Meine Themenliste hatte über 40 mehr oder weniger beliebige Schlagworte. Das habe ich auf eine Handvoll fokussierte Kernthemen zusammengestrichen, möglichst als eindeutige Referenzen statt als nackte Wörter. Der Grund: zu viele Themen verwässern das Signal so sehr, dass man für kein einziges Feld als Autorität erkennbar ist. Die Wette dahinter ist, dass ein scharfes Profil in wenigen Feldern für eine Antwortmaschine wertvoller ist als eine lange, unscharfe Stichwortliste. Der Preis ist Breite bei Nischen-Anfragen. Den zahle ich gerne.

Neuntens, jede Seite soll sagen, was sie ist. Profilseite, Kontaktseite, Artikel, Autorenarchiv: jeder Seitentyp deklariert jetzt seine Rolle und welche Rolle ich dort spiele. Das stärkste Signal „diese Adresse ist das kanonische Profil dieser Person“ entsteht erst dadurch, dass die Profilseite sich auch als Profilseite zu erkennen gibt. Vorher sah sie für eine Maschine aus wie jede beliebige andere Seite und verschenkte diese Aussage komplett. Die Abwägung: mehr Fallunterscheidung im Code, dafür präzise, rollenrichtige Signale.

Wie Antwortmaschinen ihre Quellen wählen

Eine einzelne perfekte Seite reicht nicht. KI-Systeme kreuzprüfen eine Entität über mehrere unabhängige Quellen, bevor sie zitieren. Im schema.org-Vokabular heißt das Stichwort sameAs, frei übersetzt der Verweis auf denselben Ausweis anderswo. Konsistente, echte Verweise erhöhen die Vertrauenswürdigkeit, garantieren aber nichts. Es braucht übereinstimmende Spuren an mehreren Orten. Und Vorsicht vor dem Trugschluss „mehr ist besser“: tote oder inkonsistente Verweise schaden, nur gepflegte, echte Profile zählen.

Der vielleicht wichtigste Befund für alle, die keine Marketing-Abteilung haben: Zitierwürdigkeit ist nicht dasselbe wie Ranking. Ahrefs hat im August 2025 rund 15.000 Long-Tail-Anfragen ausgewertet und KI-Assistenten wie ChatGPT, Gemini und Perplexity dieselben Fragen gestellt. Ergebnis: im Schnitt ranken nur rund 12 Prozent der von diesen Tools zitierten URLs in Googles Top 10, rund 88 Prozent also nicht. Etwa 80 Prozent tauchen für die ursprüngliche Anfrage überhaupt nicht in Googles Ergebnissen auf. Ein Detail der Ehrlichkeit halber: das ist ein Durchschnitt, und Perplexity schert mit knapp 29 Prozent Überschneidung deutlich nach oben aus, hängt also stärker an der klassischen Suche als die anderen. Die Botschaft bleibt trotzdem: Antwortmaschinen wählen nach antwortfertig, glaubwürdig und strukturell sauber, nicht primär nach Suchplatzierung. Genau deshalb kann ein Nischenblog ohne Spitzen-Rankings trotzdem zitierfähig sein. Wer nur in Keyword-Rankings denkt, greift zu kurz.

Und was steigert nun messbar die Sichtbarkeit in generativen Antworten? Eine viel zitierte akademische Arbeit von Forschenden der Princeton University und des IIT Delhi, dazu zwei unabhängige Autoren, hat genau das untersucht, vorgestellt auf der KDD 2024. Sie gilt als die erste Arbeit, die den Begriff Generative Engine Optimization geprägt hat. Die Antwort ist herrlich unspektakulär, und das ist die eigentliche Pointe. Was hilft, ist: wörtliche Zitate einbauen (in der Studie der stärkste Hebel mit rund 41 Prozent mehr Sichtbarkeit), Statistiken nennen (rund 33 Prozent), Quellen angeben (rund 28 Prozent), flüssig und gut lesbar schreiben (ähnliche Größenordnung). Insgesamt bis zu rund 40 Prozent mehr Sichtbarkeit. Zwei Einschränkungen gehören dazu: gemessen wurde nicht Traffic oder Klicks, sondern eine positionsgewichtete Sichtbarkeit innerhalb der KI-Antwort, und die Prozente sind relativ zu einer unoptimierten Ausgangsversion. Das Schlusslicht, mit deutlichem Abstand: klassisches Keyword-Stuffing senkte die Sichtbarkeit sogar, um rund 8 bis 9 Prozent. Die Botschaft ist also kein Geheimtrick, sondern fast schon eine Erlösung: gute, belegte, lesbare Substanz ist die Strategie. Das ist auch der Kern von E-E-A-T, also Erfahrung, Fachkenntnis, Autorität und Vertrauen. Kein Algorithmus-Schalter, sondern ein Signalbündel. Und genau hier zahlt die verifizierte Identität ein: echte Werke, externe Bestätigung und Konsistenz machen Erfahrung und Expertise überhaupt erst maschinell nachvollziehbar.

Ehrlicher Kassensturz

Bleibt die unbequeme Frage: hat das alles etwas gebracht? Fangen wir mit der Korrektur meiner eigenen Anfangs-Wette an, der llms.txt. Die läuft live, der Aufwand für die Datei ist billig und harmlos. Aber sie ist kein bewiesener Hebel. Auf der Search Central Live im Juli 2025 stellte Gary Illyes klar, dass llms.txt keine Google-Initiative ist und Google nicht plant, das Format zu unterstützen. John Mueller hatte sie schon im Frühjahr 2025 mit dem längst ignorierten Keywords-Meta-Tag verglichen, weil sie vom Seitenbetreiber kontrolliert und damit letztlich eine Selbstauskunft ist, die man genauso gut direkt an der Seite überprüfen könnte. Im Dezember 2025 tauchte eine llms.txt kurz in Googles eigener Entwickler-Dokumentation auf und war am selben Tag wieder weg, allem Anschein nach ein automatischer Rollout des Redaktionssystems, keine Kursänderung. Wie es mit der Nutzung auf Anbieterseite wirklich steht, ist unübersichtlich: formell als Standard zugesagt hat es keiner, Google lehnt ausdrücklich ab, OpenAI hat sich nicht festgelegt. Von einzelnen Anbietern heißt es, sie berücksichtigten das Format in ihren Abläufen, aber diese Angaben stammen aus SEO-Quellen, nicht aus offiziellen Hersteller-Mitteilungen. Ich verkaufe das also nicht als Wundermittel. Es schadet nicht, es ist schnell gemacht, aber es ist eher eine Höflichkeitsgeste an Maschinen als ein Garant für irgendetwas.

Anders sieht es bei der strukturierten Identität aus. Hier ist aus „ich habe da mal was erwähnt“ etwas Substantielles geworden. Nicht weil ein Schema magisch wirkt, sondern weil mich der Prozess gezwungen hat, meine eigene Online-Existenz aufzuräumen, Widersprüche zu beseitigen und nur noch Prüfbares zu behaupten. Das wäre auch ohne jede Maschine eine gute Übung gewesen.

Und meine selbstironische Prognose von damals, dass klassische Blogs seltener werden? Die stimmt und stimmt nicht. Dieser Blog schreibt weiter, sehr aktiv sogar. Aber die Verteilung verschiebt sich tatsächlich. Neue Beiträge gehen über ActivityPub ins Fediverse und über eine Brücke nach Bluesky, nicht mehr in erster Linie über die Suchmaschine zum Leser. Insofern stützt die Realität die Prognose, sie widerlegt nur das „Blog ist tot“-Pathos. Es ist kein Sterben, es ist ein Umzug der Verteilwege.

Hat die Maschinenlesbarkeit messbar etwas gebracht? Differenziert betrachtet ja und nein. Die KI-Crawler holen die strukturierten Daten nachweislich ab, das war meine Anfangsprognose und sie hat sich bestätigt. Aber Abruf ist nicht gleich Klick. Die Klick-Konversion aus diesen Kanälen ist niedrig. Das ist kein Widerspruch, das ist genau der Punkt des ganzen Themas, siehe Zero-Click weiter oben. Sichtbar zu sein und besucht zu werden sind zwei verschiedene Dinge geworden.

Damit zum Kerngedanken, der für mich am Ende übrig bleibt: Man kontrolliert nicht, ob eine KI einen zitiert. Man kontrolliert nur, ob man zitierbar ist. Das ist die ganze Aufgabe. Fehlende oder widersprüchliche Daten machen ein Zitat fast unmöglich. Saubere, konsistente, belegbare Daten machen es wahrscheinlicher. Mehr Versprechen gibt es nicht, und jeder, der mehr verspricht, verkauft etwas. SEO ist dabei übrigens nicht tot, das wäre Übertreibung. Technische Hygiene, Crawlbarkeit und gute Inhalte bleiben die Basis. Es verschieben sich nur die Gewichte.

Vor einem halben Jahr habe ich geschrieben, ich sei gespannt, was passiert. Daran hat sich nichts geändert. Ich weiß heute ein paar Dinge genauer, ich habe meine eigene Anfangs-Euphorie an einigen Stellen kassiert, und ich habe vor allem gelernt, dass der ehrlichste Weg auch der robusteste ist. Ob das langfristig der richtige war, weiß ich immer noch nicht. Ich bin weiterhin gespannt.

Siehe auch: Von SEO zu AEO, warum llms.txt, JSON-LD und Answer Engines das Web verändern (der Vorgängerpost mit der ursprünglichen Wette).

Gegenmeinung, eigene Erfahrungen oder ein Befund, der meinem widerspricht? Immer her damit, einfach fragen.

grav-plugin-fediverse-publisher: ActivityPub für Grav-Blogs, neun Iterationen bis v0.1.0

Illustration eines Grav-Plugin-Adminbereichs, von dem ActivityPub-Beiträge über ein föderiertes Netzwerk an verschiedene Fediverse-Instanzen verteilt werden.

WordPress hat seit Jahren das wunderbare wordpress-activitypub von Matthias Pfefferle und Automattic. Damit wird ein WordPress-Blog zu einem nativen Mastodon-Account, jeder Beitrag landet in den Timelines der Follower, Likes und Reposts kommen zurück. Für Grav gab es genau das nicht. Die Website meiner Frau läuft auf Grav, sie schreibt hin und wieder fachlich, und seit längerem wollte ich diesen Blog vernünftig ins Fediverse bringen. Bisher half feed2toot, also RSS in Mastodon-Posts übersetzt, das funktioniert zwar, ist aber kein ActivityPub. Keine Profilseite, keine Follower-Beziehung, kein nativer Hashtag-Index, keine saubere Article-Card. Also will ich versuchen selbst etwas zu schreiben. Das Ergebnis heißt grav-plugin-fediverse-publisher, ist seit ein paar Tagen als v0.1.0 draußen und läuft auf ihrer Webseite produktiv.

Grav-Admin Plugin-Liste mit aktiviertem Fediverse Publisher v0.0.9 zwischen Form, Login und Markdown Notices
Im Grav-Admin reiht sich der Fediverse Publisher unaufgeregt zwischen Form, Login und Markdown Notices ein. Aktiviert, Version 0.0.9 im Screenshot, das ist genau die Iteration in der es zum ersten Mal richtig sauber durchlief.

Das Repo liegt auf GitHub unter Kernel-Error/grav-plugin-fediverse-publisher (MIT). Release v0.1.0 inklusive Changelog gibt es hier. Eine Vorstellung mit Bitte um Feedback liegt im Grav-Discourse-Forum: I tried to build an ActivityPub plugin for Grav.

Warum überhaupt, und warum jetzt

Im Grav-Forum gibt es einen Thread aus dem Jahr 2019: Grav & ActivityPub. Dort hat über sechs Jahre hinweg dreimal jemand explizit nach genau dieser Funktion gefragt. Antworten gab es kaum, Code gar nicht. Das ist die Sorte Lücke die ich charakteristisch finde für kleinere Open-Source-Ökosysteme: alle finden es gut, niemand setzt sich hin. WordPress hatte über Jahre dieselbe Situation, bis Pfefferle das wordpress-activitypub-Plugin gebaut und Automattic später übernommen hat. Für Grav ist niemand vorbeigekommen.

Bei mir kam dazu, dass ich einen echten produktiven Anwendungsfall habe. Nicole, meine Frau, betreibt einen Grav-Blog im Rahmen ihrer weiteren Ausbildung. Inhaltlich vollkommen anders gelagert als alles was hier üblicherweise federiert, aber genau deshalb auch ein wertvoller Stress-Test: andere Zielgruppe, andere Empfängerinstanzen, anderes Hashtag-Vokabular. Wenn das Plugin dort sauber läuft, läuft es überall.

Phase 0: Machbarkeitscheck mit Scope-Disziplin

Bevor eine Zeile produktiver Code entstand, gab es eine Phase 0: gibt es die Lücke wirklich, ist das PHP-Bibliotheks-Ökosystem für ActivityPub brauchbar, und schaffe ich die langfristige Wartung als Solo-Entwickler? Verdikt: ja, aber nur als Broadcast-only-MVP. Konkret heißt das, der Blog kann senden, also Beiträge als Create-Activity an alle Follower-Inboxes ausliefern, sowie auf Standard-ActivityPub-Queries antworten (Actor-Profile, Outbox, Followers, NodeInfo, WebFinger, HTTP-Signaturen rein und raus). Nicht im Scope sind Replies als Kommentare zurück in Grav, Multi-Actor-Setups, Authorized Fetch und Theme-seitige Patches. Diese Disziplin durchzuhalten war im Verlauf ein paar Mal anstrengend, hat sich aber durchweg ausgezahlt.

Vor der ersten Code-Zeile entstanden vier ADRs (Architecture Decision Records) zu Storage, HTTP-Signaturen, asynchronem Push und Content-Negotiation. Diese ADRs habe ich zweimal kritisch gegenlesen lassen. In Runde zwei kamen drei Lücken zum Vorschein, die ich allein übersehen hätte. Die ungemütlichste: landrok/activitypub verifiziert beim Parsen verlinkter Objekte still im Hintergrund Netzwerk-I/O. Das hätte die gesamte SSRF-Härtung des Inbound-Pfads ausgehebelt. Konsequenz: landrok nur für die Erzeugung von AS-2.0-Objekten und WebFinger benutzen, der gesamte sicherheitsrelevante Inbound-Pfad geht nicht mehr durch landrok. Phpseclib übernimmt die Krypto direkt, der HTTP-Signature-Verifier ist eigene Implementierung. Insgesamt sind über die ADR-Reviews und ein nachgelagertes Review der ersten Implementierung acht sicherheitsrelevante Fixes eingeflossen, bevor das Plugin produktiv ging.

Konfigurationsseite des Fediverse-Publisher-Plugins im Grav-Admin mit Local-Actor-Feldern, Blog-Scope, Article-Threshold und Canonical-Host-Eintrag
Die ganze Konfiguration auf einer Seite. Lokaler Actor, Avatar- und Header-URL, ein Blog-Path-Filter und der Canonical Host. Das war einer der früh festgelegten Designgrundsätze: ein Admin-Screen, alles in Klartext, kein eigenes UI-Framework.

Stack-Entscheidungen

Die wichtigsten Bauklotz-Entscheidungen in Stichworten:

  • landrok/activitypub für die AS-2.0-Objekte und WebFinger
  • phpseclib/phpseclib für die Krypto direkt, ohne Umweg über landrok auf dem Inbound-Pfad
  • HTTP-Signaturen nach draft-cavage-12, für Sign und Verify selbst implementiert
  • SQLite per PDO im WAL-Modus für Follower-Tabelle und Push-Queue
  • Synthetic Endpoints via onPluginsInitialized, ein Pattern aus grav-plugin-form, mit PSR-7-Responses statt Symfony HttpFoundation
  • Outbound-Queue mit echtem Idempotenz-Anker per INSERT OR IGNORE auf (activity_id, recipient_inbox)
  • Retry-Schedule 1m / 5m / 30m / 2h / 12h / 24h mit Jitter, Cap bei sieben Versuchen, danach status='dead'
  • SSRF-gehärteter keyId-Fetch: HTTPS-only, Block privater IP-Bereiche, keine Redirects, 64 KiB Response-Cap, Negative-Cache
  • Strikte Identity-Bindung: keyId, publicKey.owner und activity.actor müssen nach Normalisierung auf dieselbe Actor-URL zeigen

Wer den vollen technischen Aufschrieb sucht, findet die ADRs als Markdown-Dateien im Repo unter docs/adr/. Die sind als Lese-Doku formuliert, nicht als Mitschrift.

Neun Iterationen in zwei Tagen, jede mit echtem Erkenntnisgewinn

Wenn jemand fragt, warum ein scheinbar geradliniges Plugin neun Patch-Versionen gebraucht hat: weil jede einzelne dieser Versionen eine echte Produktions-Erkenntnis war, die ich nicht in einem theoretischen Vorab-Design hätte antizipieren können. Hier in kompakt, weil die Liste für sich spricht:

  • v0.0.1: Boot-Crash auf der Produktion. Composer hat psr/log v3 reingezogen, Grav 1.7 bringt aber v1. Resultat: ganze Site HTTP 500, und das obwohl das Plugin noch gar nicht aktiviert war. Grav ruft autoload() bei jedem installierten Plugin schon beim Boot auf.
  • v0.0.2: psr/log auf ^1.1 gepinnt, defensives try/catch im Plugin-Entry, Preflight-Check via explizitem require_once statt Autoload-Pfad.
  • v0.0.3: SQL-Quoting-Falle. PHP 8.3 mit aktuellem libsqlite parst Double-Quoted-Strings als Identifier, nicht als String-Literale. Klassischer Fall, früh genug erkannt, danach konsequent Single-Quotes überall. Im selben Schritt das Listing-Page-Filter geschärft, dazu Actor-Endpoints fertig gemacht.
  • v0.0.4: Router-Wiring vergessen. Die Routen für followers und following waren im Code zwar vorhanden, aber nicht registriert. Mastodon zeigte hartnäckig „0 followers“ obwohl reale Follower in der DB lagen. Plus Diagnostics-Logging für Outbound-401-Antworten, damit ich die nächste Klasse von Bugs überhaupt sehen konnte.
  • v0.0.5: psr/log-Konflikt war tiefer als gedacht. Die Default-Einstellung autoload(prepend=true) hat den Plugin-eigenen Vendor-Pfad über Grav 2.0s gebundeltes psr/log v3 gezogen, was unter Grav 2.0 in ganz neue Fehlerbilder kippte. Fix: prepend=false. Im selben Schritt ein neues, mandatorisches canonical_host-Config-Feld eingeführt, sonst signiert die Cron mal eben keyId=http://localhost/..., was kein Empfänger akzeptiert. Ab hier läuft im Dev parallel ein zweiter Container mit Grav 1.7.52 und PHP 8.3.31 (matcht die Produktion byte-genau). Dieser Dual-Grav-Stack fängt seitdem alle Bugs der Klasse „passt auf einer Major-Version, kracht auf der anderen“ lokal ab, statt sie über Nicole laufen zu lassen.
  • v0.0.6: AS-2.0-Spec-Violation. Bei rückdatierten Posts war updated < published, was strenge ActivityPub-Implementierungen wie GoToSocial und Pleroma silently droppen. HTTP 202 vom Empfänger, aber kein Eintrag in der Timeline. Fix war eine Zeile Clamp, der Fehler dahinter aber lehrreich: ein 2xx-Status sagt zu Federation-Erfolg gar nichts.
  • v0.0.7: der echte Showstopper. Grav-Admin 1.10 und neuer routet Page-Saves nicht mehr durch onAdminAfterSave, sondern durch onFlexAfterSave (über das Flex-Objects-Plugin). Folge: User speichert einen Beitrag im Admin, der Save feuert, das Plugin macht nichts, kein Queue-Eintrag, keine Federation. Im selben Release dann auch AS-2.0-Hashtag-Federation hinzugefügt. Hashtags sind in der Praxis der primäre Discovery-Pfad für einen Actor mit null Followern, weil Mastodons Hashtag-Index fremde Actors automatisch aufnimmt. Drittens kam ein neues broadcast:post-CLI-Kommando rein, das einen einzelnen Pfad in die Queue legt, für Recovery und Backfill.
  • v0.0.8: Hotfix nach Hotfix. Meine Diagnostik-Zeile in v0.0.7 hat iterator_to_array($event) auf RocketThemes Event-Klasse aufgerufen. Die implementiert ArrayAccess, aber nicht Traversable. Ergebnis: TypeError bei jedem Admin-Save, HTTP 500, Nicole sah eine 624 KB große Fehlerseite. Diese Klasse Bug ist genau die Sorte, die das „passt schon“-Gefühl belohnt. Fix: iterator_to_array raus, Logik in eine testbare PageSaveDiagnostics-Klasse extrahiert, 13 neue Unit-Tests die alle möglichen Event-Object-Shapes durchfuzzen. Wenn schon dasselbe Problem mehrfach, dann wenigstens mit Tests dagegen.
  • v0.0.9: stale Collection. Auch nach v0.0.8 hat der Broadcaster bei frischen Posts nicht gefeuert. Ursache: $pages->find() arbeitet auf einem Pre-Save-Snapshot der Pages-Collection. Frisch gespeicherte Inhalte sind zu dem Zeitpunkt noch nicht drin. Fix: eine neue findByPage(PageInterface)-Methode, die direkt das Event-Payload nimmt, statt durch die stale Collection zu spazieren. Plus per-bail INFO-Logging: jede Regression dieser Klasse ist seither ein einziger grep entfernt von actionable. Mit v0.0.9 lief es dann durch, sauber, unter Last, an echten Followern auf realen Instanzen.

v0.1.0 ist im Wesentlichen v0.0.9 mit ehrlichem README, einem Changelog der die ganze Geschichte oben dokumentiert, und dem Vorstellungspost im Grav-Forum. Mir war wichtig, das Ding nicht als „stable“ zu vermarkten. Es ist Early Access, ich bin solo, ich brauche Tester.

Methodisch: drei Dinge die den Unterschied gemacht haben

Aus den neun Iterationen kann man ein paar Schlüsse ziehen, die ich selbst spannender finde als die einzelnen Bugs.

Erstens, der Dual-Grav-Dev-Stack. Zwei Container, derselbe Plugin-Source, einmal Grav 1.7.52 und einmal Grav 2.0 RC. Eingerichtet als Reaktion auf das psr/log-Drama in v0.0.5. Seitdem werden Bugs der Klasse „passt unter Major X, crasht unter Major Y“ lokal sichtbar, bevor sie Nicole erreichen. Das ist im Tagesgeschäft die langweiligste Investition, die ich gemacht habe, und gleichzeitig die wertvollste.

Zweitens, ein dedizierter Production-Feedback-Loop. Deployment und Smoke-Test auf der Live-Site liefen über einen sauber getrennten Track. Dort wird das Plugin installiert, gegen mastodon.social und bonn.social geprüft, eine Feedback-Notiz zurück in das Planungs-Verzeichnis geschrieben. Lokal wird daraufhin triagiert, gefixt, die Patch-Version gebumpt, gepusht. Beide Tracks haben klare Scopes: Deploy-Zugang und Auth bleiben dort wo das auch tatsächlich passiert, Architektur-Wissen und Code-Änderungen im anderen Bereich. Klingt nach Overhead, war aber der Grund, warum ich Bugs wie das onFlexAfterSave-Routing-Problem aus v0.0.7 überhaupt in akzeptabler Zeit gefunden habe: der Live-Blog war der Detektor, nicht meine lokale Dev-Maschine.

Drittens, externes Gegenlesen an sicherheitskritischen Stellen. HTTP-Signaturen, Inbox-Verifikation, SSRF-Härtung, Identity-Binding. Überall wo ein Fehler in einer realen Verwundbarkeit landet, ging der Code durch eine zusätzliche Review-Runde mit eigenem Blickwinkel. Vier potenzielle Regressionen sind dabei aufgefallen, die ich allein gemacht oder übersehen hätte. Ein zweites Augenpaar ersetzt kein Sicherheits-Audit, hebt aber das untere Drittel der „hätte mir auch auffallen können“-Klasse Fehler ziemlich zuverlässig nach oben.

Wie das für einen Mastodon-User aussieht

Mastodon-Profil von @nicole@www.beratung-rheinbach.de mit Header-Bild, Bio, Registrierungsdatum, 12 Beiträgen und 2 Followern
Aus Sicht eines Mastodon-Users ist das ein ganz regulärer Account: Header-Bild, Avatar, Bio, Beiträge, Follower-Counter, Folgen-Button. Nichts verrät, dass dahinter kein Mastodon-Server steht, sondern ein Grav-Blog mit ein paar tausend Zeilen PHP drumherum.

Genau das war das Ziel. Ein Mastodon-User abonniert @nicole@www.beratung-rheinbach.de, drückt auf Folgen, und sieht ab dem nächsten Beitrag jeden neuen Artikel direkt in der Home-Timeline. Avatar und Header werden gezogen, Bio steht da, die Profilseite zeigt alle bisher federierten Beiträge, und die Hashtags am Ende jedes Posts landen im Hashtag-Index der Empfängerinstanz. Letzteres ist nicht kosmetisch. Für einen Account mit null Followern ist der Hashtag-Index der primäre Discovery-Pfad, weil Mastodon dabei auch Posts fremder Actors mitnimmt, die zum gleichen Tag federieren.

Einzelner Blogpost zu Pausen aus dem Grav-Blog Beratung Rheinbach, in der Mastodon-Timeline mit Featured Image, Excerpt und Hashtags beratung, systemischeberatung und pausen
Ein konkreter Artikel in der Mastodon-Timeline. Header-Bild aus dem Grav-Asset, Titel, Excerpt, drei Hashtags und ein Klick-Link zurück auf den Originalbeitrag. Nicht beeindruckender als bei wordpress-activitypub, und genau das ist der Punkt.

Die End-to-End-Latenz vom Speichern im Grav-Admin bis zur Anzeige in der Heimat-Timeline eines Followers liegt bei rund 15 Sekunden. Das ist der asynchrone Push aus der SQLite-Queue plus der üblichen ActivityPub-Delivery. Fällt eine Instanz aus, retryed das Plugin nach Schedule. Erreicht der Push nach sieben Versuchen kein 2xx, markiert die Queue den Eintrag als dead und lernt aus dem letzten Statuscode, ob die Instanz dauerhaft weg ist oder ob es nur ein vorübergehendes Problem war.

Was geplant ist, und was bewusst draußen bleibt

Roadmap für v0.2, ohne festes Datum:

  • Update-Activity bei Re-Saves. Auf Mastodon kosmetisch, für Pleroma und Misskey möglicherweise relevant, da diese die Post-Card neu ziehen.
  • Delete-Federation. Aus Compliance-Sicht eine sinnvolle Komplettierung.
  • push:purge-old-activities als Housekeeping-CLI, damit die SQLite-Datei nicht ewig wächst.
  • Breitere Peer-Tests gegen Friendica und Misskey, um die HTTP-Signatur-Verifikation gegen weitere Implementierungen abzusichern.

Was explizit nicht kommt, jedenfalls nicht als integraler Plugin-Bestandteil:

  • Replies aus dem Fediverse als Kommentare zurück in den Grav-Blog. Sehr beliebte Wunschfunktion, aber sie sprengt das Broadcast-only-Scope und bringt eine ganze eigene Klasse von Spam-, Moderations- und Persistenz-Fragen mit, die ich nicht halbgar lösen will.
  • Multi-Actor pro Grav-Instanz. Ein Blog ist ein Actor, fertig.
  • Authorized Fetch. Ist aktuell ohnehin nur eine Untermenge der Mastodon-Welt, und die Kosten in Komplexität stehen nicht im Verhältnis zum Nutzen für ein Plugin in diesem Stadium.
  • Theme-seitige Patches. Das Plugin hängt sich nicht in die Twig-Templates des Blogs.

Die nächsten Schritte sind bewusst reaktiv. Statt einer langen Vorab-Roadmap warte ich, was aus der Community zurückkommt. Erste Reaktion im Discourse-Thread: ein User schlägt Software-Release-Changelogs als zweiten Anwendungsfall vor. Anderer Content-Shape als ein normaler Blog-Broadcast, aber technisch derselbe Page-Save-zu-Create-Activity-Pfad. Notiert für v0.2. Im alten 2019er-Thread habe ich ebenfalls einen Cross-Link gesetzt, damit Leute die nach Grav und ActivityPub googeln in der ersten Trefferzeile beim Repo landen statt bei sechs Jahre alten Fragen ohne Antwort.

Ehrliche Einordnung

Das Plugin läuft, aber es läuft auf einer Grav-Instanz unter genau einer Konfiguration mit zwei test Followern auf zwei Mastodon-Instanzen. Das ist ein winziger Ausschnitt aus dem, was Fediverse so an Implementierungen, Versionen und Edge-Cases zu bieten hat. Ich habe gegen die ActivityPub-Spec implementiert, gegen die HTTP-Signature-Drafts gelesen und gegen Mastodons faktisches Verhalten validiert. Pleroma- und Misskey-Spezifika sind nur sekundär berücksichtigt, Friendica gar nicht. Wer das Plugin auf einer eigenen Grav-Installation ausprobiert und gegen seine Lieblings-Mastodon-Heimat-Instanz federiert, hilft mir mehr als jeder weitere Unit-Test, den ich allein schreiben kann.

Wenn etwas nicht läuft, ist ein Issue im Repo der direkteste Weg. Eine Antwort im Forum-Thread erreicht zusätzlich auch andere Grav-Anwender die womöglich Ähnliches sehen. Über die Kontaktseite hier auf dem Blog komme ich genauso an Mails.

Bleibt eine kleine Beobachtung für alle, die sich an ähnliche Projekte heranwagen. Zwei Tage Iterationen, neun Patch-Releases, ein Production-Inzident mit der 624-KB-Fehlerseite. Das ist genau der Realismus, den ein Plugin im ersten echten Einsatz mitbringt. Es wäre bequemer, das im Changelog wegzulassen und v0.1.0 als unbefleckte erste Veröffentlichung zu inszenieren. Mir war es wichtiger, die Story so zu erzählen, wie sie ablief, weil ich diesen Schreibstil bei anderen Open-Source-Projekten selbst sehr schätze.

Siehe auch: ts3level (anderes Solo-Projekt, andere Domain, gleicher Workflow).

Fragen, Kommentare, Bug-Reports, oder einfach Lust auf einen kurzen Austausch zum Plugin gerne über die fragen-Seite oder direkt als Issue im Repo.

© 2026 -=Kernel-Error=-RSS

Theme von Anders NorénHoch ↑