Files
KantSites/docs/ARCHITECTURE.md
T
kantarciandClaude Opus 5 8e0006f883 Unbekannte Blöcke erhalten statt abweisen (Dokumentschema v4)
Der Lesevalidator wies bisher jeden unbekannten Blocktyp ab, womit der
Fallback im Renderer unerreichbar war: eine Seite mit Bausteinen aus
einer deaktivierten Erweiterung wäre nicht mehr ladbar gewesen. Ab
Schema v4 gilt ein Typ mit Namensraum (acme.slider) als Baustein einer
Erweiterung und bleibt erhalten. Der Kern prüft weiterhin ID, Tiefe,
Blockanzahl und Dokumentgröße, aber bewusst nicht die Props - er kennt
das Schema der Erweiterung nicht.

Damit ungeprüfte Werte nie ausgegeben werden, rendert der Kern aus
einem solchen Block nichts: öffentlich nur ein HTML-Kommentar, in der
Editorvorschau ein Platzhalter. Im Editor bleiben die Blöcke
auswählbar, verschiebbar, duplizierbar und löschbar, aber nicht
bearbeitbar, und es lässt sich nichts in sie einfügen.

Testumgebung: Twig lädt Vorlagen jetzt automatisch neu. Ohne Debug
konnte eine kompilierte Vorlage im Cache den Ausgang eines Testlaufs
bestimmen.

Geprüft: 64 PHP-Tests / 229 Assertions unter PHP 8.2.12 und 8.4.22,
11 JavaScript-Modelltests, Lint/Format/Container. Keine Browser- oder
Lighthouse-Läufe.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 10:37:30 +02:00

23 KiB
Raw Blame History

Architekturentscheidungen

Stand: 12.09.2026. Arbeitstitel Kant CMS; Produktname offen.

ADR 001 – PHP-Webhosting, Symfony 7.4

Umut hat im laufenden Auftrag ausdrücklich herkömmliches PHP-Webhosting gewählt. Deshalb PHP statt des zunächst aus dem Ordnernamen vermuteten Django-Stacks. Symfony 7.4 LTS benötigt PHP ab 8.2 und bringt Routing, Dependency Injection, Konfiguration und später anschließbare Sicherheitskomponenten mit. Twig maskiert Inhalte standardmäßig. Doctrine DBAL und Migrations übernehmen Datenbankzugriff und Migrationen. Keine eigene Authentifizierung erfinden: vor HTTP-Schreibzugriffen Symfony Security, CSRF-Schutz und serverseitige Rollen ergänzen.

Eine Kundeninstallation ist eine Anwendung mit eigenem Datenbestand. Es gibt keine Abhängigkeit von einem dauerhaften Node-, Python- oder Queue-Prozess. Öffentliche Dateien liegen in public/, der Projektstamm muss außerhalb des Webroots bleiben. Konkreter Hostinganbieter, PHP-Version, Datenbank und CLI-/Deploymentmöglichkeiten sind noch offen.

Alternativen: Django passt nicht zum gewählten PHP-Profil. Laravel ist möglich, Symfony wurde wegen der LTS-Linie und modularen Grundlage gewählt. Ein selbst geschriebenes Mini-Framework würde Standardfunktionen unnötig neu implementieren.

Quellen bei Einrichtung geprüft: Symfony 7.4 Setup, Symfony-Versionen. Exakte installierte Versionen stehen im Composer-Lockfile; PHP-Untergrenze wird im Composer-Resolver auf 8.2.12 simuliert. Auf dem Server dennoch aktuellen Sicherheits-Patchstand verwenden.

ADR 002 – Versionierte Inhalte und Veröffentlichung

Dokumentschema v1: schemaVersion, title, description, blocks. Jeder Block hat eine stabile id, einen type, inhaltsbezogene props, optionale tokenbasierte layout-Werte und bei Containern children. Globale Designwerte gehören zum Website-/Template-Vertrag, nicht zu Texten. Die erste feste CSS-Vorlage nutzt bereits getrennte Designvariablen; konfigurierbare Themes folgen später.

