# Headless mit Cockpit und JSON

URL: https://www.mikebild.dev/de/blog/headless-json-fuer-andere-systeme/

ContentKit liefert unter `/cockpit` eine Verwaltungsoberfläche für Menschen aus und bleibt ein Headless-System. Headless bezeichnet hier die Trennung von Inhaltsmodell und Ausgabekanal, nicht das Fehlen einer Oberfläche. Der veröffentlichte Stand existiert einmal und wird in mehrere Projektionen übersetzt: gerendertes HTML für Browser, geschütztes JSON für Systeme über die Read API. Das Cockpit gibt Menschen über dieselbe API und dasselbe Inhaltsmodell Zugriff; was es als veröffentlicht zeigt, ist genau dieser eine Stand.

*Teil 6 der Serie „Contentkit – Publishing für Maschinen“.*

Diese Begriffsklärung hat eine praktische Konsequenz. Eine zweite Redaktionsansicht mit eigener Datenhaltung neben dem API-System würde einen zusätzlichen Synchronisationspfad und mögliche Zustandsabweichungen erzeugen, die bei jedem Auseinanderlaufen geklärt werden müssten. Wer headless als getrennte Projektion versteht, stellt stattdessen eine andere Frage: Welche Abnehmer brauchen welchen Vertrag über denselben Veröffentlichungsstand?

## Ein Vertrag für Maschinen, kein Nebenprodukt der Website

Autorisierte Systeme lesen den veröffentlichten Stand über eine eigene Read API. Sie liefert die Liste veröffentlichter Inhalte und einzelne Dokumente als JSON und bindet jeden Zugriff an den Scope `content:read`. Einen zweiten anonymen JSON-Ausgabekanal auf der öffentlichen Domain gibt es bewusst nicht: Die öffentliche Website bedient Browser, serverseitige Abnehmer erhalten über die geschützte Lese-API JSON. Beide Wege zeigen ausschließlich veröffentlichte Revisionen.

Ein Agent, der freigegebene Anleitungen als Grounding liest, braucht keinen Publisher- oder Admin-Schlüssel. Für die Read API ist `content:read` erforderlich; ob schon das Lesen ein relevanter Schaden wäre, hängt davon ab, wie vertraulich die geschützten Inhalte sind. Für reine Lesezugriffe gehört deshalb ein eigener Schlüssel mit `content:read` angelegt, kein wiederverwendeter Publisher-Schlüssel.

## Drei Zugriffsmuster: Liste, Einzeldokument, Suche

Die Read API kennt drei wesentliche Zugriffsmuster. Die Liste verschafft einen Überblick über die veröffentlichten Inhalte, das Einzeldokument liefert einen konkreten Inhalt, die serverseitige Suche findet Inhalte aus demselben veröffentlichten Stand. Liste und Einzeldokument kommen als JSON, der Zugriff ist an den Scope `content:read` gebunden.

```bash
curl "$CONTENTKIT_URL/v1/sites/$SITE/published" \
  -H "Authorization: Bearer $CONTENTKIT_READ_API_KEY"
```

Welche Filter, Felder und Grenzwerte im Einzelnen gelten, definiert der API-Vertrag im Repository. Architektonisch entscheidend ist eine andere Zusage: Alle drei Zugriffsmuster zeigen ausschließlich den veröffentlichten Stand; Entwürfe und historische Revisionen bleiben ausgeschlossen.

## Das Einzeldokument: nur die veröffentlichte Revision

Ein einzelnes Dokument liefert die Read API ausschließlich in seiner veröffentlichten Fassung. Ein Entwurf oder eine beliebige historische Revision lässt sich darüber nicht abrufen, selbst wenn ein Client gezielt danach fragt. Entwürfe gehören nicht zu diesem Lesevertrag. Für Abnehmer bedeutet diese Grenze Verlässlichkeit: Was aus der Read API kommt, ist freigegeben – ein Agent kann es als Grounding verwenden, ohne versehentlich einen halbfertigen Text zu erwischen.

## ETags: schwach für die Liste, stark für das Dokument

Die beiden Repräsentationen erhalten unterschiedliche ETag-Semantiken, und der Unterschied ist architektonisch begründet. Die Listenrepräsentation verwendet eine schwache ETag-Bindung an die Veröffentlichungs-Epoche: Die Liste ist eine abgeleitete Sicht, deren Gültigkeit sich mit jedem neuen Veröffentlichungsstand ändert, auch wenn einzelne Einträge unverändert bleiben. Ein einzelnes Dokument erhält dagegen einen starken ETag über den Quellhash seiner Revision – seine Identität hängt an genau dieser unveränderlichen Quelle, nicht an der Epoche der Website.

