Skip to content

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() und page.heroImageRef() — und damit das Megamenü und die Motive der Startseite,
  • jede KQL-Abfrage, die page.content.blocks.toBlocks stellt,
  • 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 production

Warum 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 Bausteinen877 Dateien880 Dateien
… über Kirby gezählt (mit Sprach-Rückfall)889—
Bausteine2.884 Dateien / 2.976 über Kirby2.975
Entwürfe unter _changes/02
schon in Zeilenform00
Alt-Schlüssel Layout:00

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, search
  • fields/matev-blocks-product.yml → category, product, produkte
  • fields/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:

  1. /^Blocks: (.*)$/ms — mit s frisst der Punkt bis zum Dateiende, also bis in die Folgefelder. Darum .*? und ein Blick nach vorn auf \n----.
  2. 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.)
  3. glob('content/**/*.txt') trifft in PHP nur zwei Ebenen — RecursiveDirectoryIterator nehmen, 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.php ist 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 Typ layout.
  • KQL mit dem echten BLOCK_SELECT aus kirbyQueries.ts: toLayouts flach gelesen ist identisch mit toBlocks — 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 nach deploy:staging wieder dastehen.
  • Kein ItemList — und das ist richtig so: die Entität entsteht nur aus von Hand gepflegten Karten, und der matev-product-areas-Baustein der Startseite führt auf dem Server cards = 0. Automatische Karten aus den Produktwelten zählt useSeo bewusst nicht.
  • Kein FAQPage — keine lebende Seite benutzt area-faq. Die acht Dateien mit diesem Wort liegen unter _demo/ und führen ein altes Feld Builder:, 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 sind 1/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.php ist auf der Produktion noch nicht gelaufen: 6 Seiten führen dort weiter einen matev-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 aus HEAD~1). Dem lokalen Abzug fehlen die PIM-Verweise, die auf dem Server stehen.