Unterstützte Blöcke: section, row, column, heading, text, image, button. Alle bilden einen Baum; Layoutwerte sind Allowlist-Tokens, kein beliebiges CSS. Texte sind Klartext. Links erlauben lokale absolute Pfade, Fragmente oder HTTPS. Bilder benötigen Abmessungen und eine explizite Aussage, ob sie dekorativ sind. Begrenzungen: 256 KiB Dokument, 300 Blöcke, Tiefe 10, eindeutige IDs, genau eine Hauptüberschrift. Die aktuellen Textgrenzen zählen UTF-8-Bytes. Medienoptimierung/Uploads sind noch nicht enthalten.

Unbekannte Schemaversionen und Blocktypen werden beim Schreiben abgewiesen; die ursprüngliche Revision bleibt erhalten. Der Renderer enthält einen Platzhalter, aber der derzeit strikte Lesevalidator lässt unbekannte Blöcke noch nicht bis dorthin gelangen. Ein erhaltender Plugin-Fallback samt Registry ist ausdrücklich Phase 4, nicht bereits funktionsfähig.

Revisionen sind über den Repository-Vertrag unveränderlich. Die Datenbank schützt die Referenzen Website→Seite und Seite→Revision mit Fremdschlüsseln. Revisionszeiger auf einer Seite werden wegen des Einfügezyklus im Repository geprüft; zusätzlich fordert der öffentliche JOIN dieselbe Seiten-ID. Externe direkte Datenbankänderungen sind kein unterstützter Schreibweg.

Jedes Speichern/Publizieren erhöht page.version. Ein bedingtes UPDATE mit erwarteter Version erkennt veraltete Schreibversuche; Revisionseinfügung und Zeigeränderung laufen in einer Transaktion. Dadurch bleibt bei einem Konflikt auch keine verwaiste Revision zurück. Publizieren einer historischen Revision stellt deren Inhalt wieder her, der aktuelle Entwurf bleibt unabhängig erhalten.

ADR 003 – Datenmodell

Entität Bereits implementiert / vorgesehener Vertrag
Website ID, Name, Standardsprache, JSON-Theme-Konfiguration; aktuell eine Installation mit ID default.
Seite ID, Website-ID, Slug, Sprache, Entwurfs-/Veröffentlichungszeiger, Schreibversion; Slug je Website und Sprache eindeutig.
Revision ID, Seiten-ID, vollständiges versioniertes Dokument, UTC-Erstellungszeit; nächste Ausbaustufe: Benutzer/Änderungsnotiz.
Block Innerhalb des Dokuments mit stabiler ID; keine Einzelblocktabelle.
CMS-Benutzer ID, eindeutiger normalisierter Benutzername, Passwort-Hash, Rollen; hinzugefügt durch Migration 20260912000200.
Medium (Entwurf) ID, Originaldatei, MIME-Typ, Maße, Varianten, Prüfsumme; Alt-Text kontextbezogen pro Verwendung. Noch keine Tabelle.
Menü (Entwurf) ID, Website, Sprache, stabile Menüpunkt-IDs und Seitenreferenzen statt hart codierter Ziel-URLs. Aktuell feste Navigation.
Template (Entwurf) Manifest-ID, Version, unterstützte Block-/Schemaschnittstellen, Design-Tokens; derzeit feste Twig-/CSS-Vorlage.

Sprachkennung und entsprechende Eindeutigkeit sind gespeichert; öffentliche Sprachwahl und Übersetzungsoberfläche fehlen. Seitentitel/Beschreibung sind Teil der Revision, damit Entwurfsmetadaten nicht öffentlich durchsickern. Sitemap, strukturierte Daten, Weiterleitungen und umfassende SEO-Einstellungen folgen später.

ADR 004 – Ausgabe, Cache und Ressourcen

Twig rendert vollständiges semantisches HTML auf dem Server. Der öffentliche Pfad lädt nur lokales CSS und ein SVG-Favicon, kein JavaScript. Der Editor unter /admin hat separate CSS-/JS-Ressourcen. Sein geschützter Preview-Endpunkt rendert page.html.twig mit demselben validierten Blockbaum wie die öffentliche Seite.

