KantSites
Lokaler CMS-Prototyp für PHP-Webhosting mit erstem visuellen Editor. Symfony 7.4, Twig, Doctrine DBAL/Migrations; SQLite lokal, MySQL/MariaDB als Hostingziel. Drei gespeicherte Beispielseiten werden ohne JavaScript als HTML ausgeliefert. Anmeldung, geschützte Bearbeitung und getrennte Veröffentlichung sind implementiert; weitere Layoutwerkzeuge folgen.
Die übernommenen Originaldokumente sind AGENTS.md und ENTWICKLUNGSPLAN.md. Den tatsächlichen Stand enthält docs/STATUS.md, Entscheidungen und Datenmodell stehen in docs/ARCHITECTURE.md.
Lokal starten
Voraussetzungen: PHP ab 8.2 mit Composer 2, PDO SQLite, Ctype, Iconv, Session, XML und GD mit WebP-Unterstützung. EXIF wird für die automatische Ausrichtung von JPEG-Fotos empfohlen. Für Tests zusätzlich die von composer install geprüften Erweiterungen, insbesondere DOM, XMLWriter und Mbstring. Für Hosting eine aktuell gepflegte PHP-Patchversion verwenden. Node 24 wird für Browserprüfungen und das Generieren der Design-CSS gebraucht, nicht im Hostingbetrieb.
composer install
php bin/setup.php
php vendor/bin/doctrine-migrations migrate --no-interaction
php bin/console cms:seed-demo
php -d upload_max_filesize=8M -d post_max_size=10M -d memory_limit=256M -S 127.0.0.1:8080 -t public bin/dev-router.php
Danach http://127.0.0.1:8080/ öffnen. Der Routerskript-Parameter ist nicht optional: der eingebaute PHP-Server liefert vorhandene Dateien selbst aus und antwortet für jeden fehlenden Pfad mit Dateiendung mit 404, sodass /sitemap.xml, /robots.txt, /llms.txt und /feed.xml den Frontcontroller nie erreichen. Auf echtem Hosting erledigt das public/.htaccess; bin/dev-router.php bildet dasselbe lokal nach. Unter Windows hier vorhanden: C:\xampp\php\php.exe (8.2.12) und C:\php84\php.exe (8.4.22). Die Befehle verwenden die PHP-Version im PATH; alternativ den vollständigen Pfad einsetzen. Diese vorhandenen Patchstände sind kein empfohlenes Produktionsimage.
bin/setup.php erzeugt ein zufälliges lokales Secret und var/; bestehende .env.local bleibt erhalten. Die Datenbank liegt in var/cms.db. Wiederholtes Seeding erhält existierende Seiten einschließlich ihrer Bearbeitungen. Standardzugangsdaten gibt es nicht.
Installation ohne Konsole läuft über /install: Systemprüfung, Datenbank, Migration, Website, erstes Konto. Der Installer antwortet nur, solange keine Installation existiert – sobald ein Konto angelegt ist, liefert jede Adresse darunter 404, und die Sperre hängt am Zustand der Datenbank, nicht an einer Datei. Solange nichts installiert ist, führt jede Adresse dorthin. Davor genügt eine einzelne hochgeladene Datei: install.php aus einem Release lädt das Archiv von einer fest eingetragenen Adresse, vergleicht die SHA-256, entpackt es und löscht sich selbst. Ohne passende Prüfsumme wird nichts entpackt. Editor-Endpunkte benötigen Anmeldung, Berechtigung und bei POST einen CSRF-Token.
Anmelden und bearbeiten
Für eine neue Installation im interaktiven Terminal ein Konto anlegen:
php bin/console cms:user:create meinname
Das Passwort wird verdeckt zweimal abgefragt (14–72 Bytes). Bestehende Benutzer werden nicht überschrieben. Optional --role=editor für Entwürfe oder --role=publisher für Entwürfe plus Veröffentlichung; Standard ist admin. Es gibt noch keine Benutzerverwaltung oder Passwortänderung im Browser. In der aktuellen lokalen Installation wurde auf Nutzerwunsch bereits der Administrator umut angelegt und angemeldet; keine Zugangsdaten sind im Repository abgelegt.
Unter http://127.0.0.1:8080/admin eine Seite auswählen. Links Blöcke hinzufügen/auswählen, in der Mitte die Vorschau, rechts Eigenschaften. Die Auswahl ist zwischen Strukturbaum und Vorschau synchron. Sektion, Zeile, Spalte, Überschrift, Text, Bild, Button und Galerie sind vorhanden. Bilder können aus der Mediathek oder über vorhandene lokale/HTTPS-Adressen eingebunden werden.
Blöcke per Drag-and-drop verschieben: auf einen Container ziehen, um hineinzuverschieben; auf einen Text-/Bild-/Linkblock ziehen, um davor einzufügen. Alternativ unten am Strukturbaum die Schaltflächen für Auf/Ab, Verschachteln und Herausheben verwenden. Duplizieren vergibt neue Block-IDs und wandelt eine kopierte H1 in H2 um. Löschen lässt sich rückgängig machen. Jede speicherbare Seite benötigt genau eine H1.
Undo/Redo gilt für die lokale Bearbeitung, Strg+S speichert den Entwurf. Strg+Z/Strg+Umschalt+Z gelten außerhalb der Texteingaben für das Dokument; Texteingaben behalten ihr natives Undo. Erst speichern, dann separat veröffentlichen. Die Bestätigungsdialoge unterstützen Escape und Fokus-Rückkehr. Reine Redakteure können nicht veröffentlichen.
Ein Speicherkonflikt überschreibt nichts: lokale Änderungen als JSON sichern und den Serverstand bewusst neu laden. Auch Netzwerk-/Sitzungsfehler lassen den lokalen Stand erhalten. Die Vorschau behält bei ungültigen Eingaben die letzte gültige Ausgabe.
Rechts zwischen Inhalt und Design wechseln. Unter Design je nach Block Abstände, Schriftgröße/-stärke, Zeilenhöhe, Ausrichtung, Spalten und Ecken wählen. Oben Desktop/Tablet/Mobil bestimmt das bearbeitete Gerät: Desktop ist die Basis, Tablet überschreibt bis 1024 px, Mobil bis 650 px. „Übernehmen“ entfernt einen eigenen Wert und stellt die Vererbung wieder her. Gerätewerte lassen sich gemeinsam zurücksetzen und rückgängig machen. Farben und Inhaltsbreite einer Sektion stehen unter Inhalt.
Die Vorschau rendert auf Desktop mit mindestens 1240 px Layoutbreite und wächst bei mehr Platz mit. Tablet/Mobil verwenden 768/390 px; bei weniger Platz wird die Vorschau passend skaliert. Links lassen sich fertige Intro-, Leistungs- und Kontaktabschnitte einfügen. Der Werkzeugkasten kann für mehr Platz geschlossen werden. Die Struktursuche findet Text oder Elementtypen und zeigt den Pfad zum Treffer; Dreiecke klappen Teilbäume ein. Oberhalb der Vorschau führen die Pfadschaltflächen direkt zum übergeordneten Element.
Platz und Blockgestaltung im Editor (1.6.1)
Neue Sektion auf Seitenebene steht dauerhaft oben links und fügt nach dem aktuellen Seitenabschnitt ein, unabhängig von der ausgewählten Verschachtelung oder einem gesetzten Plus-Einfügepunkt. Auch „Sektion“ im Werkzeugkasten verwendet ohne expliziten Einfügepunkt die Seitenebene. Verschachtelte Sektionen sind weiterhin über die Plus-Einfügepunkte oder Drag-and-drop möglich.
Die Trennlinien links und rechts der Vorschau lassen sich ziehen. Mit Tab fokussieren und mit den Pfeiltasten ändern; Umschalt vergrößert den Schritt, Home/End wählen die Grenzen, Doppelklick setzt die jeweilige Breite zurück. Die Breiten werden im Browser behalten und bei schmalen Fenstern an den verfügbaren Platz angepasst. Auf dem Telefon stehen die Panels unter der Vorschau.
Text und Überschriften nutzen die Containerbreite. Die Sektion wählt unter Inhalt → Inhaltsbreite zwischen Websitebreite und randloser Ausgabe. Design → Größe → Maximale Breite begrenzt ein einzelnes Element ausdrücklich, etwa auf 60ch; „Ohne Begrenzung“ hebt einen eigenen Grenzwert auf. Der Feinschliff zeigt passende Optionen für Text, Bild oder Container zuerst. Alle Eigenschaften anzeigen macht die übrigen Optionen zugänglich. Bereits gespeicherte Werte bleiben sichtbar.
Produktionsmodus lokal prüfen
composer build
$env:APP_ENV = 'prod'
$env:APP_DEBUG = '0'
php -d upload_max_filesize=8M -d post_max_size=10M -d memory_limit=256M -S 127.0.0.1:8080 -t public bin/dev-router.php
Unter Linux dieselben PHP-Argumente mit vorangestelltem APP_ENV=prod APP_DEBUG=0 verwenden. Vorher eine laufende Instanz auf diesem Port beenden. Zum Entwickeln APP_ENV=dev und APP_DEBUG=1 setzen. Debug-Seiten senden absichtlich X-Robots-Tag: noindex; Audits laufen im Produktionsmodus. Der eingebaute PHP-Server bleibt ein lokaler Entwicklungsserver.
Medien, Galerien und Effekte
Links im Werkzeugkasten Medienverwaltung öffnen oder auf der Seitenübersicht Medienverwaltung wählen. JPEG/PNG/WebP hochladen, Titel und Alternativtext bearbeiten und speichern. Bilder lassen sich suchen, archivieren und wiederherstellen. Die Dateien werden als optimierte WebP-Varianten in vier Breiten gespeichert (480, 960, 1600, 2400 px; ein schmaleres Bild bekommt weniger Stufen und wird nie hochskaliert). Originaldateien werden nicht aufgehoben. Bildbausteine liefern srcset, Hintergrundbilder wählen die Variante je Gerät. Uploadgrenzen: 8 MB, 12 Megapixel und 8000 px Kantenlänge; Hostinglimits können niedriger sein. Bild-URLs sind öffentlich erreichbar, auch wenn sie noch in keinem veröffentlichten Inhalt verwendet werden.
Für ein einzelnes Bild Bild aus Mediathek wählen anklicken. Für eine Galerie den Block Galerie einfügen und Galeriebilder auswählen öffnen. Mehrere Bilder markieren, Alt-Texte speichern oder die dekorative Verwendung wählen und Auswahl übernehmen. Im Strukturbaum einzelne Bilder auswählen, beschriften und sortieren; Reihenfolge und Einfügen lassen sich rückgängig machen. Unter Design Spaltenzahl und Abstände pro Gerät einstellen.
Unter Design → Effekte Schatten, Hover-Anheben/Bildzoom und Einblendung beim Laden wählen. Effekte gelten für alle Geräte; bei aktivierter Bewegungsreduktion werden Animationen abgeschaltet. Die öffentliche Seite benötigt dafür kein JavaScript. Lightbox, Slideshow, Video/PDF-Upload und Bildzuschnitt sind noch nicht enthalten.
Medien liegen standardmäßig in var/media; mit MEDIA_DIRECTORY lässt sich ein absoluter Ordner außerhalb von public konfigurieren. Er muss für PHP beschreibbar sein. Auf dem Hosting upload_max_filesize=8M, post_max_size=10M und ausreichend Arbeitsspeicher (lokal 256M) einstellen, soweit der Tarif dies erlaubt. Datenbank und Medienordner gemeinsam sichern. Archivieren erhält Bild-URLs; es ist keine Dateilöschung.
Prüfen
composer validate --strict
composer lint
composer test
php bin/console lint:twig templates
php bin/console lint:yaml config
php bin/console lint:container
composer audit
composer build
npm.cmd ci
npx.cmd playwright install chromium
npm.cmd run test:browser
npm.cmd run test:links
npm.cmd test
npm.cmd run test:editor
npm.cmd run test:editor -- --audit
npm.cmd run audit
Unter Linux/macOS npm und npx ohne .cmd; in CI npx playwright install --with-deps chromium. test:browser und audit benötigen den laufenden lokalen Produktionsmodus auf Port 8080. Ein anderes Ziel lässt sich über CMS_URL konfigurieren. test:editor startet einen eigenen PHP-Prozess auf einem freien Port mit isolierter Datenbank und zufälligem Testkonto; es ändert keine echten Seiten oder Benutzer. PHP_BINARY kann dafür einen alternativen PHP-Pfad angeben. Testdaten bleiben unter var/browser-tests, Berichte/Screenshots unter reports/. Das Skript erneuert zuvor den Produktionscache.
Datenbank: SQLite ab Fassung 3.16 (Doctrine liest das Schema mit Tabellenfunktionen, die es erst seit dieser Fassung gibt) oder MySQL/MariaDB. Auf älteren Systemen – manche Shared Hoster liefern noch SQLite 3.7 – bleibt MySQL; die Systemprüfung des Installers nennt die gefundene Fassung und bietet SQLite gar nicht erst an, wenn sie zu alt ist.
Die Verwaltung liegt unter /admin. php bin/console cms:admin:path meine-verwaltung legt sie auf eine andere Adresse: der Wert landet in .env.local, gilt für Routen, Firewall und Schutzkopfzeilen, und wird in der robots.txt bewusst nicht genannt. Ohne Argument zeigt der Befehl die aktuelle Adresse.
composer format formatiert PHP. composer build wärmt den Symfony-Produktionscache; öffentliche CSS-Dateien sind direkt auslieferbar, es ist kein Frontend-Bundler nötig. Bei Änderungen an public/assets/design-tokens.json einmal npm.cmd run build:design ausführen und die erzeugte CSS-Datei mitliefern; Node ist auf dem Hosting nicht erforderlich. Nach Änderungen an Produktionskonfiguration/Templates php bin/console cache:clear --env=prod --no-debug ausführen. Falls lokale Tests alte Templates verwenden, vor PHPUnit auch php bin/console cache:clear --env=test --no-debug ausführen.
test:links folgt der Sitemap, sammelt jeden internen Verweis samt Bildern, Stylesheets und Hintergrundbildern und meldet jede Antwort ab 400 – gebaut, nachdem im Nachbau von kant-it.de vier Verweise ins Leere zeigten, die kein Test finden konnte.
Das Lighthouse-Skript misst dreimal je Profil und prüft die Mediane gegen die Projektziele. Welche Adressen geprüft werden, sagt CMS_PATHS (mit Komma getrennt, Standard /); in der Git-Bash davor MSYS_NO_PATHCONV=1 setzen, sonst wird aus / ein Windows-Pfad. test:editor -- --audit prüft die im isolierten Editortest gestaltete v3-Seite mit Abschnittsvorlage, Galerie, tatsächlichen Bildvarianten und Effekten; Berichte stehen unter reports/lighthouse-editor. Uploads und Datenbank des Tests liegen getrennt unter var/browser-tests. Diese kleinen Prototypseiten ersetzen nicht die spätere Referenzsuite mit Kundeninhalten, Fotos, Beitrag und Formular. Messgrenzen und Ergebnisse: docs/QUALITY.md.
Inhalte und Entwicklung
Die Formate v1/v2/v3 und erlaubten Blöcke stehen in src/Content/DocumentValidator.php, Gestaltungstokens in public/assets/design-tokens.json, Entwicklungsinhalte in src/Content/DemoContent.php. Designwerte erfordern mindestens v2, Medien/Galerien/Effekte v3. Die erste entsprechende Aktion migriert den lokalen Entwurf erhaltend; erst Speichern schreibt eine neue Revision. Bestehende v1/v2-Seiten bleiben lesbar. Migrations- und Rückweg: ADR 008/009 in docs/ARCHITECTURE.md. PageRepository stellt saveDraft, publish und findPublished bereit. Speichern und Publizieren erfordern die erwartete Seitenversion; veraltete Zugriffe brechen mit StaleWrite ab.
Ein JSON-Dokument lässt sich zusätzlich lokal über php bin/console cms:save-draft PAGE_ID PFAD_ZUR_DATEI ERWARTETE_VERSION speichern. Der Befehl gibt die neue Revisions-ID aus. Die redaktionelle Bearbeitung und Veröffentlichung erfolgen jetzt über den geschützten Editor.
Wiederkehrende Aufgaben
Sicherung, geplante Veröffentlichung, Statistik verdichten, Updateprüfung und Aufräumen laufen über einen eingebauten Plan. Auf geteiltem Hosting gibt es oft keinen Cronjob – deshalb stößt hier die Anfrage die Arbeit an, nach der ausgelieferten Antwort (kernel.terminate), sodass kein Besucher darauf wartet. Der Bildschirm Verwaltung → Aufgaben zeigt Plan, letztes Ergebnis und Protokoll und schaltet jede Aufgabe einzeln.
Vier Auslöser, in aufsteigender Verlässlichkeit:
- Besuch – jeder Seitenaufruf prüft, ob etwas fällig ist. Der Normalfall kostet eine gelesene Datei (
var/scheduler.json) und keine Datenbankabfrage. Standardmäßig an. - Geöffnete Verwaltung – das Backend fragt jede Minute nach, solange der Reiter sichtbar ist. Damit läuft der Plan auch auf einer Website ohne Besucher. Standardmäßig an.
- URL-Cronjob – wenn der Hoster nur das Aufrufen einer Adresse anbietet: die Adresse steht auf dem Bildschirm „Aufgaben“ (
/cms-cron/<Schlüssel>, ausAPP_SECRETabgeleitet), zum Beispiel alle fünf Minuten. - Konsole –
php bin/console cms:tasks:run, für einen echten Cronjob jede Minute:
* * * * * cd /pfad/zur/installation && php bin/console cms:tasks:run
cms:tasks zeigt denselben Plan wie die Verwaltung (--log zusätzlich das Protokoll), cms:tasks:run --task=backup startet eine bestimmte Aufgabe sofort, cms:tasks:run --loop=60 --max-time=3600 läuft als Dauerläufer unter einem Prozessverwalter. Die alten Einzelbefehle cms:backup, cms:stats:prune und cms:publish-due gibt es weiterhin; wer sie in einen Cronjob einträgt, schaltet die entsprechende Aufgabe im Plan besser ab, damit nicht beides läuft.
Die Grenze gehört dazu: ohne Besuch und ohne geöffnetes Backend passiert nichts. Eine Aufgabe für 03:00 Uhr läuft auf einer stillen Website erst beim nächsten Aufruf, vielleicht um 07:15 Uhr. Wer Minutengenauigkeit braucht, richtet Weg 3 oder 4 ein und schaltet den Besuchs-Auslöser ab.
PHP-Webhosting
Anforderungen vor dem Pilotbetrieb beim Anbieter bestätigen:
- Gepflegte PHP-Version ab 8.2, PDO MySQL und die Composer-Plattformanforderungen.
- MySQL 8 oder kompatible MariaDB; tatsächliche Version und Berechtigungen noch überprüfen.
- Domain-Dokumentenstamm muss auf
public/zeigen.vendor/,var/, Konfiguration und Secrets gehören außerhalb des öffentlichen Verzeichnisses. - Apache mit Rewrite-Unterstützung für
public/.htaccessoder entsprechende FastCGI/Nginx-Konfiguration. - Schreibzugriff für PHP auf
var/, HTTPS und eine Möglichkeit, Migrationen/Cache-Kommandos kontrolliert auszuführen.
Serverkonfiguration über echte Umgebungsvariablen oder eine nicht öffentliche .env.local: APP_ENV=prod, APP_DEBUG=0, eigenes zufälliges APP_SECRET, SITE_ORIGIN=https://eigene-domain.example und DATABASE_URL=mysql://USER:URL_ENCODED_PASSWORD@HOST:3306/DATABASE?charset=utf8mb4. Das ist eine Vorlage, keine funktionierende Zugangskonfiguration.
Composer wird auf dem Hosting nicht vorausgesetzt. Ein Release wird auf einem Rechner gebaut, der Composer hat: php scripts/build-release.php legt var/releases/kantsites-<Fassung>.zip samt .sha256 an. Das Archiv enthält die Anwendung mit fertigem vendor/ ohne Entwicklungspakete, aber keine Tests, keine Skripte, kein var/ und kein .env; als Vorlage für die eigene Konfiguration liegt .env.example bei. Auf dem Hosting wird es entpackt, .env.local angelegt, migriert und der Cache gebaut. Node wird dort ebenfalls nicht gebraucht. Keine Installation ins öffentlich erreichbare Projektstammverzeichnis.
Ein Livegang, Hostzugriff, Restore-Test und Produktionsfreigabe sind nicht erfolgt. Bei bestehenden Installationen vor dem Update Datenbank und Medien sichern und php vendor/bin/doctrine-migrations migrate --no-interaction ausführen. Migration 20260912000200 ergänzt Benutzer, 20260912000300 die Medientabelle; beide verändern keine Seitendokumente. Migration 20260912001100 ergänzt drei Datumsspalten an cms_user (drei ADD COLUMN, kein Tabellenneubau). Benutzerverwaltung und Passwortänderung gibt es jetzt in der Oberfläche unter „Benutzer“ und „Mein Konto“; php bin/console cms:user:create bleibt der Weg, wenn kein Konto mehr funktioniert. Migration 20260912001300 ergänzt zwei Spalten an page für Kopf- und Fußbereich je Seite (zwei ADD COLUMN). Migration 20260914000100 legt die Tabelle des Aufgabenplans an (neue Tabelle, kein Eingriff in Bestehendes); ohne sie läuft der Plan nicht, meldet das aber, statt eine Seite zu zerstören. Migration 20260912001200 legt die Zählertabelle der Statistik an (neue Tabelle, kein Eingriff in Bestehendes); php bin/console cms:stats:prune und php bin/console cms:backup erledigt jetzt der eingebaute Aufgabenplan (Abschnitt „Wiederkehrende Aufgaben“); ohne Cronjob genügen Besuche und das geöffnete Backend – die Sicherung schreibt nach var/backups/, rotiert selbst und enthält Datenbank und Medien, aber weder .env noch vendor/. Zurückspielen: php bin/console cms:restore <name>. Der Code liegt auf https://git.kant-it.de/kantarci/KantSites.git; Fassungen sind Tags der Form v1.0.1, und die laufende Fassung steht in VERSION. Ein Update zieht php bin/console cms:update (mit --check, --dry-run, --to=v1.2.0) oder der Bildschirm „System“ in der Verwaltung; --maintenance=an|aus schaltet den Wartungszustand von Hand. Ändert ein Update die Abhängigkeiten (composer.lock) und ist Composer auf dem Server nicht aufrufbar, bricht der Updater ab und verweist auf das Releasearchiv; Updates ohne Abhängigkeitsänderung laufen auch ohne Composer durch. Ablauf und Grenzen stehen in docs/UPDATE.md. Der Abgleich mit der Pilotwebsite bleibt der nächste Schritt.