Für Abnehmer sind diese ETags die Validatoren, mit denen sich Änderungen erkennen lassen, ohne Inhalte erneut zu übertragen und zu verarbeiten. Ein sinnvoller Ablauf prüft zuerst den Listen-ETag; eine geänderte Liste ist das Signal für einen Abgleich. Der Client sendet dann in bedingten Einzelabrufen den jeweils gespeicherten Dokument-ETag als `If-None-Match` mit und verarbeitet nur die Antworten, deren Inhalt sich tatsächlich geändert hat. Unveränderte Dokumente beantwortet der Server mit `304 Not Modified` ohne Response-Body.

```mermaid
sequenceDiagram
    participant P as Abnehmer
    participant A as ContentKit Read API
    participant C as Lokaler Cache
    P->>A: Liste mit gespeichertem ETag prüfen
    alt Liste unverändert
        A-->>P: kein neuer Stand
        P->>C: vorhandenen Stand behalten
    else Liste geändert
        A-->>P: neue Liste und neuer ETag
        loop je gelistetes Dokument
            P->>A: Einzelabruf mit If-None-Match (gespeicherter Dokument-ETag)
            alt Dokument unverändert
                A-->>P: 304 Not Modified, kein Response-Body
            else Dokument geändert
                A-->>P: veröffentlichtes Dokument als JSON
                P->>C: Eintrag ersetzen
            end
        end
    end
```

## Suche aus demselben Stand

Die serverseitige Suche verbindet ausschließlich veröffentlichte Revisionen; Entwürfe bleiben durch die Datenbankfunktion unsichtbar – eine strukturelle Garantie, kein Filter im Anwendungscode. Öffentliche Sites behalten daneben ihren statisch gebauten Suchindex: Der Website-Besucher sucht ohne Management-Zugang im statischen Index, ein Supportdienst oder Agent sucht serverseitig mit einem begrenzten Leseschlüssel. Beide Suchwege schließen Entwürfe aus und basieren auf veröffentlichten Revisionen; Ranking und Suchtechnik können abweichen.

JSON, gerendertes HTML, Suche und Cockpit müssen denselben veröffentlichten Stand abbilden. Weichen die Stände ab, können nachgelagerte Systeme veraltete Revisionen indexieren oder ausliefern.

## Das Cockpit liest dieselbe Domäne

Das Cockpit unter `/cockpit` ist der menschliche Zugang zu genau diesem Modell. Es verwendet dieselbe API und dasselbe Inhaltsmodell wie die maschinellen Zugänge – kein separates Inhaltsmodell für die veröffentlichte Sicht, das mit der API abgeglichen werden müsste. Was ein Mensch im Cockpit als veröffentlicht sieht, ist derselbe Zustand, den die Read API als JSON liefert und den die öffentliche Website als HTML zeigt.

Headless heißt bei ContentKit also nicht, dass die Oberfläche fehlt oder die Website abgeschaltet wäre. Es heißt, dass keine Projektion – weder Cockpit noch Website noch JSON – die Quelle ist. Quelle ist der veröffentlichte Stand; jede veröffentlichte Sicht ist daraus abgeleitet.

## Synchronisation braucht einen gespeicherten Stand

Aus Client-Sicht empfiehlt es sich, neben den Inhalten auch den letzten bestätigten Synchronisationsstand zu speichern: den Listen-ETag und die Dokument-ETags der übernommenen Revisionen. Erst wenn alle geänderten Dokumente erfolgreich verarbeitet wurden, wird dieser Synchronisationsstand verschoben. Bricht ein Lauf nach der Hälfte ab, bleibt der alte Stand bestehen; der nächste Versuch fragt dieselben Änderungen erneut an und überspringt bereits gespeicherte Dokumente.

Auch der Umgang mit Fehlern ist eine Client-Entscheidung. Vorübergehende Fehler wie `429` oder `5xx` lassen sich mit begrenztem Abstand wiederholen; `401` und `403` deuten auf Konfigurationsfehler und sind kein Wiederholungsfall. Bleibt ein zuvor gelisteter Inhalt beim Einzelabruf aus, kann er zwischen Liste und Abruf zurückgezogen worden sein – der lokale Eintrag wird dann beim nächsten vollständigen Listenabgleich entfernt.