Aktuell kein Inhaltscache: pro Anfrage wird die veröffentlichte Revision gelesen und Cache-Control: no-store gesetzt. Damit gibt es keine veralteten Seiten nach Veröffentlichung. Twig-/Containercache enthält nur Programmstruktur. Ein späterer Inhaltscache muss Revisions-IDs im Schlüssel und gezielte Invalidierung für Navigation/Sitemap berücksichtigen; die entsprechenden Tests sind dann auszubauen.

Canonical entsteht aus konfiguriertem SITE_ORIGIN, nicht dem ungeprüften Request-Host. /home leitet auf / um. Unbekannte oder unveröffentlichte Seiten liefern 404. Die öffentlichen Routen akzeptieren keine Schreibmethoden. Die geschützten Admin-Endpunkte sind in ADR 006 beschrieben.

ADR 005 – Migrationen und Reproduzierbarkeit

Version20260912000100 ist das erste Schema. Doctrine erzeugt plattformspezifisches SQL. SQLite lokal getestet; MySQL-Smokejob in CI vorbereitet, noch nicht tatsächlich ausgeführt. MariaDB ist noch nicht getestet. Keine Behauptung identischer Transaktions- oder Hostingbedingungen ohne Prüfung.

Alle künftigen Datenbankänderungen erhalten eine neue Migration; diese erste Migration später nicht nachträglich umschreiben. Dokumentänderungen brauchen explizite vN → vN+1-Konverter und Roundtrip-/Erhaltungstests. Die erste Migration verweigert ein zerstörerisches down: Rückkehr durch geprüftes Datenbankbackup und dazu passenden Code. MySQL-DDL ist nicht vollständig transaktional; vor Migrationen sichern und Wiederherstellung separat testen.

Lockfiles sind versionierbar, lokale DB/Secrets/Abhängigkeiten und Messartefakte ausgeschlossen. Kein Git-Remote, kein externer Dienst und keine Veröffentlichung eingerichtet.

ADR 006 – Anmeldung und geschützter Editor (12.09.2026)

Symfony Security übernimmt Formularanmeldung, Passwortprüfung/-Upgrade, Session-Fixation-Schutz, Logout und Login-Throttling (fünf Versuche pro Minute gemäß Framework-Limiter). Der DBAL-UserProvider lädt lokale Benutzer. Beim automatischen Passwort-Rehash werden Datenbank und In-Memory-Benutzer synchron aktualisiert, damit die nächste Sessionprüfung nicht das gerade angemeldete Konto abmeldet. EquatableInterface vergleicht ID, Name, Passwort-Hash und Rollen; Änderungen der Berechtigungen oder Zugangsdaten invalidieren eine bestehende Sitzung.

Firewall nur für /admin: öffentliche Seiten starten keine Session und erhalten keinen Editorcode. Cookies gelten für /admin, mit HttpOnly, SameSite=Lax und Secure abhängig von HTTPS. Sessions liegen außerhalb von public unter var/sessions/. Alle Adminantworten, auch Fehler und Redirects, erhalten no-store und noindex. Ein initiales Konto entsteht über ein lokales CLI-Kommando mit verdeckter Passwortabfrage. Keine offene Registrierung, keine Default-Zugangsdaten, keine Browser-Passwortänderung oder vollständige Benutzerverwaltung.

Rollen: ROLE_EDITOR darf Entwürfe lesen, vorschauen und speichern. ROLE_PUBLISHER darf zusätzlich veröffentlichen. ROLE_ADMIN erbt beide Rollen. Aktuell alle Seiten einer einzelnen Installation, noch keine Rechte je Seite oder Website. HTTP-POSTs erfordern einen Symfony-CSRF-Token. Veröffentlichung prüft Rolle, erwartete Version und die konkrete gespeicherte Entwurfsrevision. Veraltete Stände liefern HTTP 409. Rückmeldungen enthalten die durch diese Anfrage geschriebene Revision, statt möglicherweise einen unmittelbar danach fremd gespeicherten Stand als eigenen Erfolg zurückzugeben.

Die neue Migration ergänzt cms_user, ohne Inhaltstabellen umzuschreiben. Der erste Editor nutzte ausschließlich Dokumentformat v1. Den nachfolgenden Ausbau zu v2 beschreibt ADR 008.

ADR 007 – Editorzustand und Vorschau

