feat(cms): Startseiten-Datenmodell und fünf neue Blocktypen #39

Merged
Phil merged 4 commits from feat/craft-home-content-model into main 2026-08-13 06:20:14 +00:00
Owner

Was

  • Neues Matrix-Feld homeContent, exklusiv am Entry-Type startseite.
  • websiteContent bleibt an unterseite, reduziert auf informationPage (per Live-Check der einzige Blocktyp, den Impressum/Datenschutz nutzen).
  • Neue Geschwisterdatei templates/_components/core/content-home.twig: .eagerly() gegen N+1, jeder Block über only isoliert statt kompletten Template-Kontext zu leaken. content.twig bleibt für die Unterseiten unverändert.
  • Fünf neue Blocktypen (methodSection, proofSection, videoTestimonialSection, killerQuestionSection, offerSection) plus verschachtelter Entry-Type videoTestimonial.
  • Vier neue Felder am Entry-Type columns: columnPrice, columnPriceNote, columnBtn, columnHighlight.
  • Alles über eine Craft-Content-Migration (migrations/m260813_055620_add_home_content_model.php), damit php craft up es reproduziert statt nur lokal im CP geklickt zu sein.

Kein MCP-Write verfügbar in dieser Session (nur craft-cms-prod war als Tool sichtbar) — alle Schema-Änderungen liefen über docker exec philflow_php_local php craft ... und die Migration, nicht über den lokalen craft-cms-MCP.

Zwei Modellierungsentscheidungen

1. Der Schwellwert im ROI-Rechner (methodSection) speichert den Paketpreis kein zweites Mal. Die Sektion braucht eine Zahl, ab der sie abrät — die soll dem Preis des kleinsten Pakets entsprechen. Statt einem eigenen Zahlenfeld auf methodSection wird der Preis ausschließlich im Feld columnPrice der columns-Einträge gepflegt (Pflichtfeld laut Auftrag, Plain Text für "ab 3.900 EUR"). content-home.twig scannt beim Rendern einmal alle columnsSection-Blöcke, parst columnPrice auf die reine Zahl und reicht das Minimum als packageStartPrice-Prop an methodSection durch. Ändert ein Editor den Preis in der Paketliste, zieht der Rechner automatisch nach, ohne dass zwei Stellen synchron gehalten werden müssen.

2. Video/Poster-Formate werden aus einem Feld pro Medientyp abgeleitet, nicht vervierfacht. web/video/testimonials/ liegt je Clip als MP4 + WebM und je Poster als JPG + WebP vor, jeweils unter demselben Basisnamen. Das Feldmodell hat nur videoFile (MP4-Upload) und posterImage (JPG-Upload) — das WebM- und WebP-Pendant werden in pl_videoTestimonialSection.twig durch Dateiendungstausch auf der Asset-URL abgeleitet. Vier Felder mit vier Uploads pro Testimonial hätten das gleiche Risiko erzeugt wie der Preis: zwei Dateien, die bei einem Reupload auseinanderlaufen.

Verifikation

  • php craft up läuft grün und idempotent (zweimal hintereinander geprüft, project-config/diff zeigt "No pending changes").
  • /, /blog, /impressum, /datenschutz liefern alle 200.
  • Feld-Layout-Auflösung aller fünf neuen Entry-Types plus videoTestimonial und columns programmatisch verifiziert (getFieldLayout()->getCustomFields() ohne Fehler).
  • Reserved-Handle-Check gegen alle 83 Handles — dabei einen echten Treffer gefunden und korrigiert: offerSection hatte zunächst den Feld-Handle title (kollidiert mit dem nativen Element-Attribut), umbenannt zu heading. DB wurde aus dem vorab gezogenen Backup restauriert und die korrigierte Migration sauber neu appliziert.
  • End-to-End-Rendertest: alle fünf neuen Blocktypen testweise mit Platzhalterinhalt befüllt, Rendering aller fünf Sektionen inklusive Übersetzungsstrings (|t('site')) und ROI-Datenattribute geprüft, danach homeContent wieder geleert (keine Datenmigration, siehe unten).
  • git status sauber, keine root-eigenen Dateien im Commit.

Keine Datenmigration: homeContent ist nach diesem PR leer, die Startseite wird ohnehin neu getextet. Die alte websiteContent-Fassung von Entry 3 bleibt als Craft-Revision und über ein lokales DB-Backup (backups/predeploy_homecontent_*.sql.gz, nicht committed) erhalten.

Closes #38

🤖 Generated with Claude Code

