API-Referenz
Die JSON-API von ki.karlkratz.com verwaltet die Seiten, die unter / sowie unter den jeweiligen Slug-URLs ausgeliefert werden. Diese Seite beschreibt jeden Endpoint, das Eingabe-Schema, mögliche Antworten und die Validierungsregeln, die serverseitig durchgesetzt werden.
Basisinformationen
| Aspekt | Wert |
|---|---|
| Basis-URL | https://ki.karlkratz.com/api/pages |
| Format Anfrage | application/json, UTF-8 |
| Format Antwort | application/json; charset=utf-8 |
| Authentifizierung | keine (Demo-System, durch Hetzner-Firewall öffentlich für 0.0.0.0/0 auf Port 443 erreichbar) |
| Versionierung | keine; das Schema steht im PHP-Quelltext unter /var/www/ki.karlkratz.com/src/ |
Erlaubte HTML-Tags im Feld content | h1, h2, h3, p, ol, ul, li, a (mit href und title), strong, table, thead, tbody, tr, th, td, pre, code |
Endpoints im Überblick
| Methode | Pfad | Zweck |
|---|---|---|
| GET | /api/pages | Liste mit Suche, Sortierung, Pagination |
| POST | /api/pages | Neue Seite anlegen, alle Felder erforderlich |
| GET | /api/pages/<slug> | Eine Seite lesen |
| PATCH | /api/pages/<slug> | Felder einzeln aktualisieren |
| PUT | /api/pages/<slug> | Alias auf PATCH, identische Semantik |
| DELETE | /api/pages/<slug> | Seite löschen |
Datenstruktur einer Seite
Jede Seite hat folgende Felder. Pflicht beim Anlegen sind slug, title, meta_description und content.
| Feld | Typ | Pflicht beim Anlegen | Regeln |
|---|---|---|---|
| id | Integer | nein | wird serverseitig vergeben (BIGSERIAL) |
| slug | String | ja | Regex ^[a-z0-9][a-z0-9-]{0,63}$, eindeutig |
| title | String | ja | 1 bis 200 Zeichen |
| meta_description | String | ja | 1 bis 200 Zeichen, SEO-Empfehlung 50 bis 160 |
| content | String (HTML) | ja | wird per DOMDocument auf die Tag-Whitelist gefiltert |
| created_at | ISO-8601-Timestamp | nein | serverseitig gesetzt |
| updated_at | ISO-8601-Timestamp | nein | Trigger aktualisiert bei jedem Schreibvorgang |
GET /api/pages, Liste lesen
Query-Parameter
| Parameter | Default | Erlaubt |
|---|---|---|
| q | leer | Volltext über slug, title, meta_description (case-insensitive Substring) |
| sort | updated_at | slug, title, meta_description, updated_at, created_at |
| dir | desc | asc, desc |
| limit | 50 | 1 bis 200 |
| offset | 0 | 0 oder positiv |
Beispiel
curl -sS 'https://ki.karlkratz.com/api/pages?q=mcp&sort=updated_at&dir=desc&limit=10'Antwortformat
{
"total": 7,
"items": [
{
"id": 16,
"slug": "mcp-anleitung",
"title": "MCP-Server bauen, die ki-web-Anleitung",
"meta_description": "...",
"content": "<h1>...</h1>...",
"created_at": "2026-05-21T03:33:52+02:00",
"updated_at": "2026-05-21T11:42:18+02:00"
}
]
}GET /api/pages/<slug>, eine Seite lesen
curl -sS https://ki.karlkratz.com/api/pages/mcp-anleitung | jq .Antwort: identische Struktur wie ein einzelnes Item aus dem Listen-Endpoint. Status 200 oder 404, wenn der Slug nicht existiert.
POST /api/pages, neue Seite anlegen
Request-Body
{
"slug": "neue-seite",
"title": "Neue Seite",
"meta_description": "Kurze Beschreibung der Seite, 50 bis 160 Zeichen.",
"content": "<h1>Überschrift</h1><p>Inhalt mit <strong>Hervorhebung</strong>.</p>"
}Antworten
201 Createdmit dem angelegten Datensatz inklusiveid,created_at,updated_at409 Conflictwenn der Slug bereits vergeben ist422 Unprocessable Entitybei Validierungsfehlern (zum Beispiel Slug-Regex verletzt, Titel zu kurz)
Beispiel
curl -sS -X POST https://ki.karlkratz.com/api/pages \
-H 'Content-Type: application/json' \
-d '{"slug":"hallo","title":"Hallo","meta_description":"Eine Begruessung.","content":"<h1>Hallo</h1>"}'PATCH /api/pages/<slug>, Felder ändern
Der Request-Body enthält nur die Felder, die geändert werden sollen. Nicht gelieferte Felder bleiben unverändert. PUT verhält sich identisch.
curl -sS -X PATCH https://ki.karlkratz.com/api/pages/hallo \
-H 'Content-Type: application/json' \
-d '{"title":"Hallo Welt"}'Antworten: 200 mit aktualisiertem Datensatz, 404 wenn der Slug nicht existiert, 422 bei Validierungsfehlern.
DELETE /api/pages/<slug>, Seite löschen
curl -sS -X DELETE https://ki.karlkratz.com/api/pages/halloAntwort:
{"deleted": "hallo"}Status 200 bei Erfolg, 404 wenn der Slug nicht existiert.
Fehlerformat
Fehlerantworten haben einheitliche Struktur:
{
"error": "validation",
"message": "Title length must be 1..200, got 0"
}| error | Status | Bedeutung |
|---|---|---|
| not_found | 404 | Slug existiert nicht, oder Pfad ist unbekannt |
| already_exists | 409 | POST mit bereits vergebenem Slug |
| validation | 422 | Eingabe verletzt eine Regel (Slug-Regex, Längen, Content-Sanitization) |
| method_not_allowed | 405 | HTTP-Methode ist auf diesem Pfad nicht unterstützt |
| internal | 500 | interner Fehler, Details stehen im Server-Log /var/log/frankenphp/access.log |
Content-Sanitization im Detail
Beim Schreiben (POST und PATCH) wird das Feld content per DOMDocument geparst und alle Tags ausserhalb der Whitelist werden entfernt. Das a-Tag akzeptiert nur die Attribute href und title; href muss ein gültiges Schema sein: http, https, mailto oder ein relativer Pfad beginnend mit / oder #. Andere Werte wie javascript: werden ohne Warnung entfernt.
Code-Beispiele innerhalb von pre oder code müssen vom Aufrufer mit HTML-Entities geliefert werden (< für <, > für >), damit der Sanitizer sie nicht als Markup missversteht.
Versionierung und Wiederherstellung
Jede Aenderung an einer bestehenden Zeile in pages archiviert die alte Version datenbankseitig vor dem Schreiben. Das passiert ueber einen PostgreSQL-Trigger pages_archive_trigger und ist fuer die Anwendung unsichtbar. DELETE folgt derselben Logik: die Zeile verschwindet aus pages, bleibt in pages_history erhalten und kann ueber den Restore-Endpoint zurueckgeholt werden. Es gibt keinen Weg, eine Aenderung oder Loeschung am Trigger vorbei zu fahren.
Aufbau der History-Tabelle
| Spalte | Typ | Bedeutung |
|---|---|---|
| history_id | Integer | Primaerschluessel der History-Tabelle |
| page_id | Integer | id der urspruenglichen Zeile in pages |
| slug, title, meta_description, content | wie pages | Snapshot des Zustands VOR der Mutation |
| created_at, updated_at | ISO-8601 | kopiert aus dem urspruenglichen Datensatz |
| archived_at | ISO-8601 | Zeitpunkt der Archivierung, also der Mutation auf pages |
| archive_op | update oder delete | ausloesendes Ereignis |
| archive_actor | String | wer hat geschrieben; bei API-Aufrufen php-api |
| request_id | String | 16-stellige Hex-ID pro PHP-Request, identisch zum HTTP-Header X-Request-Id |
GET /api/pages/<slug>/history
Liefert alle History-Eintraege fuer den angegebenen Slug, sortiert nach archived_at absteigend. Optionale Query-Parameter limit (1 bis 200, Default 50) und offset (ab 0, Default 0).
curl -sS https://ki.karlkratz.com/api/pages/<slug>/history | jq{
"slug": "<slug>",
"total": 3,
"items": [
{
"history_id": 7,
"page_id": 42,
"slug": "<slug>",
"title": "alter Titel",
"meta_description": "alte Beschreibung",
"content": "<p>alter Inhalt</p>",
"created_at": "2026-05-21T10:00:00+02:00",
"updated_at": "2026-05-21T10:05:12+02:00",
"archived_at": "2026-05-21T10:07:33+02:00",
"archive_op": "update",
"archive_actor": "php-api",
"request_id": "367c5efc0d1c"
}
]
}POST /api/pages/<slug>/restore
Stellt einen Slug aus der History wieder her. Ohne Body wird der zuletzt archivierte Snapshot dieses Slugs verwendet. Mit {"history_id": N} wird genau dieser Eintrag wiederhergestellt; gehoert er zu einem anderen Slug, antwortet der Server mit 404.
Wenn der Slug aktuell noch in pages existiert, wird die heutige Zeile vor der Wiederherstellung archiviert (UPDATE-Pfad). Wenn der Slug geloescht ist, geht der Restore als INSERT durch und erzeugt keinen zusaetzlichen History-Eintrag.
# Restore des juengsten Snapshots
curl -sS -X POST https://ki.karlkratz.com/api/pages/<slug>/restore \
-H 'Content-Type: application/json' -d '{}'
# Restore eines bestimmten Snapshots
curl -sS -X POST https://ki.karlkratz.com/api/pages/<slug>/restore \
-H 'Content-Type: application/json' -d '{"history_id": 7}'Antwort: 200 mit dem wiederhergestellten Datensatz; 404 wenn keine History fuer diesen Slug existiert oder die angegebene history_id nicht zu dem Slug gehoert.
Hard-Delete?
Die API bietet keinen Hard-Delete an. Wer einen Slug endgueltig entfernen will, muss zusaetzlich DELETE FROM pages_history WHERE slug = ... direkt auf der Datenbank ausfuehren. Das ist bewusst so getrennt, damit kein API-Aufruf versehentlich Daten unwiederbringlich loescht.
Konsumenten
- der MCP-Server ki-web (Python), siehe /mcp-ki-web
- das PHP-Frontend selbst, das die Daten ohne HTTP-Hop direkt aus der Geschäftslogik liest
- beliebige curl- oder httpx-Aufrufe mit
Content-Type: application/json
Weiterführend
- /mcp-vs-api-vs-cli, Einordnung MCP gegen API gegen CLI
- /mcp-anleitung, Implementierung des MCP-Adapters über dieser API
- /mcp-ki-web, Architektur des ki-web-MCP