Kleiner Editor als lokale JavaScript-Module ohne Framework. Dokumentzustand, Tree-Operationen und Undo/Redo stehen getrennt in editor-model.js, DOM/Netzwerk in editor.js. Bis zu 100 vollständige lokale Undo-Stände; aufeinanderfolgende Eingaben in dasselbe Feld werden zu einem Schritt zusammengefasst. Das gespeicherte Dokument wird inhaltlich verglichen; Undo über einen Speicherpunkt zeigt dadurch wieder „ungespeichert“. Neuladen beendet den lokalen Verlauf. Serverseitige Revisionen sind davon unabhängig.

Die HTML-Vorschau läuft in einem Iframe mit sandbox="allow-same-origin" ohne allow-scripts. Der Elterneditor darf die Auswahl markieren und Klicks zuordnen; Inhalte können keine eigenen Skripte, Navigationen oder Formulare ausführen. Preview-Antworten werden bei neueren Änderungen sofort als veraltet markiert, laufende Requests abgebrochen und Eingaben kurz zusammengefasst. Bei Validierungsfehlern bleibt die letzte gültige Vorschau stehen. Gespeichert wird nur nach serverseitiger Validierung. Während Speichern/Veröffentlichen sind Bearbeitungsaktionen gesperrt.

Bei HTTP-/Netzwerkfehlern oder 409 bleibt der lokale Dokumentzustand erhalten. JSON-Export und expliziter Server-Neuladedialog ermöglichen die Konfliktauflösung ohne stilles Überschreiben. Der Export liegt als nutzerinitiierter Download außerhalb des Repositories. Kein Autosave und kein automatischer Merge.

Die Browserprüfungen nutzen eine eigene Datenbank und zufällige Testzugänge. axe benötigt Skriptausführung im geprüften Frame, die Sandbox erlaubt dies absichtlich nicht: daher Adminoberfläche ohne Frameinhalt prüfen und den identischen veröffentlichten Renderer separat prüfen. Die Sandbox wird nicht für Tests abgeschwächt. Klickauswahl/Rendering im echten Frame werden durch Playwright funktional getestet.

ADR 008 – Responsive Gestaltung und Dokumentformat v2

v2 ergänzt optional block.layout.design mit den Bereichen desktop, tablet und mobile. Die bestehenden Inhalte, IDs, Baumstruktur sowie tone und width bleiben unverändert. Erlaubte Eigenschaften, Blocktypen und Werte stehen zentral in public/assets/design-tokens.json: Ausrichtung, Schriftgröße/-stärke, Zeilenhöhe, Innen-/Außenabstände, Spaltenzahl/-abstand und Ecken. Der PHP-Validator und die Editorfelder lesen dasselbe Manifest. npm run build:design erzeugt daraus die eingecheckte design.css; ein Test erkennt abweichende generierte Dateien. Beliebige CSS-Werte und unbekannte Geräteschlüssel werden vor dem Speichern abgewiesen.

Vererbung: Desktop ist die Basis auf allen Breiten. Tablet überschreibt bis einschließlich 1024 px, Mobil bis einschließlich 650 px. Fehlende Eigenschaften übernehmen den Wert des größeren Geräts oder die ursprünglichen Theme-Regeln. „Übernehmen“ entfernt genau eine Überschreibung; „Werte zurücksetzen“ entfernt die Werte des gewählten Geräts am gewählten Block. Größere Geräte und andere Blöcke bleiben erhalten. Alle Aktionen nehmen an Undo/Redo teil. Farben und Sektionsbreite bleiben globale Blockeinstellungen im Inhaltbereich.

Migration v1 → v2: upgradeDocument in editor-model.js ist ein idempotenter Konverter innerhalb einer geklonten History-Transaktion. Er ändert ausschließlich die Versionsnummer; setDesign ergänzt anschließend den gewählten Wert. Die erste Designänderung oder eine Vorlage mit responsiven Werten löst die Umstellung aus. Öffnen, reine Texteingaben und unverändertes Speichern migrieren v1 nicht. Erst ein explizites erfolgreiches Speichern schreibt eine neue v2-Revision. Keine Datenbankmigration oder automatische Umschreibung bestehender Revisionen. Der neue Server liest, rendert und schreibt beide Versionen. Unbekannte Zukunftsversionen werden abgewiesen.

