- The installer only opens with a per-installation key that install.php
creates and shows; the key is deleted with the first account.
- Lock files keep .env, the database, backups and logs out of reach when a
domain points at the project folder instead of public/.
- Linked sections render by their own schema version, so an old section
cannot carry unfiltered HTML into a newer page.
- Editors no longer move, trash or change areas and post data of published
pages; a scheduled publication goes live with the revision that was
scheduled (migration 20261012000100).
- Form stamps are bound to their form, IPv6 senders count per /64, and a
flood per form or overall stops storing with a visible message.
- The consent redirect rejects control characters, update targets must look
like 1.2.3, and own CSS cannot reach outside its block.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A site can have further languages (Settings → Languages). A page is built once; each language
adds only its texts, keyed by the stable block IDs, plus its own address under /en/… (ADR 111,
document schema v27, migration 20261009000100 for page_translation).
- Editor: DE | EN switch, a list of all texts with original, state (missing, translated,
original changed) and filter; structure stays with the main language. Undo, drafts,
publishing and revisions cover all languages at once.
- Website: /en and translated slugs, lang, canonical, hreflang with x-default, og:locale,
sitemap; links to translated pages follow the language; a language switch block.
- Header, footer and linked sections translate the same way; menu entries carry labels per
language; fixed template texts come from a small dictionary.
- Forms: labels, help, options, button and success message per language; messages and the
step script speak the language, the submission and the email stay in the main language.
- Posts, archives, category names, consent text and the 404 page follow the language.
- Single-language sites see none of it and never touch the new table.
The forms administration becomes an overview with one row per form and its own edit page;
fields are one line each with details collapsed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The menu editor gets "Als Button darstellen" per entry: shape, appearance and
size from the button design, fill and ink with hover and palette, icon colour,
icon from the dialog, motion, icon-only and where the button sits on the phone
(in the opened menu, beside the burger, or there as an icon only). Colours go
into the page stylesheet under a stable per-button id because the CSP forbids
inline styles. ADR 110.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A menu entry picks its anchor from the linked page, link settings of plugins and own menu
links get page and PDF choice next to the address, and the post settings offer existing
categories and tags to click.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The share image and featured image of a page become image fields with the library dialog,
as do image settings of plugins. The editor offers forms, sections, categories and tags as
lists, a page search next to every link target and in text links, the redirect target has a
page search, and the form editor picks the reply field, the condition and the option images.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Umut asked for a block that takes custom HTML, and for an iframe on a new
DEEG offers page. Only one of those was missing.
New core block "Eigenes HTML" (document schema v24): one property, the
markup, up to 20.000 characters, written to the page unchanged. Changing it
is reserved for administrators and compared against what is stored - on
saving, on saving an area, and on restoring an older revision - exactly like
an own script. <script> and <iframe> are refused with the way to the right
place instead of being swallowed by the content security policy: scripts
belong in the script field, frames in the "Externer Inhalt" block, because
only that one asks for consent and puts the host into frame-src (ADR 106).
DEEG: /angebote existed as a draft with placeholder text. It is now built
like the legal pages - small caps line, large light heading, lead on the
tinted band - with the IS24 result list below it and a line pointing to the
portal and to the contact form. Navigation and the offers band on the home
page point there. The consent question is switched on, so the frame loads
only after the visitor agrees, and both the banner and the placeholder carry
the site's own shapes and colours.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Forms gained steps, conditions, image choices, number fields with units and a
summary step; the DEEG price finder uses all of it. Plugins are manifest plus
Twig: blocks, settings, containers, routes, presets and theme packs, switched
per installation. The review backlog is done: archive pages render through the
normal page pipeline, forms and post lists work inside areas and sections,
repeated sections get their own ids, breakpoints and colours come from the
palette, backup and update work on MySQL and lock each other out.
The task worker no longer shortens a process that has no time limit:
set_time_limit() sets the limit instead of extending it, which turned an
unlimited CLI run into eighty seconds and killed the full test suite far away
from the scheduler.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
SITE_ORIGIN alone decides whether the site is www or not: CanonicalHostSubscriber
sends the other spelling there with 301, cms:site:origin writes the setting, the
system screen shows it. The roles now match the role help (editor: drafts,
publisher: live content, admin: design, settings, users, system). The installer no
longer reopens on an unreachable database, the session cookie follows ADMIN_PATH,
percent-encoded credentials no longer break the container, consent only accepts
same-site decisions, JSON-LD is HTML-safe, own CSS stays in its block, plus the
editor, updater, download and public-output findings from the full review
(ADR 099, ADR 100).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Fonts were still reachable from Google in two places. The editor and area
previews were handed `googleFonts` and no `localFonts`, so the screen used
most of the day fetched Lato from fonts.googleapis.com on every open; the
public page fell back to the same link whenever the files were missing.
Neither path remains: the template has no Google branch, `googleFontsUrl()`
is gone, and the policies name no foreign font host - `font-src 'self'`
publicly, `style-src 'self'; font-src 'self'` in administration. Without
local files a site now shows the theme's fallback and says so.
Preloading took the first two weights per family. The Kant-IT theme names
Lato 400, 700 and 900 and sets its headings on 800/900, so the one weight
that carried them loaded after the stylesheet and `font-display:swap`
swapped the heading in plain sight. Every weight of the theme is preloaded
now, latin subset, and the links sit before the stylesheet: measured cold,
all three files finish at 168 ms against a first paint at 292 ms.
Open tasks no longer hide on the screen that owns them. A strip above every
management screen names what needs a button and carries it: a new version
leads to System, missing fonts to Design. Neither asks the network - the
version comes from the daily update-check task, the fonts from a look in
the folder.
System and update was unusable on a phone: `.stack` is a grid whose
automatic column grows to its widest content, and one long database path
pushed the page sideways. It gets `minmax(0, 1fr)`, the backup table stacks
below 620px, and the prose is 45% shorter with the fine print folded away.
The device chips drew the current device with an outline, which lies outside
the box, ran under its neighbour and was clipped by the sidebar - it read as
a cut-off red frame. Used devices are filled in `#9d4e0d` now (5.6:1), the
current one carries a border inside the chip.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Cards, quotes and call-to-action bands side by side in a row are equal in
height again: the stacking margin meant for a column no longer applies inside
a row. The header can carry a second set of colours for the scrolled page,
the Flatsome transparent header: a "scrolled" state next to "hover", limited
to colours, shadow and opacity, marked by a small script only where it is
used, plus a setting that lets the header float over the first section.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Wiederkehrende Aufgaben ohne Cronjob (ADR 073): Sicherung, Statistik kuerzen,
geplante Veroeffentlichung, Updatepruefung und Aufraeumen laufen als Plan in
der Tabelle scheduled_task - eine Zeile je Aufgabe mit Faelligkeit, Sperre und
letztem Ergebnis. Auf geteiltem Hosting gibt es oft keinen Cronjob; der Plan
haengt stattdessen an Anfragen und an einem Takt aus dem geoeffneten Backend.
Diese Arbeit entstand in einer parallel laufenden Sitzung und lag bis hierher
unveroeffentlicht im Arbeitsverzeichnis: die Releases 1.5.0 bis 1.5.5 wurden
bewusst aus dem jeweiligen Tag gebaut und enthielten sie deshalb nicht.
Vor der Veroeffentlichung geprueft, nicht bloss eingepackt:
- phpunit 452/452, npm test 28/28, npm run test:editor vollstaendig.
- Die Migration 20260914000100 legt nur die Tabelle scheduled_task an und nimmt
sie beim Zurueckrollen wieder weg. Sie fasst keine bestehenden Daten an.
- Seit einer Stunde keine Schreibzugriffe der anderen Sitzung mehr.
Dabei zwei Fehler behoben:
- config/services.yaml verwendet !tagged_iterator. Das ist gueltiges Symfony,
aber "lint:yaml config" kennt eigene Tags nur mit --parse-tags und gab 1
zurueck - der CI-Lauf waere daran gescheitert. Flag ergaenzt.
- AdminHeadersSubscriber.php (aus 1.5.3) entsprach nicht dem Stil von
php-cs-fixer; composer lint gab 8 zurueck. Behoben.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
php bin/console cms:admin:path <neu> legt die Verwaltung auf eine
andere Adresse. Geprueft wird, dass der Name frei ist und keine Seite
verdeckt; der Wert steht in .env.local und fuellt einen Parameter, der
als Praefix in jeder Verwaltungsroute steht, in der Firewall, in den
Schutzkopfzeilen und in der Statistik.
Die robots.txt nennt einen geaenderten Namen nicht - sie ist oeffentlich
und waere sonst ein Wegweiser. Beim Standardnamen bleibt die Zeile,
weil diese Adresse ohnehin jeder kennt.
Die Auffangroute fuer Seiten steht jetzt zuletzt: sie kann einen frei
gewaehlten Namen nicht per Regex ausschliessen, und die Reihenfolge ist
ohnehin die richtige. Der Befehl raeumt den Zwischenspeicher auf, aber
nicht den, in dem er selbst laeuft.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Getrennter Titel fuer Suchergebnisse je Seite und Beitrag (Migration
20260913000100): auf der Seite steht "Leistungen", im Suchergebnis darf
der lange Titel stehen. Leer heisst weiterhin: der Titel der Seite.
Schriften kommen jetzt vom eigenen Server. Ein Knopf im Designbereich
oder cms:fonts:fetch holt die gewaehlten Google-Schnitte als WOFF2 nach
public/assets/fonts. Das spart zwei fremde Verbindungen nacheinander
(gemessen 820 ms) und die IP-Weitergabe an Google. Ohne Dateien bleibt
es beim bisherigen Verweis - eine Seite ohne Schrift waere schlimmer.
Ein Stylesheet statt vier, mit Pruefsumme im Namen und ohne Kommentare,
dazu eine .htaccess mit Kompression, Cache und Schutz interner Dateien.
Der Entwicklungsserver sendet dieselben Kopfzeilen, sonst misst man
lokal etwas, das es im Betrieb nicht gibt.
Bilder in der Groesse, in der sie erscheinen: ein nur fuer Desktop
gesetztes Hintergrundbild gilt auf dem Telefon in Telefongroesse
(189 KB -> 40 KB), img nennt seine feste Breite oder sizes="auto", und
fetchpriority liegt auf dem ersten Bild des Inhalts statt auf dem Logo.
Neu: npm run test:links prueft jeden internen Verweis auf seine
Antwort. Lighthouse gegen den Nachbau: Barrierefreiheit, Best
Practices und SEO 100 auf allen vier geprueften Adressen, Performance
100 ausser auf der bilderreichen Startseite (98 mobil, 96 Desktop).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Umut wollte mehrere Designs zur Auswahl. Bei Seiten waere das falsch -
dort weiss vorher niemand, was daraus wird. Bei Beitraegen ist die Form
fast immer dieselbe, und wer jedes Mal von vorn anfaengt, baut jedes Mal
etwas leicht anderes.
Bildkopf (Bild ueber die volle Breite, Titel darauf), Klassisch (Titel und
Vorspann schmal, Bild darunter) und Magazin (Titel neben den Eckdaten).
Gewaehlt wird beim Anlegen; danach ist jeder Block aenderbar wie jeder
andere. Kein Aufbau verlaesst sich auf ein Bild, das es noch nicht gibt.
Dabei ist eine Regel gefallen: der erste Textblock eines Abschnitts wurde
automatisch zur Kleinbeschriftung in Akzentfarbe. Das gestaltet nach
Position statt nach Absicht - im Fussbereich wurde daraus roter
Versalientext, in der Magazin-Vorlage aus dem Einstiegsabsatz eine
Beschriftung. Wer eine Beschriftung will, stellt sie ein.
Geprueft: 398 Tests unter PHP 8.2 und 8.4, Editortest, Browserpruefung,
axe ohne Verstoss auf allen drei Aufbauten.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Arbeitspaket M sagte "Abfragen je Seite zaehlen. Messen, dann zusammenfassen."
Messen braucht ein Messgeraet, also gibt es jetzt eines: ein Zaehler als
Doctrine-Middleware, dessen Zahl ausserhalb der Produktion im Kopf
X-Kant-Queries steht. Keine Profiler-Maschinerie, eine Zahl.
Die erste Messung: die Startseite brauchte 18 Abfragen. Nicht wegen einer
komplizierten Seite, sondern weil SiteSettings::load() und Theme::load() je
Aufrufer dieselbe Zeile erneut holten - Renderer, strukturierte Daten,
Einwilligungsschalter, Vorlagen. Beide merken sich ihren Stand jetzt fuer die
Dauer einer Anfrage: 18 auf 8.
Dabei ist mir die klassische Falle des Merkens selbst passiert, und ein
bestehender Test hat sie sofort gezeigt: ich leerte den Merker am Anfang des
Speicherns. Das Speichern liest den Stand aber noch einmal und fuellte ihn
damit erneut - der Rest der Anfrage bekam den alten Stand. Geleert wird jetzt
nach dem Schreiben.
Das Ladebudget wird gemessen statt behauptet: die Browserpruefung liest je
Seite HTML-Groesse, CSS-Groesse, Anzahl Anfragen, Anzahl Skripte und
Abfragezahl und prueft Obergrenzen. Startseite: 7,4 KB HTML, 22,7 KB CSS, 2
Anfragen, 8 Abfragen, 0 Skripte. Die Null ist die wichtigste Zahl - sie haelt
als Test fest, was bisher nur Absicht war.
Das erste Bild einer Seite ist nicht mehr lazy. Es ist meistens das groesste
Ding ueber der Falz, und es zu verzoegern verzoegert genau das Pixel, auf das
jemand wartet. Der Renderer bestimmt es in Renderreihenfolge, Kopfbereich
zuerst.
Eine feste Obergrenze ist kein Blaettern: der Fassungsverlauf hoerte bei
dreissig auf, die Einsendungen bei fuenfzig, die Weiterleitungen zeigten bis
zu zweitausend auf einmal. Das Erste sieht aus wie "es gibt dreissig" und ist
das Einzige, was eine Historie nicht tun darf. Alle drei blaettern jetzt, mit
derselben Rechnung und demselben Pager wie Beitragsarchiv und Mediathek.
347 PHP-Tests / 1422 Assertions unter PHP 8.2.12 und 8.4.22.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Drei Fragen, drei unterschiedliche Antworten.
Die globale Seitenbreite war einstellbar und wirkte nur in eine Richtung:
site.css schrieb width:min(1180px,...), also schluckte die 1180 jeden
groesseren Wert aus dem Designbereich. Schmaler ging, breiter nicht, ohne
jede Meldung. Die Zahl kommt jetzt aus dem Theme. Das ist der dritte Fehler
dieser Art an einem Tag - nach der Rahmenstaerke und der Bildrundung -, und
alle drei hatten dieselbe Ursache: CSS, das nie gegen eine Erwartung geprueft
wurde.
Menueplaetze und Menueart waren vermischt. Wo ein Menue steht, bestimmt der
Block und war immer frei. Welche Eintraege es zeigt, bestimmt der Platz -
davon gab es zwei, jetzt vier (Haupt, zweite, Fusszeile, Rechtliches). Wie es
aussieht, war gar nicht einstellbar: jetzt waagerecht, untereinander oder
aufklappbar als Burger. Der Burger ist ein details-Element, denn ein Menue,
das ein Skript zum Oeffnen braucht, geht auf einer Seite ohne Skript nicht auf.
"Seiten ohne Menue" war nicht moeglich - der Kopfbereich war ein Dokument fuer
alle Seiten. Jetzt waehlt jede Seite: Standard, ein anderer Bereich, oder
keiner. Dafuer musste die Regel "es gibt genau einen Kopf- und Fussbereich"
fallen; sie war richtig, solange niemand waehlen konnte, und wurde falsch,
sobald man waehlen kann. Ein bestehender Test, der diese Regel zusicherte, ist
deshalb umgeschrieben und prueft jetzt die neue.
Gespeichert wird die Wahl, nicht das Ergebnis: leer heisst Standard, none
heisst keiner, sonst die Adresse. Ein Verweis auf einen geloeschten Bereich
faellt auf den Standard zurueck, statt die Seite stillschweigend ohne
Navigation auszuliefern.
Neu im Ausbauplan: ein einzelnes install.php, das das Releasearchiv
herunterlaedt und entpackt, damit niemand tausende Dateien per FTP hochladen
muss. Mit den Regeln, die darueber entscheiden, ob das eine Hilfe oder eine
Einladung ist: Adresse und SHA-256 stehen in der Datei und nicht im Formular,
HTTPS mit Zertifikatspruefung, Schutz gegen ZIP-Slip, Selbstloeschung danach.
336 PHP-Tests / 1354 Assertions, 17 JS-Modelltests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Dokumentschema v13. Der neue Reiter "Erweitert" im Editor nimmt zu jedem
Element eigenes CSS und eigenes Skript auf.
Eigenes CSS parst `CustomCode` und schreibt es selbst wieder aus, mit einem
Selektor, den die Klasse setzt und der nie aus der Eingabe stammt. Erlaubt
sind Eigenschaften für das Element und verschachtelte Regeln mit `&`
(`&:hover`, `& > p`, `& .block-text`). Weil neu serialisiert wird, kann eine
überzählige `}` den Block nicht verlassen. Abgewiesen: @-Regeln,
Backslash-Escapes, `expression()`, fremde Selektoren und jede Adresse außer
einem einzelnen `/` oder `https://`. Das darf jeder mit Editorrolle.
Eigenes Skript ist etwas anderes: beliebiges JavaScript auf der
veröffentlichten Website. Deshalb nur für Administratoren, durchgesetzt beim
Schreiben über einen Vergleich mit dem gespeicherten Stand - ein Editor kann
eine Seite mit Skript weiter bearbeiten, das Skript aber nicht anfassen, auch
nicht über eine selbst gebaute Anfrage. Ausgeliefert wird es als
`/assets/seite/{id}.js` und nur für veröffentlichte Seiten, jedes Skript in
einer eigenen Closure mit gesuchtem `element` und `try`/`catch`. Nur eine
Seite mit eigenem Skript sendet `script-src 'self'`, jede andere behält
`script-src 'none'`; inline ist nirgends erlaubt. In der Editorvorschau wird
nichts ausgeführt.
Zwei echte Löcher kamen beim Testen heraus und sind behoben: `& .block-text`
wurde zu `#id.block-text`, weil das führende Leerzeichen wegtrimmt wurde, und
`url(//fremd.example/x.png)` kam durch, weil jede Adresse mit `/` erlaubt war.
201 PHP-Tests / 716 Assertions unter PHP 8.2.12 und 8.4.22, Playwright-
Editortest und öffentliche Browserprüfung grün. Nicht geprüft: der Reiter
"Erweitert" ist nicht von Hand bedient und nicht mit einem Screenreader
getestet.
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>
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>
Der Playwright-Editortest war durch den Editorumbau kaputt: er
steuerte die abgeschafften Auswahllisten. Umgeschrieben auf die
freien Bedienelemente - Einheitenfelder, Vererbung über drei Geräte,
einzelnes Zurücksetzen, Palettenfarbe, Abstand je Seite, Hover-Regel,
freie Rasterspalten und die weiter vorhandenen Bewegungseffekte.
Dabei kamen drei Fehler heraus, die ohne Browserlauf unsichtbar sind:
1. Die frei gestalteten Werte wirkten in der Editorvorschau gar
nicht. Ein srcdoc-Iframe erbt die Richtlinie des einbettenden
Dokuments, und die Verwaltung verbietet Inline-Styles - das
erzeugte style-Element wurde stillschweigend verworfen.
Ausgeliefert wurde es korrekt, nur die Vorschau log. Jetzt als
inertes template plus adoptedStyleSheets.
2. Die Einrückung der Unterseiten in der Seitenliste hat nie
funktioniert; sie lief über ein style-Attribut, das dieselbe
Richtlinie verwirft. Jetzt über Klassen.
3. Das Theme setzte alle Links auf die Akzentfarbe und überschrieb
die kontextabhängigen Farben: 3,08:1 auf dunklem Grund. Die Regel
gilt jetzt nur für Links in Textblöcken, die voreingestellte
Linkfarbe liegt auf allen hellen Tönen über 4,5:1.
Geprüft: 155 PHP-Tests / 579 Assertions unter PHP 8.2.12 und 8.4.22,
15 JavaScript-Modelltests, vollständiger Editortest mit axe (null
Verstöße) und Lighthouse 13.4.1 mit je drei Läufen mobil und Desktop,
alle fünf Kategorien 100.
Offen: npm run test:browser schlägt wegen eines Inhalts fehl -
/grundlagen trägt einen im Editor gesetzten Textblock mit 2,75:1.
Nicht verändert, weil es eine Nutzerentscheidung ist.
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>
Der bisherige Editor bot drei Farben, keine Schriftwahl und elf
Auswahllisten mit 44 festen Werten. ADR 008 hatte das mit Sicherheit
begründet - der Schluss war falsch: die Alternative zu beliebigem
CSS-Text ist nicht eine Auswahlliste, sondern ein typisiertes
Designmodell.
StyleSchema erklärt jede Eigenschaft einmal mit Typ, CSS-Entsprechung
und Grenzen. Validator, CSS-Erzeugung und Editorpanel lesen dieselbe
Definition, also kann der Editor nichts anbieten, was der Server
ablehnt. Rund fünfzig Eigenschaften in sieben Gruppen, je Block, je
Gerät und für den Hover-Zustand, mit Vererbung und einzelnem
Zurücksetzen.
Werte sind kurze Zeichenketten mit verankertem Muster, nie CSS-Text;
zusammengesetzte Deklarationen baut der Compiler selbst. Der Server
erzeugt daraus pro Seite ein blockgenaues Stylesheet, das über seinen
eigenen SHA-256-Hash erlaubt wird statt über unsafe-inline.
Theme unter /admin/theme: freie Farbpalette als @schlüssel, Schriften
aus Systemschrift oder Google Fonts, Grundtypografie mit modularer
Überschriftenskala. Google Fonts auf ausdrückliche Nutzerentscheidung;
die Richtlinie öffnet die Hosts nur, wenn wirklich eine eingestellt
ist.
Alte Tokenwerte aus v2 bis v9 bleiben gültig und im Editor unter
"Klassische Werte dieser Seite" bearbeitbar.
Geprüft: 154 PHP-Tests / 563 Assertions unter PHP 8.2.12 und 8.4.22,
15 JavaScript-Modelltests, Lint/Format/Container. Im Browser wurde nur
der Aufbau des Panels bestätigt, kein Gestaltungsvorgang von Hand
ausgeführt.
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>
Auf Entscheidung von Umut werden Tabs jetzt mit JavaScript umgesetzt.
Die Regel "der Seiteninhalt braucht kein Browser-JavaScript" bleibt
dabei eingehalten: der Server liefert eine Ankerliste und alle Panels
mit eigenen Überschriften, versteckt nichts, und ohne Skript ist
jeder Inhalt lesbar und über die Tastatur erreichbar.
public/assets/tabs.js ergänzt tablist/tab/tabpanel, Auswahl per
Pfeiltasten, Home und Ende sowie aria-selected und wählt bei einem
passenden Sprungziel das zugehörige Panel.
Die strenge Richtlinie bleibt Voreinstellung: öffentliche Seiten
senden weiterhin script-src 'none'. Nur eine Seite mit Tab-Block
sendet script-src 'self' und bindet die eine externe Datei ein;
Inline-Skript ist nie erlaubt.
Geprüft: 109 PHP-Tests / 395 Assertions unter PHP 8.2.12 und 8.4.22,
14 JavaScript-Modelltests, Lint/Format/Container. Das Skript selbst
wurde nicht ausgeführt: Tastaturbedienung und ARIA-Verhalten sind
ohne Browserlauf ungeprüft.
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>
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>
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>