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

AspektWert
Basis-URLhttps://ki.karlkratz.com/api/pages
Format Anfrageapplication/json, UTF-8
Format Antwortapplication/json; charset=utf-8
Authentifizierungkeine (Demo-System, durch Hetzner-Firewall öffentlich für 0.0.0.0/0 auf Port 443 erreichbar)
Versionierungkeine; das Schema steht im PHP-Quelltext unter /var/www/ki.karlkratz.com/src/
Erlaubte HTML-Tags im Feld contenth1, h2, h3, p, ol, ul, li, a (mit href und title), strong, table, thead, tbody, tr, th, td, pre, code

Endpoints im Überblick

MethodePfadZweck
GET/api/pagesListe mit Suche, Sortierung, Pagination
POST/api/pagesNeue 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.

FeldTypPflicht beim AnlegenRegeln
idIntegerneinwird serverseitig vergeben (BIGSERIAL)
slugStringjaRegex ^[a-z0-9][a-z0-9-]{0,63}$, eindeutig
titleStringja1 bis 200 Zeichen
meta_descriptionStringja1 bis 200 Zeichen, SEO-Empfehlung 50 bis 160
contentString (HTML)jawird per DOMDocument auf die Tag-Whitelist gefiltert
created_atISO-8601-Timestampneinserverseitig gesetzt
updated_atISO-8601-TimestampneinTrigger aktualisiert bei jedem Schreibvorgang

GET /api/pages, Liste lesen

Query-Parameter

ParameterDefaultErlaubt
qleerVolltext über slug, title, meta_description (case-insensitive Substring)
sortupdated_atslug, title, meta_description, updated_at, created_at
dirdescasc, desc
limit501 bis 200
offset00 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

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/hallo

Antwort:

{"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"
}
errorStatusBedeutung
not_found404Slug existiert nicht, oder Pfad ist unbekannt
already_exists409POST mit bereits vergebenem Slug
validation422Eingabe verletzt eine Regel (Slug-Regex, Längen, Content-Sanitization)
method_not_allowed405HTTP-Methode ist auf diesem Pfad nicht unterstützt
internal500interner 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 (&lt; für <, &gt; 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

SpalteTypBedeutung
history_idIntegerPrimaerschluessel der History-Tabelle
page_idIntegerid der urspruenglichen Zeile in pages
slug, title, meta_description, contentwie pagesSnapshot des Zustands VOR der Mutation
created_at, updated_atISO-8601kopiert aus dem urspruenglichen Datensatz
archived_atISO-8601Zeitpunkt der Archivierung, also der Mutation auf pages
archive_opupdate oder deleteausloesendes Ereignis
archive_actorStringwer hat geschrieben; bei API-Aufrufen php-api
request_idString16-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

Weiterführend