Rückweg: Undo der ersten Designänderung stellt auch das ursprüngliche v1-Dokument wieder her. Historische v1-Revisionen bleiben unverändert verfügbar. Ein Code-Downgrade auf einen reinen v1-Server darf erst nach Wiederherstellung passender v1-Revisionszeiger oder eines geprüften Backups erfolgen; v2-Dokumente niemals durch bloßes Entfernen der Versionsnummer als v1 ausgeben. Nach Reset aller Werte bleibt ein gespeichertes v2-Dokument v2.

Twig bildet validierte Tokens auf statische Klassen ab. Nur v2-Seiten laden die zusätzliche CSS-Datei; die öffentliche Ausgabe enthält weiter kein JavaScript oder Inline-CSS. Vorschau und Veröffentlichung haben denselben Renderer und dieselben Media Queries. Die Vorschau besitzt echte Layoutbreiten von 1240/768/390 px und wird bei kleiner Editorfläche visuell skaliert. Die Skalierung betrifft nur die Editoroberfläche und verändert die CSS-Breakpoints im Iframe nicht.

Drei lokale Abschnittsvorlagen erzeugen gewöhnliche, vollständig bearbeitbare Blöcke mit neuen IDs und H2/H3 statt einer zweiten H1. Das ist keine Plugin- oder globale Template-API. Werkzeugkasten und Struktur lassen sich einklappen; Suche zeigt passende Elemente einschließlich ihrer Vorfahren. Suchzustand, Panelwahl und eingeklappte Knoten sind reine Oberflächenzustände und werden nicht im Dokument gespeichert.

ADR 009 – Bilder, Galerie und Effekte (Dokumentschema v3)

Migration 20260912000300 ergänzt ausschließlich die Tabelle media: stabile ID, Titel, vorgeschlagener Alt-Text, unveränderliche Varianten, Archivstatus, Schreibversion und UTC-Erstellungszeit. Die lokale SQLite-Datenbank wurde vor Anwendung über VACUUM INTO konsistent gesichert. Keine Seiten oder Benutzer wurden umgeschrieben. Ein Restore muss künftig Datenbank und Medienverzeichnis gemeinsam wiederherstellen.

Uploads und Metadatenänderungen verlangen ROLE_EDITOR und einen eigenen CSRF-Token. Metadaten nutzen erwartete Versionen; konkurrierende Änderungen liefern 409. Bildtitel/Alt-Text im Medienkatalog sind Vorlagen für künftiges Einfügen. Bereits gespeicherte Seiten behalten ihre kontextbezogenen Texte, Beschriftungen und Bildvarianten. Archivieren blendet Bilder aus der normalen Auswahl aus und ist umkehrbar; bestehende URLs, Seiten und Undo-Stände bleiben funktionsfähig. Es gibt noch keine physische Löschung, Nutzungsübersicht, Ordner oder Katalog-Paginierung.

Nur tatsächlich decodierbare JPEG-, PNG- und WebP-Dateien werden angenommen. Dateinamen und vom Browser gemeldete MIME-Typen bestimmen weder Speicherpfade noch Dateiformat. Maximal 8 MiB, 12 Millionen Pixel und 8000 px pro Kante; zusätzlich konservative Prüfung gegen das PHP-Speicherlimit. GD decodiert und erzeugt WebP-Varianten bis 480/960/1600 px Breite ohne Hochskalieren. Das hochgeladene Original wird nicht übernommen; Metadaten und angehängte Nutzdaten werden nicht in die Varianten kopiert. EXIF-Orientierung wird bei JPEG berücksichtigt, sofern die EXIF-Erweiterung vorhanden ist. Transparenz wird erhalten; animierte Bilder/Dateianhänge/SVG gehören nicht zum unterstützten Uploadvertrag. Grundlage: PHP-Bilddecodierung.

