Appearance
Umstellung auf Spalten (type: layout)
Stand 2026-10-02. Das Feld blocks im Kirby ist seit dieser Änderung ein Layout-Feld: eine Seite ist eine Folge von Zeilen, jede Zeile trägt eine oder mehrere Spalten, jede Spalte ihre Bausteine.
Was sich nicht ändert
Der Feldschlüssel bleibt blocks. Das ist die wichtigste Entscheidung dieser Umstellung. Kirbys Blocks::factory() ruft extractFromLayouts() (cms/kirby/src/Cms/Blocks.php:62) und zieht die Bausteine aus den Spalten heraus. Deshalb arbeiten unverändert weiter:
page.heroBlock()undpage.heroImageRef()— und damit das Megamenü und die Motive der Startseite,- jede KQL-Abfrage, die
page.content.blocks.toBlocksstellt, - der Pimcore-Import, der den Bestand liest.
Nachgemessen: über alle 590 Seiten liefert toLayouts, flach gelesen, exakt dieselben Bausteine wie toBlocks — gleiche IDs, Typen, Inhalte, gleiche Reihenfolge, mit und ohne migrierten Inhalt.
Ein Umbenennen auf layout hätte dagegen jeden dieser Leser gleichzeitig treffen müssen; zwischen Inhalts-Migration und Frontend-Deploy hätten die Seiten leer ausgesehen.
Aus welchem Baum deployen — nicht aus einer Worktree
Der Deploy muss aus dem Haupt-Checkout laufen, nachdem der Branch dort angekommen ist. deploy.php setzt repo_root auf __DIR__ und rsynct den lokalen Arbeitsbaum mit --delete.
Gemessen mit rsync -n --delete aus der Worktree gegen /var/www/cms: 4.667 Löschungen, darunter der komplette kirby/-Kern (2.378) und zero-one/ (2.045 Dateien, 35 MB auf dem Server). Beide sind gitignored (cms/.gitignore), liegen nur im Haupt-Checkout und fehlen jeder Worktree — --delete würde sie vom Server entfernen.
Für staging/ sind es nur 119 Löschungen, alle unter .design-system/; die legt deploy.php direkt nach dem rsync wieder ab (sharedLibrary). Das ist der normale Ablauf, kein Problem.
Mit admin/vendor/bin/dep -f <pfad>/deploy.php liesse sich ein anderer Baum wählen — für cms ist das aber genau der falsche Weg. Also: Branch in den Haupt-Checkout bringen, dort rsync -n gegenprüfen, dann deployen.
Reihenfolge auf dem Server — nicht tauschen
Der Inhalt liegt außerhalb des Deploys (deploy.php schließt content, media und site/accounts aus). Lokal migrierte Inhalte erreichen die Produktion also nie — die Migration läuft auf dem Server.
1. admin/vendor/bin/dep deploy:cms production
2. ssh matev-deploy "grep -h 'type:' /var/www/cms/site/blueprints/fields/matev-blocks*.yml | head -3"
→ muss dreimal `type: layout` zeigen
3. ssh matev-deploy "cd /var/www/cms && tar czf ~/content-vor-layout-$(date +%F).tar.gz content && tar tzf ~/content-vor-layout-$(date +%F).tar.gz | wc -l"
4. ssh matev-deploy "cd /var/www/cms && php bin/migrate-to-layout.php" # Probelauf
5. ssh matev-deploy "cd /var/www/cms && php bin/migrate-to-layout.php --write"
6. admin/vendor/bin/dep deploy:staging productionWarum Schritt 1 vor Schritt 5 steht: BlocksField::fill() schickt seinen Wert durch dieselbe Blocks::factory() (cms/kirby/src/Form/Field/BlocksField.php:113). Steht das Feld noch als type: blocks, flacht es die Zeilen beim nächsten Speichern im Panel still wieder ab — ohne Fehlermeldung, und das Raster ist weg. Erst der Code, dann der Inhalt.
Schritt 6 darf auch vorher laufen: die Abfrage toLayouts funktioniert gemessen auch auf unmigriertem, flachem Inhalt (Kirby wickelt ihn selbst in eine 1/1-Zeile). Nur die Reihenfolge 1 → 5 ist zwingend.
Nicht über ddev exec migrieren. Das einzige cms-ddev-Projekt wurzelt im Haupt-Checkout und würde dessen content/ schreiben. Das Skript nimmt darum --content=/pfad.
Zahlen (gemessen, nicht geschätzt)
| lokal (Worktree) | Produktion | |
|---|---|---|
| Seiten-Sprach-Kombinationen mit Bausteinen | 877 Dateien | 880 Dateien |
| … über Kirby gezählt (mit Sprach-Rückfall) | 889 | — |
| Bausteine | 2.884 Dateien / 2.976 über Kirby | 2.975 |
Entwürfe unter _changes/ | 0 | 2 |
| schon in Zeilenform | 0 | 0 |
Alt-Schlüssel Layout: | 0 | 0 |
Die Dateizählung (877/2.884) und die Kirby-Zählung (889/2.976) messen Verschiedenes: Kirby zählt Übersetzungen mit, die das Feld selbst nicht führen und es von der Standardsprache erben.
14 Seiten-Blueprints binden das Feld ein, nicht 12 — hub und preislistengenerator kamen später hinzu. Über drei Felddateien:
fields/matev-blocks.yml→ dealer, default, error, home, hub, landingpage, legal, preislistengenerator, searchfields/matev-blocks-product.yml→ category, product, produktefields/matev-blocks-portal.yml→ portal, portal-page
Das Raster
Die layouts-Liste der drei Felddateien ist die von fields/matev-page-builder.yml, die bis 09/2026 schon einmal in diesem Projekt stand — echte Kirby-Brüche, keine UIkit-Klassen.
Im Frontend rechnet Kirby die Breite in Zwölftel (span: 1/2 → 6). Das Raster in staging/assets/css/main.css hat genau zwölf Spalten und setzt den Wert über --column-span; unter 48rem stehen die Spalten von selbst untereinander. Deshalb braucht es keine Klassen je Breite — zusammengesetzte Tailwind-Namen würde der Übersetzer ohnehin nicht finden.
Zero Ones tabs/layout-grid|section|advanced.yml taugen als Vorbild, ihre Werte sind aber UIkit-Klassen, die Tailwind ignoriert. Sie sind nicht übernommen.
Warum der Bestand aussieht wie vorher
Der migrierte Bestand steht in genau einer Zeile voller Breite (gemessen: 889 Zeilen, 889 Spalten, alle 1/1). LayoutRenderer gibt solche Zeilen unverändert an BlockList weiter — dieselbe Ausgabe wie vorher. Das Raster kommt erst zum Zug, wenn die Redaktion eine Zeile wirklich teilt.
Wer schreibt, schreibt Zeilen
Ein Layout-Feld nimmt eine flache Bausteinliste an — Kirby wickelt sie in eine 1/1-Zeile (Layouts::factory()). Dabei vergibt es aber bei jedem Lesen eine neue ID, die Seite wäre nie stabil. Darum schreiben alle Erzeuger Zeilen, über eine Stelle: LayoutShape::wrap() in cms/site/plugins/matev-blocks/src/LayoutShape.php.
Umgestellt: bin/content-import/KirbyPageContentImporter.php (Pimcore-Import), bin/build-block-showcase.php, bin/build-block-reference.php.
bin/migrate-to-blocks.php ist erledigt und überspringt jetzt alles, was schon Zeilen führt — sonst hätte ein weiterer Lauf die Umstellung zurückgedreht, weil es den Bestand über den abflachenden Weg liest.
bin/migrate-hero-slider.php ist noch offen: 6 Seiten auf dem Server führen weiter einen matev-hero-slider. Das Skript findet ihn jetzt in beiden Formen — flach und in Spalten —, die Reihenfolge gegenüber der Migration ist also gleichgültig. Vorher hätte es sie nach der Migration stumm übersehen.
Fallen im Inhaltsformat
Drei Stück, alle im Skript berücksichtigt:
/^Blocks: (.*)$/ms— mitsfrisst der Punkt bis zum Dateiende, also bis in die Folgefelder. Darum.*?und ein Blick nach vorn auf\n----.- Ohne
sübersieht dieselbe Regex 10 Seiten, die ihren Wert in Kirbys mehrzeiliger Form führen (Blocks:, Leerzeile, dann JSON). Sie blieben stumm stehen. (Diese Falle stand in keiner Vorbereitung.) glob('content/**/*.txt')trifft in PHP nur zwei Ebenen —RecursiveDirectoryIteratornehmen, sonst bleiben Entwürfe unter_changes/stehen.
Ein grep '"columns"' taugt nicht als Prüfung auf Zeilenform: Bausteine führen selbst ein Feld columns (die Spaltenwahl der Produktbereiche), auf dem Server in 52 Fällen. Geprüft wird columns in der ersten Zeile (LayoutShape::isLayout()).
Was geprüft ist
migrate-to-layout.phpist wiederholbar: ein zweiter Lauf fasst nichts an (877 → „schon Zeilen").- Datei für Datei wird vor dem Schreiben geprüft, dass aus den Zeilen genau dieselbe Bausteinliste herausfällt; sonst bleibt die Datei stehen.
- Über Kirby verglichen, vorher gegen nachher: 889 Kombinationen, 2.976 Bausteine, 0 Unterschiede.
- Nur Blocks-Werte geändert: 877 Dateien, 0 veränderte Zeilen außerhalb des Feldes. Trenner und Leerzeilen bleiben, wie sie waren.
- Alle 590 Seiten in drei Sprachen: 1.770 Panel-Formulare gebaut, 0 Fehler, alle
blocks-Felder vom Typlayout. - KQL mit dem echten
BLOCK_SELECTauskirbyQueries.ts:toLayoutsflach gelesen ist identisch mittoBlocks— auf migriertem und auf flachem Inhalt.
JSON-LD: Grundstand vor dem Deploy
Von innen auf dem Server gemessen (öffentlich antwortet nichts: curl auf cms.matev.eu und staging.matev.eu gibt 000, der Container unter http://172.18.0.28:3000 gibt 200):
/trägt einen JSON-LD-Block:Organization,WebSite,WebPage. Genau das muss nachdeploy:stagingwieder dastehen.- Kein
ItemList— und das ist richtig so: die Entität entsteht nur aus von Hand gepflegten Karten, und dermatev-product-areas-Baustein der Startseite führt auf dem Servercards = 0. Automatische Karten aus den Produktwelten zähltuseSeobewusst nicht. - Kein
FAQPage— keine lebende Seite benutztarea-faq. Die acht Dateien mit diesem Wort liegen unter_demo/und führen ein altes FeldBuilder:, das Kirby nicht liest.
Damit lässt sich für ItemList und FAQPage kein Vorher/Nachher am echten HTML ziehen — es gibt sie heute nicht. Geprüft ist stattdessen der Weg, auf dem sie entstehen: pageBlocks() liefert über alle 365 Seiten Baustein für Baustein dasselbe wie toBlocks, und in einer geteilten Zeile findet die Suche einen area-faq in der zweiten Spalte ebenso wie den matev-hero in der Zeile darüber.
Panel-Vorschauen: unberührt
Die Vorschauen der Bausteine hängen nicht am Feld, sondern am Bausteintyp (k-block-type-matev-… in matev-block-previews/index.js, abgeleitet von k-block-type-default). Das Layout-Feld rendert dieselben Komponenten in seinen Spalten. Auch matev-editor-preview und content-media-bridge haben keinen Bezug auf die Baustein-Feldkomponente — die geteilte Editor-Ansicht bleibt wie sie ist.
Offen, für die Kundenentscheidung
- Die Redaktion kann ab jetzt einen vollflächigen Baustein (
matev-hero,matev-product-areas, die Bänder) in eine geteilte Zeile legen. Er rendert dann eingebettet in seiner Spalte, nicht über die volle Breite. Am Bestand ändert sich nichts — alle 889 Zeilen sind1/1—, aber die Möglichkeit ist neu. Wer das nicht will, schränkt die Bausteinliste je Spaltenbreite ein; das wäre eine eigene Entscheidung. bin/migrate-hero-slider.phpist auf der Produktion noch nicht gelaufen: 6 Seiten führen dort weiter einenmatev-hero-slider. Das Skript findet ihn jetzt in beiden Formen, die Reihenfolge ist also frei.heroImageRef()liefert lokal für 16 von 19 Hauptpunkten nichts. Das ist kein Ergebnis dieser Umstellung: vorher wie nachher dieselben 3 mit Motiv (gemessen gegen den Inhalt ausHEAD~1). Dem lokalen Abzug fehlen die PIM-Verweise, die auf dem Server stehen.