Turnstile laesst sich in den Einstellungen einschalten: aus, bis beide
Schluessel eingetragen sind; die Sicherheitsrichtlinie oeffnet sich nur
auf Seiten mit Formular, und nur fuer challenges.cloudflare.com.
Antwortet Cloudflare nicht, gilt die Absendung als bestanden - ein
Ausfall dort darf keine Kundenanfrage kosten. Die drei eingebauten
Pruefungen ohne Dritte bleiben davon unberuehrt.
Dabei gefunden: wer ein Pflichtfeld vergass, bekam die Seite ohne
Gestaltung zurueck. Der Formularcode baute sie selbst ein zweites Mal
auf und liess Theme, Blockstile und Schriften weg. Jetzt geht auch
dieser Weg durch den Seitencontroller, die Meldungen reisen als
Anfrageattribut mit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Eine Website ohne Beitraege redet nirgends von Beitraegen: Archiv und
Feed antworten mit 404, der Feedverweis faellt aus dem Kopf, und der
Hinweiskasten der Beitragsliste bleibt im Editor statt oeffentlich zu
erscheinen. Das gilt auch je Kategorie, sobald deren letzter Beitrag
weg ist. Dabei gefunden: die Liste fragte je Beitrag nach Bild und
Begriffen - jetzt je eine Abfrage fuer die ganze Liste (22 -> 16
Abfragen auf der Startseite des Nachbaus).
Unterstrichene Links gibt es nur noch im Fliesstext, dort mit einem
Schalter im Designbereich; Karten, Menue, Kontaktzeilen und der
Ansehen-Streifen haben keinen mehr. Die aktuelle Seite im Menue wird
ueber das Schriftgewicht kenntlich - die Akzentfarbe fiel im dunklen
Fussmenue durch die Kontrastpruefung.
Neu: das Symbol der Website laesst sich festlegen. Aus einem Bild der
Mediathek entstehen beim Speichern quadratische PNG-Dateien in 32 und
180 px; schlaegt das fehl, wird die Einstellung nicht gespeichert.
Die Browserpruefung laeuft jetzt gegen jede Installation statt nur
gegen die Demo: geprueft wird, was in der Sitemap steht, und das
Ladebudget trennt Bilder vom Beiwerk.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Uploads werden weiterhin nach WebP umgewandelt, jetzt in vier statt drei
Breiten (480/960/1600/2400) und ohne Hochskalieren. Hintergrundbilder
waehlen ihre Variante je Geraet, weil CSS kein srcset kennt.
Dabei kam ein Fehler heraus, den erst der Browsertest zeigte: der
Dokumentpruefer erlaubte drei sources je Bild, also war mit der vierten
Variante jedes Dokument mit einem Bild ungueltig - ohne sichtbare Meldung.
Die Grenze haengt jetzt an MediaLibrary::WIDTHS.
Geteiltes Hosting hat meistens keinen Composer. scripts/build-release.php
baut deshalb auf einem Build-Rechner ein Archiv mit fertigem vendor/ ohne
Entwicklungspakete, samt SHA-256. Der Updater bricht ab, wenn ein Update
composer.lock aendert und Composer auf dem Server nicht aufrufbar ist,
statt mit einem unpassenden vendor/ weiterzulaufen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"Wieviel wann wo wie die Seite benutzt" - naheliegend wäre ein Analysewerkzeug
gewesen, das eine Einwilligung braucht und damit genau die Besucher verliert,
die ablehnen. Also andersherum: eine Messung, die niemanden wiedererkennt.
Gespeichert wird ein Zähler je (Tag, Stunde, Pfad, Status, Verweis-Domain),
kein Datensatz je Aufruf. Keine IP-Adresse, keine Kennung, kein Cookie - auch
nicht gehasht, denn der Hash einer IP-Adresse bleibt ein Personenbezug. Ein
Test liest die Spaltenliste der Tabelle und vergleicht sie mit dieser Zusage,
damit sie beim nächsten Ausbau nicht unbemerkt fällt. Vom Verweis bleibt nur
der Host: die volle Adresse kann einen Suchbegriff oder einen Namen tragen.
Gezählt wird auf kernel.terminate, nach der Antwort - die Messung darf keine
Anfrage verlangsamen, und ein Fehler beim Zählen kostet einen Zähler, nicht
eine Seite. Nur GET auf HTML mit 200 oder 404; Verwaltung, Assets, Feed,
Sitemap und Einwilligungsseite sind ausgenommen, erkennbare Maschinen über
ihre Kennung. Das ist eine Heuristik, deshalb heißt die Zahl auf dem
Bildschirm nicht "Besucher". Erfundene Crawler-Adressen laufen gesammelt unter
(sonstige), sonst wächst die Tabelle mit jedem geprobten Pfad.
Die Diagramme zeichnet der Server als SVG mit Klassen statt Stilattributen:
kein Diagrammwerkzeug, kein Skript, und die Richtlinie bleibt streng. Die
Zahlen stehen zusätzlich in einer Tabelle, weil ein Balken für einen
Screenreader nichts ist.
Was die Zahlen nicht sagen, steht auf dem Bildschirm: Aufrufe statt Besucher,
keine Verweildauer, keine Absprungrate, kein Klickweg, kein Land. Das ist die
Folge der Entscheidung, nicht eine Lücke.
Aufbewahrung: nach 60 Tagen werden die Stunden eines Tages verschmolzen, nach
425 Tagen gelöscht - gut ein Jahr, damit der Vorjahresmonat vergleichbar
bleibt. cms:stats:prune macht beides und gehört in einen Cronjob.
279 PHP-Tests / 1119 Assertions unter PHP 8.2.12 und 8.4.22, axe auf zwölf
Verwaltungsbildschirmen ohne Verstöße. Dabei kam eine PHP-8.4-Verwarnung zu
fputcsv() heraus, jetzt mit ausdrücklichem Escape-Parameter.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ein Hinweis, der nur fragt und sonst nichts ändert, ist Theater. Deshalb zwei
Hälften, und die zweite ist die wichtigere.
Die Frage: serverseitig gerendert, per Formular beantwortet, ohne JavaScript -
die öffentlichen Seiten laufen mit script-src 'none', und ein Banner mit
Skript wäre das eine, was diese Regel bricht. "Alle akzeptieren" und "Nur
notwendige" sind dieselbe Art Knopf, gleich groß, nebeneinander. Die
Entscheidung steht in einem eigenen Cookie: Fassung der Frage, gewährte
Gruppen, Zeitpunkt. Keine Kennung, kein Serverabgleich, ein halbes Jahr. Kein
CSRF-Token, absichtlich: die Anfrage ändert nichts auf dem Server, und ein
Token bräuchte für jeden Besucher eine Sitzung - genau das, was ein
Einwilligungshinweis vermeiden soll. Geprüft wird stattdessen das
Rücksprungziel.
Das Zurückhalten: der neue Block "Externer Inhalt" (Dokumentschema v14) lädt
ohne Einwilligung nichts - kein iframe, keine Adresse im Markup, frame-src
'none'. Der Platzhalter nennt den Anbieter; sein Knopf gewährt genau diese
Gruppe und nimmt die vorher gewährten mit. Erst danach kommt der Host in die
Sicherheitsrichtlinie, und nur dieser. Im Editor bleibt es immer ein
Platzhalter.
Datenschutz und Impressum sind keine Einstellung mehr: fehlen sie, warnt die
Verwaltung und legt sie auf Knopfdruck als Gerüst an, mit NOCH AUSZUFÜLLEN an
jeder Pflichtangabe. Den Text schreibt das CMS nicht - das ist eine rechtliche
Aussage über eine Firma. Verlinkt werden beide aus dem Template, nicht aus dem
Fußbereich: ein Pflichtlink, den man löschen kann, ist keiner.
Dazu der Fehler, den Umut im Screenshot gesehen hat: die Wortmarke stand als
Buchstabenkolonne in einem 60 Pixel breiten Kasten. .site-header war noch ein
Flexcontainer aus der Zeit des handgeschriebenen Markups und quetschte die
Sektion zusammen, und design.css band jede Designklasse an #main - wo Kopf-
und Fußbereich seit ADR 026 nicht mehr liegen. Beides behoben.
Neu im Ausbauplan: Statistik (serverseitige, wiedererkennungsfreie Zählung,
die deshalb keine Einwilligung braucht), Inhalte und Ladeverhalten prüfen
(Blättern überall, minimal laden, Ladebudget), Installer (Schritt für Schritt
im Browser, Migration inklusive).
245 PHP-Tests / 976 Assertions unter PHP 8.2.12 und 8.4.22. Der Browsertest
fand zwei Fehler von mir: grauer Text auf dunklem Fußbereich mit 2,24:1, und
das fixierte Banner verdeckte den Einwilligungsknopf am Seitenende.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Das Auswahlfeld des Browsers sah aus wie das Auswahlfeld des Browsers.
Hochgeladen wird jetzt über eine Ablagefläche: Bilder hineinziehen oder
klicken, darunter steht, welche Dateien gewählt sind und welche übersprungen
wurden.
Darunter liegt weiter ein echtes input[type=file] - unsichtbar, aber
fokussierbar, mit der Fläche als label und dem Fokusring über :focus-within.
Ohne JavaScript bleibt genau das übrig: ein Dateifeld mit Beschriftung. Das
Skript ergänzt Ziehen und benennt die Auswahl; abgeschickt wird nie von
selbst. Kein "required", weil der Browser seine Meldung nicht an etwas
Unsichtbares hängen kann - der Server weist einen leeren Upload mit einem
lesbaren Satz ab.
docs/UPDATE.md plant das Update über einen Git-Tag: Vorprüfung, Sicherung,
Wartungszustand mit 503 für die Website und offenem /admin, Checkout,
composer, Migration, Cache, Rauchtest, Protokoll - und den Rückweg in
derselben Reihenfolge rückwärts. Migrationen bleiben nicht umkehrbar, der
Rückweg ist die Sicherung. Kein Updater, der eine Adresse aus dem Formular
zieht, und kein automatisches Update im Hintergrund. Zwei Entscheidungen
hängen am Hosting: Composer auf dem Server oder vendor/ im Tag, und ob es
Shell-Zugriff gibt. Gebaut ist davon nichts.
219 PHP-Tests / 881 Assertions unter PHP 8.4.22, darunter Upload über das
Formular mit zwei Dateien, CSRF-Pflicht, Versionskonflikt, Archivfilter und
Blättern mit 26 Bildern. Playwright prüft die Ablagefläche am echten
Dateifeld; axe bleibt auf elf Bildschirmen ohne Verstöße.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Konten gab es bisher nur über die Konsole. Unter "Benutzer" legt ein
Administrator jetzt Konten an, ändert Rollen, setzt Passwörter und löscht
Konten; unter "Mein Konto" ändert jeder sein eigenes Passwort, aber nur mit
dem aktuellen in der Hand.
Eine Rolle je Konto: Administrator, Veröffentlicher, Redakteur. Ein Passwort
oder eine Rolle zu ändern meldet jede offene Sitzung dieses Kontos ab - dafür
war nichts zu bauen, CmsUser::isEqualTo vergleicht Hash und Rollen mit der
Datenbank. Genau eine Sitzung überlebt: die, die ihr eigenes Passwort geändert
hat. "Jemanden sofort aussperren" ist damit dasselbe wie "Passwort setzen".
Es bleibt immer ein Administrator übrig, nicht über eine Zählung, sondern weil
niemand sein eigenes Konto löschen oder herabstufen kann und jede Änderung die
Administratorrolle voraussetzt. Die zusätzliche Prüfung auf einen zweiten
Administrator war damit unerreichbar und ist wieder heraus.
Mediathek (Umut: "das sollte direkt laden in pagination"): /admin/media zeigt
die Bilder jetzt selbst, 24 je Seite, neueste zuerst, mit Blättern und einem
Schalter für archivierte. Hochladen und Bildangaben laufen über gewöhnliche
Formulare ohne JavaScript; der Dialog bleibt der Weg, beim Bearbeiten ein Bild
auszuwählen. Die Bild-ID steht in jeder Karte, weil Beitragsbild und
Teilen-Vorschau genau diese ID erwarten.
Die Migrationsliste der Testfixtures stand dreimal da und steht jetzt einmal
in tests/TestSchema.php.
217 PHP-Tests / 845 Assertions unter PHP 8.2.12 und 8.4.22. axe deckt jetzt
elf Bildschirme ab statt drei - alle Verwaltungsseiten, nicht nur Anmeldung,
Seitenliste und Editor -, null Verstöße. Nicht geprüft: kein Screenreader;
Blättern und Upload sind mit zwei Bildern geprüft, nicht mit einer großen
Bibliothek.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Jede Verwaltungsseite trug bisher ihre eigene Kopfzeile und ihre eigene,
unvollständige Liste von Links auf die anderen Seiten - die Seitenübersicht
verlinkte sieben Bereiche, das Menü fünf, die Fassungen drei. Wo man war,
stand nirgends.
Jetzt gibt es einen Rahmen für alle: eine feste Seitenleiste mit der
Navigation, der aktive Eintrag über aria-current markiert, und eine Kopfzeile
mit Titel, einem Satz Erklärung und Platz für die Hauptaktion. Jede Seite
füllt nur noch heading, lead, actions und content. Auf schmalen Fenstern wird
die Seitenleiste zu einer scrollbaren Leiste, ohne JavaScript.
Die Seitenübersicht zeigt je Seite eine Zeile mit dem Alltäglichen und einem
Aufklappfeld "Einstellungen", das darunter an seinem Platz öffnet - vorher
waren es sechs Aufklappfelder, deren Inhalt als absolut positionierter Kasten
über den Rand der Karte hinausragte. Tabellen scrollen seitlich, statt die
Karte aus dem Fenster zu schieben.
Das Stylesheet ist geteilt: die Editorregeln stehen in editor.css, das nur
Editor und Mediathek laden; admin.css ist neu geschrieben und lesbar. Der
Editor behält seinen Aufbau und übernimmt die neue Gestaltung - Systemschrift,
gleiche Radien, gleiche Schaltflächen, und ein paar Panelgrößen, die zum Lesen
zu klein waren.
Beim Zerlegen des Stylesheets schnitt mein Skript am Ende jedes @media-Blocks
alle schließenden Klammern ab statt nur einer. editor.css war damit ab dem
ersten Medienblock ungültig: der Browser behielt 29 von 202 Regeln. Gefunden
hat das der Browsertest, an einer Vorschaubreite von 1236 statt 1240 Pixeln -
vier Pixel als einziges Anzeichen dafür, dass dreiviertel des Stylesheets
fehlte.
201 PHP-Tests / 716 Assertions unter PHP 8.2.12 und 8.4.22, Playwright-
Editortest mit axe ohne Verstöße auf Anmeldung, Seitenübersicht und Editor,
öffentliche Browserprüfung grün. Zwei Testselektoren zeigten auf umbenannte
Klassen und prüfen dieselbe Sache nun über den neuen Namen. Nicht geprüft:
kein Screenreader, und die Bildschirme Menüs, Formulare, Bereiche, Design,
Einstellungen und Fassungen sind nur von Hand bedient, nicht automatisiert
geprüft.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Migration 20260912001000 legt die Tabelle partial an: Blockdokumente,
die keine Seiten sind. Kopf- und Fußbereich je einmal,
wiederverwendbare Sektionen beliebig viele - im selben Editor
gestaltbar wie eine Seite, weil der Editor seine Schnittstelle aus
einem Datenattribut nimmt statt sie fest zu kennen.
Im Template steht keine Marke und keine Navigation mehr. Ein neuer
Block menu zeigt einen Menüplatz; die Einträge bleiben in der
Menüverwaltung.
Eine Seite braucht genau eine Hauptüberschrift, ein geteilter Bereich
keine - sonst hätte jede Seite, die ihn trägt, zwei. Geteilte
Bereiche haben keinen Entwurf und keine Veröffentlichung: sie sind
überall sichtbar, gespeichert heißt live.
Verknüpft statt kopiert: partialref speichert nur die Adresse, die
Blöcke bleiben im Bereich, und eine Änderung erreicht jede Seite, die
sie einbindet. Zyklen abgewiesen, Verschachtelung auf drei Ebenen
begrenzt, eine entfernte Sektion hinterlässt keine Lücke und der
Verweis bleibt erhalten. Die Abschnittsvorlagen kopieren weiterhin.
Ein echter Fehler kam dabei heraus, der jede dunkle Sektion mit
Spalten betraf: .block-column>.block-text:first-child setzt eine
Farbe für helle Hintergründe und gewinnt auf Spezifität gegen
.tone-ink - gemessen 1,87:1.
Geprüft: 165 PHP-Tests / 660 Assertions unter PHP 8.2.12 und 8.4.22,
15 JavaScript-Modelltests, Editortest mit axe ohne Verstöße,
öffentliche Browserprüfung, Migration nach Backup angewendet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Eine Quelle entscheidet jetzt, welche Seite unter / antwortet:
SiteSettings::homePath() und publicPath(). Das Literal home kommt im
Anwendungscode nicht mehr vor - öffentliche Routen, Canonical,
Sitemap, llms.txt, Menüauflösung, Beitragslisten und der
Formularrücksprung benutzen dieselben zwei Methoden. Die gewählte
Seite muss veröffentlicht und nicht im Papierkorb sein, ihre eigene
Adresse leitet dauerhaft auf /, und als Wurzel ist sie gegen
Umbenennen und Papierkorb gesperrt.
Zweiter echter Fehler, den nur ein HTTP-Aufruf zeigt:
/sitemap.xml, /robots.txt, /llms.txt und /feed.xml waren lokal nie
erreichbar. Der eingebaute PHP-Server antwortet für fehlende Pfade
mit Dateiendung selbst mit 404, ohne den Frontcontroller zu fragen;
alle Kernel-Tests bestanden trotzdem, weil sie die Anwendung direkt
aufrufen. bin/dev-router.php bildet die Rewrite-Regel des Hostings
nach, Start- und CI-Befehl nutzen es, und die Browserprüfung ruft die
vier Adressen jetzt über HTTP auf.
Barrierefreiheitsbefunde werden nach Ursache getrennt: ein
Kontrastfehler aus Theme oder Stylesheet bleibt ein Defekt und bricht
den Lauf ab, einer aus einer im Editor gesetzten Blockfarbe wird bei
jedem Lauf mit Seite, Farbe und Verhältnis genannt und in
reports/browser.json geschrieben, färbt aber keinen Codewechsel rot.
Geprüft: 162 PHP-Tests / 636 Assertions unter PHP 8.2.12 und 8.4.22,
15 JavaScript-Modelltests, vollständiger Editortest mit axe, wieder
grüne öffentliche Browserprüfung und Lighthouse mit je drei Läufen
mobil und Desktop, alle fünf Kategorien 100.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Migration 20260912000900 ergänzt page.publish_at, page.deleted_at und
revision.seq.
Verschieben nimmt den ganzen Teilbaum mit und hinterlässt für jede
alte Adresse ein 301. Kollisionen, Tiefengrenze und das Verschieben
unter eigene Unterseiten werden vor der ersten Änderung abgewiesen;
alles in einer Transaktion, weil ein halb verschobener Teilbaum
Seiten unerreichbar machen würde.
Der Papierkorb ersetzt das sofortige Löschen für die Editorrolle:
alle Fassungen bleiben, öffentlich 404, verschwunden aus Navigation,
Archiven, Feed, Sitemap, llms.txt und Editorliste. Unterseiten gehen
mit, beim Wiederherstellen muss das Elternteil zuerst zurück.
Eine Fassung wiederherzustellen überschreibt nichts, sondern legt
eine neue an. revision.seq behebt dabei einen echten Fehler: die
Reihenfolge des Verlaufs war nicht eindeutig, weil created_at nur
Sekunden kennt und mehrere Speichervorgänge in dieselbe Sekunde
fallen - "vorherige Fassung wiederherstellen" hätte die falsche
treffen können.
Geplante Veröffentlichung über cms:publish-due. Ohne Cron bleibt die
Seite ein Entwurf, was die sichere Richtung ist.
Eine frei wählbare Startseite fehlt bewusst: der Pfad home gilt an
elf Stellen als Wurzel, das ist ein eigener Schritt (ADR 022).
Geprüft: 160 PHP-Tests / 622 Assertions unter PHP 8.2.12 und 8.4.22,
15 JavaScript-Modelltests, vollständiger Editortest mit axe,
Migration zuerst gegen eine Datenbankkopie samt Kontrolle des
Backfills.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Inline-Textbearbeitung ohne Skript in der Vorschau: der Iframe führt
weiterhin kein eigenes JavaScript aus, der Editor greift in sein
Dokument und setzt das Element auf contenteditable. Während der
Bearbeitung wird die Vorschau nicht neu geladen, sonst wäre der
Cursor weg; Eingaben werden zu einem Undo-Schritt zusammengefasst.
Neben dem Doppelklick startet eine Schaltfläche dasselbe ohne Maus.
Kontrast wird nach WCAG 2.1 berechnet und als Warnung in drei Stufen
gemeldet, nicht als Verbot - der Plan verlangt Prüfung, nicht
Bevormundung.
Gestaltungsvorlagen liegen im Theme und gelten seitenübergreifend.
Sie werden mit demselben Compiler geprüft wie ein Blockstil, also
kann über diesen Weg kein Wert hereinkommen, für den ein Block
abgewiesen würde. Dazu Kopieren und Einfügen zwischen Blöcken und
Bildauswahl für Hintergrundbilder über die Mediathek.
Geprüft: 155 PHP-Tests / 579 Assertions unter PHP 8.2.12 und 8.4.22,
15 JavaScript-Modelltests, Lint/Format/Container. Erstmals wurde der
neue Editor im laufenden Betrieb bedient: Direktbearbeitung und
Kontrastwarnung mit echten Werten ausgelöst, Teständerungen
verworfen. Weiterhin keine axe- oder Lighthouse-Messung.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Migration 20260912000800 ergänzt page.robots, page.og_media_id,
website.settings und die Tabelle redirect.
Alles Maschinenlesbare wird aus dem veröffentlichten Bestand
abgeleitet, nichts separat gepflegt: die Sitemap listet nur
veröffentlichte, indexierbare Seiten samt Beitragsübersicht und
Begriffsarchiven, und dieselbe Einstellung setzt auch das
noindex-Meta - beide Aussagen können nicht auseinanderlaufen.
Strukturierte Daten entstehen nur aus gepflegten Werten: ohne
Angaben wird kein leerer Organization-Block ausgegeben. JSON-LD ist
ein Datenblock, kein Skript, daher bleibt script-src 'none'
unberührt.
Weiterleitungen greifen vor dem 404, aber nach der Seitensuche: eine
vorhandene Seite gewinnt immer, sonst könnte ein alter Eintrag eine
neue Seite unerreichbar machen. Ketten werden abgewiesen. robots.txt
nimmt zusätzliche Zeilen nur nach festem Muster an, damit ein
Eintrag die Datei nicht umdeuten kann. llms.txt ist standardmäßig
aus.
Geprüft: 128 PHP-Tests / 507 Assertions unter PHP 8.2.12 und 8.4.22,
14 JavaScript-Modelltests, Lint/Format/Container. Dazu eine reine
Sichtprüfung im Browser auf einer lokalen Demoseite - weiterhin
keine axe- oder Lighthouse-Messung.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Migration 20260912000700 legt form und form_submission an. Der
Formularblock im Seiteninhalt enthält nur die Adresse: Felder,
Empfänger und Einwilligungstext liegen in der Datenbank, damit ein
gespeichertes Dokument nicht ändern kann, wonach gefragt wird oder
wohin eine Anfrage geht.
Serverseitige Validierung mit einer Meldung je Feld über
aria-describedby und aria-invalid; eine Ablehnung rendert die Seite
mit HTTP 422 und allen Eingaben neu, eine gültige Absendung wird mit
HTTP 303 zurückgeleitet. Alles ohne JavaScript.
Spamschutz ohne Dienst Dritter: Honeypot, signierter Zeitstempel
gegen zu schnelle und zu alte Absendungen, fünf Absendungen je
Absender und Stunde. Ein erkannter Bot bekommt dieselbe Antwort wie
ein Erfolg. Die Absenderadresse wird nie gespeichert, nur ein Hash.
Gespeichert wird vor dem Versand, damit ein kaputter Mailversand
keine Anfrage verliert. Kein CSRF-Token auf dem öffentlichen Weg,
weil das eine Sitzung und damit ein Cookie auf jeder öffentlichen
Seite bedeuten würde; Begründung in ADR 017. Dateiuploads fehlen
bewusst.
Ein echter Fehler von den Tests gefunden: das Stundenfenster des
Limits entstand aus lokaler Zeit, die Zeitstempel aber aus UTC - auf
einer Uhr vor UTC hätte das Limit nie gegriffen.
Geprüft: 117 PHP-Tests / 450 Assertions unter PHP 8.2.12 und 8.4.22,
14 JavaScript-Modelltests, Lint/Format/Container, composer audit.
Der E-Mail-Versand wurde nie wirklich ausgeführt; keine Browser- oder
Lighthouse-Läufe.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Migration 20260912000600 ergänzt page.excerpt, author,
featured_media_id und published_at sowie die Tabellen term und
page_term. Beitragsmetadaten beschreiben die Seite, nicht ihren
Inhalt, und liegen deshalb als Spalten statt im Blockdokument: das
Archiv sortiert und paginiert damit in SQL, statt für jede
Auflistung alle Dokumente zu dekodieren.
Kategorien und Schlagwörter teilen sich eine Tabelle mit einem
kind-Feld. Beim Löschen einer Seite verschwinden ihre Zuordnungen,
die Begriffe bleiben als wiederverwendbare Etiketten; ein leer
gewordenes Archiv antwortet weiter mit 200 und leerer Liste.
Der Block postlist (Dokumentschema v5) speichert nur Anzahl und
Filter. Die Beiträge stellt der Controller beim Ausliefern zusammen,
damit ein gespeichertes Dokument keinen unveröffentlichten Beitrag
zeigen kann. Archive unter /beitraege, /kategorie/{slug} und
/schlagwort/{slug}, Feed unter /feed.xml; alle vier Segmente sind
für Wurzelseiten reserviert.
Geprüft: 91 PHP-Tests / 347 Assertions unter PHP 8.2.12 und 8.4.22,
12 JavaScript-Modelltests, Lint/Format/Container, Migration zuerst
gegen eine Datenbankkopie. Keine Browser- oder Lighthouse-Läufe.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Migration 20260912000500 legt eine Tabelle menu an: ein Menü je Platz
(header, footer) und Sprache, Elementbaum als JSON, Version für
dieselbe Konflikterkennung wie bei Seiten.
Ein Eintrag verweist auf eine Seite, auf ein Sprungziel innerhalb
einer Seite oder auf eine Adresse (HTTPS, lokaler Pfad, mailto:,
tel:). Seitenverweise speichern die Seiten-ID, nie den Pfad - ein
Eintrag folgt damit einer umbenannten Seite. Beim Rendern fallen
Einträge zu gelöschten oder unveröffentlichten Seiten heraus, ohne
den gespeicherten Baum zu ändern und ohne die übrigen Einträge zu
verlieren.
Die Verwaltung nutzt eine nummerierte Zeilenliste statt Drag-and-drop
und funktioniert damit vollständig ohne JavaScript; Untermenüs in der
Ausgabe über details/summary.
tests/bootstrap.php verwirft var/cache/test je Lauf. Ohne Debug prüft
Symfony weder den kompilierten Container noch die Vorlagen auf
Aktualität, was zweimal ein falsches Testergebnis erzeugt hat.
Geprüft: 79 PHP-Tests / 285 Assertions unter PHP 8.2.12 und 8.4.22,
11 JavaScript-Modelltests, Lint/Format/Container, Migration zuerst
gegen eine Datenbankkopie, wiederholtes Seeding. Keine Browser- oder
Lighthouse-Läufe.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Migration 20260912000400 ergänzt page.path, parent_id, type und
created_at. Die websiteweite Eindeutigkeit wandert vom Slug auf den
vollständigen Pfad, damit gleiche Segmente unter verschiedenen Eltern
möglich sind. Nur ADD COLUMN: Index- oder Fremdschlüsseländerungen
würden SQLite die Tabelle neu aufbauen lassen, was am Fremdschlüssel
revision.page_id scheitert; die Indizes werden in postUp() gesetzt.
POST /admin/pages legt Seiten und Beiträge als privaten Entwurf an
(ROLE_EDITOR), POST /admin/pages/{id}/delete entfernt eine Seite mit
allen Revisionen (ROLE_ADMIN). Beides mit CSRF, ohne JavaScript
bedienbar, mit Schutz für Elternseiten und veraltete Versionen.
Die öffentliche Route bedient verschachtelte Pfade und lässt die
CMS-Routen unberührt.
Testumgebung: Login-Throttling zählt jetzt in einem Array-Cache,
weil die persistierten Zähler die Anmeldung über Testläufe hinweg
aussperrten.
Geprüft: 56 PHP-Tests / 212 Assertions unter PHP 8.2.12 und 8.4.22,
10 JavaScript-Modelltests, Lint/Format/Container, Migration zuerst
gegen eine Datenbankkopie. Keine Browser- oder Lighthouse-Läufe.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>