Dateien liegen unter MEDIA_DIRECTORY, standardmäßig var/media außerhalb des Webroots. Nur zufällige IDs und bekannte Variantengrößen gelangen über /media/{id}/{width} mit festem image/webp, nosniff und unveränderlichem Langzeitcache nach außen. Erweiterungslose URLs funktionieren auch über die Frontcontroller-Weiterleitung des lokalen PHP-Servers; Pfade mit nicht vorhandenen .webp-Dateien wurden dort als statische 404 behandelt. Bild-URLs sind öffentlich, auch vor Veröffentlichung einer Seite und nach Archivieren; der Uploaddialog benennt dies. Die Katalog-API bleibt geschützt. Dies ist keine private Dokumentablage.

Schema v3 ergänzt den Container gallery, dessen Kinder ausschließlich Bildblöcke sein dürfen. Bestehende Baumaktionen (Auswahl, Verschieben, Löschen, Duplizieren, Undo) gelten auch für Galerieelemente; unpassende Verschiebeziele werden bereits im Modell und zusätzlich vom Server abgewiesen. Bilder dürfen mediaId, sources und eine Klartext-caption enthalten. Quellen für srcset werden einzeln auf URL und Breite geprüft. Die Ausgabe verwendet figure/figcaption, reale Maße, verzögertes Laden und responsive Quellen. Galerie-Spalten und Abstände nutzen dieselben responsiven Tokens wie Zeilen. Keine Lightbox oder Slideshow in diesem Schritt.

layout.effects erlaubt nur vordefinierte Schatten, Hover-Anheben/Bildzoom sowie Einblenden beim Laden. Effekte gelten geräteübergreifend. Die CSS-Datei wird nur für v3-Seiten eingebunden; es wird kein öffentliches JavaScript ergänzt. Bewegungsregeln gelten ausschließlich bei prefers-reduced-motion: no-preference. Inhalte bleiben ohne Animation lesbar; Einblendung ist keine scrollgesteuerte Animation.

Der idempotente Konverter unterstützt nun v1/v2 → v3 und verhindert Downgrades. Erst Einfügen einer Galerie/eines Katalogbildes, eine Bildbeschriftung oder Effektänderung hebt den lokalen Entwurf auf v3. Die Umstellung ist Teil derselben Undo-Transaktion; Speichern erzeugt eine neue Revision. v1/v2 bleiben les- und schreibbar. Kein Massendatenupdate. Rückweg über alte Revisionen oder gemeinsames Backup plus passenden Code; ältere v1/v2-Software darf keine v3-Revisionen laden. Die neue Medientabelle darf beim Rückweg nicht unbedacht gelöscht werden.

ADR 010 – Seitenerstellung, Hierarchie und Inhaltstypen (12.09.2026)

Seiten konnten bisher nur über die Kommandozeile entstehen, und page.slug war websiteweit eindeutig. Damit war weder eine URL-Hierarchie wie /leistungen/webdesign noch ein Beitragstyp möglich. Beides gehört in das Datenmodell und wird deshalb jetzt entschieden, solange noch keine Kundeninhalte gespeichert sind; eine spätere Änderung würde eine Migration über echte Kundendaten erzwingen.

Migration 20260912000400 ergänzt page.path, page.parent_id, page.type und page.created_at. slug bleibt das einzelne Adresssegment, path ist die vollständige öffentliche Adresse und trägt die websiteweite Eindeutigkeit über den Index uniq_page_path (website_id, locale, path). Der bisherige eindeutige Index über slug entfällt: ein Segment muss nur unter seinen Geschwistern eindeutig sein. Die Tiefe ist auf vier Ebenen begrenzt (PageRepository::MAX_DEPTH), Beiträge (type = post) nehmen keine Unterseiten auf.

parent_id ist bewusst ohne Fremdschlüssel angelegt, und die Indexumstellung passiert nicht über die Doctrine-Schemadifferenz, sondern in postUp() beziehungsweise Version20260912000400::applyIndexes(). Grund: SQLite baut bei Index- oder Fremdschlüsseländerungen die gesamte Tabelle neu auf (CREATE TEMPORARY TABLE … DROP TABLE page …), was am Fremdschlüssel revision.page_id scheitert. Mit reinen ADD COLUMN-Anweisungen bleibt die Migration auf SQLite und MySQL gleichermaßen ein einfacher, nicht tabellenkopierender Schritt. Die Elternbeziehung wird dafür im PageRepository innerhalb derselben Transaktion aufgelöst und geprüft. path ist auf Datenbankebene NULL-fähig, weil der eindeutige Index vor dem Backfill entsteht und wiederholte NULL-Werte in SQLite wie in MySQL nicht kollidieren; die Anwendung schreibt niemals eine Seite ohne Pfad, und postUp() bricht ab, falls nach dem Backfill noch ein Pfad fehlt.