Trotzdem bleibt ein gelegentlicher Vollabgleich sinnvoll, weil ein fehlerhafter Client oder ein verlorener Synchronisationsstand sonst dauerhaft unbemerkt bliebe. Ein nächtlicher oder wöchentlicher Lauf liest die vollständige Liste, vergleicht sie mit dem lokalen Bestand, entfernt nicht mehr veröffentlichte Einträge und muss unveränderte Dokumente dank bedingter Einzelabrufe mit den starken ETags nicht erneut übertragen und verarbeiten. Stößt ein Ereignismechanismus einen Lauf an, bleibt die anschließend gelesene Liste die maßgebliche Sicht – wer den Gesamtzustand allein aus Ereignissen rekonstruiert, verliert ihn beim ersten verpassten Ereignis.

## JSON oder gerendertes HTML bewusst auswählen

Welche Projektion ein Abnehmer verarbeitet, ist eine bewusste Entscheidung. Verarbeitet der Abnehmer Struktur und Inhalt selbst – ein Agent, ein weiterer Generator, ein Textvergleich –, ist die strukturierte JSON-Sicht der Read API geeigneter als das HTML der öffentlichen Website, aus dem Navigation und Fußzeile erst wieder herausgelöst werden müssten. Das gerenderte HTML ist die richtige Wahl, wenn ein Abnehmer die fertige Darstellung direkt zeigen will und deren Stil und erlaubte Elemente kennt.

Für einen Vektorindex vermeidet die strukturierte Sicht zusätzliches Navigations- und Layout-Markup; ob sie im konkreten Projekt die bessere Quelle ist, hängt von Renderer und Zielsystem ab. Bei einem Wechsel des Embedding-Modells werden nur die Vektoren neu berechnet, während die Inhalte unverändert aus der Read API stammen – eine technische Indexmigration wird so nicht zur redaktionellen Migration.

Ein abgeleiteter Cache – Vektorindex, mobile Datenbank, fremder Renderer – sollte den starken Dokument-ETag und die öffentliche Quellenadresse mitführen, damit Antworten und Anzeigen auf die Veröffentlichung zurückverweisen können. Die Read API bleibt die Quelle für den aktiven Veröffentlichungsstand; der Cache ist Ableitung, nicht die maßgebliche Sicht.

## Praxisbeispiel: eine Support-App aus demselben Veröffentlichungsstand

Eine mobile Support-App soll auch bei schlechter Verbindung die letzten freigegebenen Anleitungen bereithalten. Ein serverseitiger Synchronisationsdienst prüft die veröffentlichte Liste über die Read API, sendet für die gelisteten Dokumente seine gespeicherten ETags in bedingten Einzelabrufen mit `If-None-Match` mit und verarbeitet nur die Antworten mit geändertem Inhalt. Das mobile Backend verteilt daraus ein kompaktes Paket an die Geräte; der Leseschlüssel verlässt die Serverseite nie.

Die Konsequenz für die Redaktion: Eine Korrektur wird einmal veröffentlicht und kann danach von den jeweiligen Build- und Synchronisationsprozessen der Website, der App und jeder weiteren Projektion übernommen werden – ohne zweite Redaktion, ohne HTML-Scraping, ohne Abgleichskripte zwischen zwei Datenhaltungen. Dieselbe Read API kann auch ein interner Agent verwenden, der ausschließlich freigegebene Betriebsanleitungen als Grounding liest, ebenso eine Partnerwebsite mit eigenem Design oder eine CI-Pipeline, die nach jeder Veröffentlichung die Vollständigkeit der Dokumentation prüft.

## Fazit

Stand August 2026 gilt für das aktuelle Repository: Maschinen lesen den veröffentlichten Stand über die geschützte Read API mit `content:read`. Wer einen weiteren Abnehmer anbindet, entscheidet über Format, minimalen Scope und Synchronisationsstrategie, nicht über eine neue Quelle.

## Weiterführende Quellen

- [ContentKit Read API in der Gesamtdokumentation](https://github.com/MikeBild/contentkit/blob/main/docs/llms-full.txt)
- [ContentKit Architektur: Read API und Suche](https://github.com/MikeBild/contentkit/blob/main/docs/ARCHITECTURE.md)
- [ContentKit OpenAPI-Vertrag](https://github.com/MikeBild/contentkit/blob/main/docs/openapi.json)