## Was - Neues Matrix-Feld `homeContent`, exklusiv am Entry-Type `startseite`. - `websiteContent` bleibt an `unterseite`, reduziert auf `informationPage` (per Live-Check der einzige Blocktyp, den Impressum/Datenschutz nutzen). - Neue Geschwisterdatei `templates/_components/core/content-home.twig`: `.eagerly()` gegen N+1, jeder Block über `only` isoliert statt kompletten Template-Kontext zu leaken. `content.twig` bleibt für die Unterseiten unverändert. - Fünf neue Blocktypen (`methodSection`, `proofSection`, `videoTestimonialSection`, `killerQuestionSection`, `offerSection`) plus verschachtelter Entry-Type `videoTestimonial`. - Vier neue Felder am Entry-Type `columns`: `columnPrice`, `columnPriceNote`, `columnBtn`, `columnHighlight`. - Alles über eine Craft-Content-Migration (`migrations/m260813_055620_add_home_content_model.php`), damit `php craft up` es reproduziert statt nur lokal im CP geklickt zu sein. Kein MCP-Write verfügbar in dieser Session (nur `craft-cms-prod` war als Tool sichtbar) — alle Schema-Änderungen liefen über `docker exec philflow_php_local php craft ...` und die Migration, nicht über den lokalen `craft-cms`-MCP. ## Zwei Modellierungsentscheidungen **1. Der Schwellwert im ROI-Rechner (methodSection) speichert den Paketpreis kein zweites Mal.** Die Sektion braucht eine Zahl, ab der sie abrät — die soll dem Preis des kleinsten Pakets entsprechen. Statt einem eigenen Zahlenfeld auf `methodSection` wird der Preis ausschließlich im Feld `columnPrice` der `columns`-Einträge gepflegt (Pflichtfeld laut Auftrag, Plain Text für "ab 3.900 EUR"). `content-home.twig` scannt beim Rendern einmal alle `columnsSection`-Blöcke, parst `columnPrice` auf die reine Zahl und reicht das Minimum als `packageStartPrice`-Prop an `methodSection` durch. Ändert ein Editor den Preis in der Paketliste, zieht der Rechner automatisch nach, ohne dass zwei Stellen synchron gehalten werden müssen. **2. Video/Poster-Formate werden aus einem Feld pro Medientyp abgeleitet, nicht vervierfacht.** `web/video/testimonials/` liegt je Clip als MP4 + WebM und je Poster als JPG + WebP vor, jeweils unter demselben Basisnamen. Das Feldmodell hat nur `videoFile` (MP4-Upload) und `posterImage` (JPG-Upload) — das WebM- und WebP-Pendant werden in `pl_videoTestimonialSection.twig` durch Dateiendungstausch auf der Asset-URL abgeleitet. Vier Felder mit vier Uploads pro Testimonial hätten das gleiche Risiko erzeugt wie der Preis: zwei Dateien, die bei einem Reupload auseinanderlaufen. ## Verifikation - `php craft up` läuft grün und idempotent (zweimal hintereinander geprüft, `project-config/diff` zeigt "No pending changes"). - `/`, `/blog`, `/impressum`, `/datenschutz` liefern alle `200`. - Feld-Layout-Auflösung aller fünf neuen Entry-Types plus `videoTestimonial` und `columns` programmatisch verifiziert (`getFieldLayout()->getCustomFields()` ohne Fehler). - Reserved-Handle-Check gegen alle 83 Handles — dabei einen echten Treffer gefunden und korrigiert: `offerSection` hatte zunächst den Feld-Handle `title` (kollidiert mit dem nativen Element-Attribut), umbenannt zu `heading`. DB wurde aus dem vorab gezogenen Backup restauriert und die korrigierte Migration sauber neu appliziert. - End-to-End-Rendertest: alle fünf neuen Blocktypen testweise mit Platzhalterinhalt befüllt, Rendering aller fünf Sektionen inklusive Übersetzungsstrings (`|t('site')`) und ROI-Datenattribute geprüft, danach `homeContent` wieder geleert (keine Datenmigration, siehe unten). - `git status` sauber, keine root-eigenen Dateien im Commit. Keine Datenmigration: `homeContent` ist nach diesem PR leer, die Startseite wird ohnehin neu getextet. Die alte `websiteContent`-Fassung von Entry 3 bleibt als Craft-Revision und über ein lokales DB-Backup (`backups/predeploy_homecontent_*.sql.gz`, nicht committed) erhalten. Closes #38 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Neues Matrix-Feld homeContent, exklusiv am Entry-Type startseite.
websiteContent bleibt an unterseite, reduziert auf informationPage
(einzig genutzter Blocktyp auf Impressum/Datenschutz).