Anlegen und Löschen laufen über Formulare, nicht über die JSON-API: POST /admin/pages benötigt ROLE_EDITOR, POST /admin/pages/{id}/delete benötigt ROLE_ADMIN, beide mit CSRF-Token. Eine neue Seite entsteht als privater Entwurf mit einem gültigen Startdokument (Schema v3, genau eine H1) und wird erst durch bewusstes Veröffentlichen öffentlich. Löschen entfernt alle Revisionen einer Seite, weist aber Seiten mit Unterseiten ab und prüft die erwartete Version, damit keine Inhalte unbemerkt verschwinden. Die reservierten Adressen admin, media, assets und favicon sind für Wurzelseiten gesperrt; zusätzlich hält ein Lookahead in der öffentlichen Route die CMS-Pfade aus dem Auffangmuster heraus.

Rückweg: Code-Rollback allein genügt nicht, sobald Unterseiten existieren, weil ältere Software path und parent_id nicht kennt. Rückweg über das vor der Migration erzeugte Backup (var/backups/before-hierarchy-*.db) zusammen mit dem passenden Codestand.

ADR 011 – Erhalt unbekannter Blöcke (Dokumentschema v4, 12.09.2026)

Bisher wies der Lesevalidator jeden unbekannten Blocktyp ab. Eine Seite mit Bausteinen aus einer deaktivierten oder noch nicht installierten Erweiterung wäre damit nicht mehr ladbar gewesen – der Fallback im Renderer war unerreichbar. AGENTS.md und der Entwicklungsplan verlangen für fehlende Blocktypen aber einen kontrollierten Fallback. Dieser Vertrag wird jetzt festgelegt, bevor es Plugins gibt, weil er sonst nachträglich über bestehende Kundeninhalte eingeführt werden müsste.

Ab Dokumentschema v4 gilt ein Typ mit Namensraum – ^[a-z][a-z0-9]{1,30}\.[a-z][a-z0-9-]{1,40}$, zum Beispiel acme.slider – als Baustein einer Erweiterung. Kernblöcke bleiben ohne Punkt; ein unbekannter Typ ohne Namensraum wird weiterhin abgewiesen. Für Plugin-Blöcke prüft der Kern weiterhin Block-ID, Verschachtelungstiefe, Blockanzahl, die Dokumentgröße von 256 KiB sowie dass props und layout Objekte sind. Ihre props prüft er bewusst nicht: er kennt das Schema der Erweiterung nicht.

Damit ungeprüfte Werte nie in die Ausgabe gelangen, rendert der Kern aus einem solchen Block nichts. Öffentlich entsteht nur ein HTML-Kommentar mit ID und Typ; Besucher sehen kein kaputtes Fragment. Nur in der Editorvorschau erscheint ein sichtbarer Platzhalter, damit der Block auffindbar und verschiebbar bleibt. Kinder eines Plugin-Blocks sind erlaubt und werden gespeichert, aber nicht gerendert, weil der Kern ihr Layout nicht kennt. Kern-Designtokens gelten für Plugin-Blöcke nicht, da das Tokenmanifest ihre Typen nicht führt.

Im Editor bleiben Plugin-Blöcke auswählbar, verschiebbar, duplizierbar und löschbar; das Eigenschaftenpanel zeigt statt Feldern einen Hinweis. In den Baustein hinein lässt sich nichts einfügen (canContain ist false), und createBlock kann keinen erzeugen – Plugin-Blöcke entstehen ausschließlich durch die jeweilige Erweiterung oder durch Import.

Rückweg: v1 bis v3 bleiben les- und schreibbar, ein Downgrade gibt es nicht. Ältere Software darf keine v4-Revision laden. Dies ist der Erhaltungsvertrag, noch kein Pluginsystem: Registrierung, Hooks und Ressourcenladen folgen in Phase 4.