Fuenf neue Blocktypen (methodSection, proofSection,
videoTestimonialSection, killerQuestionSection, offerSection) plus
verschachtelter Entry-Type videoTestimonial. Vier neue Felder am
Entry-Type columns (columnPrice, columnPriceNote, columnBtn,
columnHighlight) fuers Preis-Paket-Modell.

Alles ueber eine Craft-Content-Migration, damit es reproduzierbar
mit php craft up laeuft statt nur lokal im CP geklickt zu sein.

Neuer Loop content-home.twig fuer die Startseite: .eagerly() gegen
N+1, jeder Block ueber only isoliert statt den kompletten
Template-Kontext zu leaken. content.twig bleibt fuer die
Unterseiten unveraendert.

Keine Datenmigration, die Sektionen werden neu getextet. Die alte
Fassung bleibt als Craft-Revision und ueber ein DB-Backup erhalten.

Closes #38

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Design-Richtung fuer die Methode-Sektion steht fest: der ROI-Rechner
ist ein Satz mit drei Luecken im Fliesstext, kein Formular neben dem
Text (Referenz: templates/preview/methode-b.twig). intro nutzt dafuer
die Platzhalter {{runs}}, {{minutes}}, {{rate}}, {{result}}, die
pl_methodSection.twig gegen die echten Eingabefelder und das
Live-Ergebnis tauscht.

columns bekommt ein zweites Link-Feld columnBookingLink fuer den
raven.ceo-Terminlink, getrennt vom primaeren columnBtn, das in die
mehrstufige Anfrage-Strecke fuehrt. Je Paket eigener Query-Parameter
fuer die Klick-Messung.

Refs #38

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
packageStartPrice war null, wenn keine columnsSection existiert, alle
columnPrice-Werte leer sind oder nicht als Zahl lesbar (z. B. 'auf
Anfrage', bewusst moeglich weil das Feld Plain Text ist). Die Sektion
haette dann entweder nie oder immer abgeraten. Fallback auf 3900,
den von Phil entschiedenen Startpreis von Paket 1 aus PRODUCT.md
und den Referenzwert aus templates/preview/methode-b.twig. Sobald
ein echter, parsbarer Preis im CMS steht, ueberschreibt er den
Fallback automatisch. Verifiziert: kein columnsSection-Block, leerer
Preis, unparsebarer Preis (auf Anfrage) und echter Preis.

Im Video-Testimonial-Block zeigte das poster-Attribut bereits auf die
echte Datei, das interne <img>-Fallback aber auf den abgeleiteten
WebP-Pfad. Beides sind Einzel-URLs ohne Fallback-Kette (anders als
die zwei <source>-Elemente), jetzt zeigen beide auf die tatsaechlich
hochgeladene Datei. Kommentar-Header verweist auf
scripts/encode-testimonials.sh und haelt fest, dass die Ableitung
nur gilt, solange alle vier Dateien aus demselben Kodierlauf stammen.

Refs #38

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Wenn ein Redakteur einen der vier Platzhalter ({{runs}}, {{minutes}},
{{rate}}, {{result}}) in methodSection.intro loescht, verdoppelt oder
verschreibt, ergab der Split nicht mehr exakt fuenf Fragmente und die
Eingabefelder haetten an der falschen Stelle oder gar nicht gestanden.

pl_methodSection.twig prueft jetzt vorab, ob alle vier Platzhalter
vorhanden sind und der Split exakt fuenf Teile ergibt. Ist das nicht
der Fall, faellt intro auf reinen Fliesstext ohne Eingabefelder zurueck
statt halb zu rendern — lieber ein statischer, korrekt lesbarer Satz
als ein kaputter Rechner. Eine falsche Reihenfolge bei sonst korrekter
Anzahl kann das Template nicht erkennen, das faengt nur eine
sorgfaeltige Vorschau-Pruefung ab.

Die Feld-Instruktion von intro im Control Panel beschreibt das
Verhalten jetzt explizit, das ist der einzige Ort, an dem ein
Redakteur die Konvention liest.

Verifiziert: gesunder Fall (alle vier Platzhalter, drei Inputs
gerendert) und Fehlerfall (zwei Platzhalter fehlen, faellt auf
statischen Text zurueck, keine 500).

Refs #38

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Phil merged commit 24e6a80d97 into main 2026-08-13 06:20:14 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Phil/philflow.io!39
No description provided.