b2in/dev/2026-08-28/entwicklungsplan.md
Kevin Adametz 093e0ece51 Abschluss-CTA: Ausnahme vom Glass-Flow-Sektionshintergrund
Nach dem Build stand der Abschluss hellgrau mit weisser Schrift da.
`.variante-glass-flow section` faerbt jede Sektion in diesem Wrapper hell ein
und ist mit (0,1,1) spezifischer als `.cta-band` mit (0,1,0). Fuer
`.bg-secondary` und `.bg-accent` gibt es dort laengst Ausnahmen – die neue
Klasse brauchte dieselbe. /marken/{slug} haengt in genau diesem Wrapper.

Der Verlauf steht jetzt in --cta-band-gradient und wird von beiden Regeln
benutzt, damit er nicht doppelt gepflegt werden muss. Ein Test haelt die
Ausnahme fest.

Nebenbei dokumentiert: `npm run build:web` baut nur die vier Frontend-Themes,
nicht das Portal. Ohne `npm run build:portal` liefert das Admin-Portal 500 –
in Deployment-Checkliste und Vorschau-Runbook ergaenzt, samt Pruefschleife
ueber alle fuenf Manifeste.

Tests: 863 gruen (3194 Assertions), Pint sauber, alle Themes gebaut.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 10:36:02 +00:00

73 KiB
Raw Blame History

Entwicklungsplan: B2in Interior – Neuausrichtung Website & CMS

Erstellt: 28.08.2026 · zuletzt aktualisiert 28.08.2026 Basis: B2in_Interior-Konzept.md, umsetzungliste.md, umsetzung-inhalte.md Vorgänger: dev/2026-02-27/b2in-website-umsetzungsplan.md Rahmen: 37–39 Std. freigegeben (18 + 8 + 4 + 3 + 4–6) Status: Revision 13 – AP0–AP6 umgesetzt, Seite abgestimmt, Markentexte Rotpunkt und Febrü eingearbeitet. Offen: Bildmaterial, Rechtstexte und die Herstellerfreigaben. Umgebung: Dev Container, php artisan … (nicht vendor/bin/sail …)

Verbindliche Schreibweisen: Interior (nicht „Interieur"), Febrü, Rotpunkt Küchen, CABINET. Das Konzeptpapier schreibt durchgehend „Interieur" – die Umsetzungsliste überschreibt das. Gilt für Code, Texte, Commit-Messages und Datenbank-Inhalte.


0. Stand & nächste Schritte

Stand 03.09.2026: T1 und B1–B13 abgearbeitet, alle Arbeitspakete AP0–AP6 umgesetzt, die Bestandsinhalte auf den Interior-Auftritt umgestellt, die Seite abgestimmt und alle vier Punkte der Abstimmungsrunde vom 03.09. umgesetzt (Nachtrag 5). Alle drei Markenseiten haben jetzt einen ausführlichen Text. php artisan test --compact: 863 grün (3194 Assertions), Pint sauber, npm run build:b2in gebaut. Die Seite ist testbereit – die ausstehenden Zulieferungen trägt Marcel selbst im CMS nach.

Paket Stand
AP0 Vorbereitung ✅
AP1 Umstrukturierung & Umtextung ✅
AP2 Kernmarken-Modul + /marken + Galerie ✅
AP3 Terminanfrage-Widget + WhatsApp-Link ✅ (Nummer fehlt noch)
AP6 Sponsoring ✅
Bestandsinhalte FAQ / About / Kontakt / Magazin ✅
Markenseiten Rotpunkt und Febrü (Zulieferung 31.08.) ✅
Abstimmungsrunde 03.09.: korrigierte Texte, CABINET-Seite, Galerie-Hinweise, Abschluss-CTA ✅
AP4 Display-Spiegelung ✅
AP5 Rubrik-Seite „Referenzen" ✅

Nächste Schritte

Vor dem Versand des Testlinks — nichts. Alle Seiten laden, die Suite ist grün, die Assets sind gebaut. Was fehlt, ist Material, kein Code.

Was Marcel selbst nachträgt (alles im CMS, ohne Codeänderung):

Was Wo im Admin
Logos und Stimmungsbilder der drei Marken Kernmarken → Marke bearbeiten
Echte Projektfotos statt der Demobilder (siehe Warnung unten) Kernmarken → Bildergalerie
Herstellerseiten-URLs Kernmarken → Herstellerseite
Motiv für den Kopf von /marken Inhalte → marken → hero
WhatsApp-Nummer Inhalte → global → contact → whatsapp
Welches Display auf der Website läuft Inhalte → referenzen → display_feed → display_id

Beim Deployment mit erledigen (Abschnitt 8):

  1. Rechtstexte – AGB und Datenschutzerklärung beschreiben weiter das alte Geschäft („Connecting Design and Property", Dubai/Lissabon, Supply-Chain-Management, resources/lang/de/b2in_legal.php Zeilen 117 und 221). Marcel lässt die Formulierung prüfen, danach eintragen. Der WhatsApp-Absatz kommt im selben Zug dazu.
  2. OG-Bild – das Layout verweist auf img/assets/b2in-og-default.jpg, die Datei existiert nicht. Bis dahin zeigt jede geteilte Seite eine Vorschau ohne Bild.
  3. Seeder-Passwort rotieren (B8) – steht weiter in der Git-Historie.
  4. Die zwei Magazin-Beiträge und die Sponsoren-Kurztexte gegenlesen lassen.
  5. CABINET-Seitentext freigeben lassen – für CABINET lag keine Zulieferung vor, der Text ist von uns nach dem Muster der beiden abgestimmten Seiten verfasst (Nachtrag 5).
  6. Demobilder der Rubrik „Realisiertes Projekt" ersetzen. Seit dem Entfallen der „BEISPIEL"-Kennzeichnung geben sie sich als echte Referenzen aus.

Danach offen, aber nicht Teil dieses Auftrags: YouTube-Einbindung mit Consent-Gate auf der Referenzen-Seite (Abschnitt 4). Sinnvoll, sobald es Videos gibt – vorher wäre es ein Consent-Gate ohne Inhalt.


1. Ist-Architektur (relevanter Ausschnitt)

1.1 Content-Pipeline

resources/lang/de/b2in.php  (themes.b2in.<section>)      ← Quelltexte / Fallback
        ├─ config/cms_section_map.php    section → [group, key]
        ├─ database/seeders/CmsContentSeeder.php
        ▼
flux_cms_contents (group, key, type, value:JSON, translatable de/en)
        ├─ FluxCms\Core\Services\CmsContentService  (Cache: flux_cms.content.__all__)
        ▼
cms_theme_section('hero')  → CMS gewinnt, sonst trans('b2in.themes.b2in.hero')

Redaktion unter admin/flux-cms/content → content-index.blade.php (Volt, 782 Z.): Group/Key-Editor mit JSON-Repeater-Modal, Medien-Picker, Flux-Editor für HTML. Kann Werte ändern, aber keine Sektion neu anlegen.

1.2 Seiten & Sektionen

  • Startseite: routes/web.php → resources/views/web/home.blade.php
  • Sektionen = Livewire-Komponenten unter app/Livewire/Web/Components/Sections/, Muster mount() { $this->content = cms_theme_section('<key>'); }
  • Ist-Reihenfolge: header → hero → founder-bar → content-section(synergie) → vision-section → ecosystem-core → cta-section → footer
  • Navigation und Ankündigungsleiste liegen bereits im CMS (global.header.navigation, global.announcement_bar) → Umbau ist Redaktion, kein Code.
  • SEO: @section('title') / @section('meta_description') je View; OG-Tags leiten sich in web-master.blade.php automatisch daraus ab → OG kostet keinen Extra-Aufwand (aber siehe B6).
  • Immobilien rendern statisch aus resources/lang/de/immobilien-azizi.json – wie gefordert.

1.3 Display-CMS

Display (Gerät) → DisplayPlaylist (draft/published) → DisplayVersion („Modul", Enum DisplayVersionType: video-display | b2in | offers) → DisplayVersionItem. DisplayPlaylistConfigBuilder erzeugt das Player-JSON. Player: public/_cabinet/display/index.html, festes 1080×1920-Raster (9:16), per Scale eingepasst. API vorhanden: /api/display/{display}/config, /api/display/preview/{token}, /api/display/module/{module}/preview – registriert für Portal-Domain und als Fallback für alle Domains.

1.4 Sonstiges

  • Kontaktformular: packages/acme/contact-form – unterstützt bereits customFields + Presets, Honeypot, Rate-Limit und SpamDetector. Das Terminanfrage-Widget kann darauf aufsetzen.
  • Tests: Pest, SQLite in-memory, php artisan test --compact.

2. Gefundene Bugs & Optimierungen

Beim Abgleich der Umsetzungsliste mit dem Code aufgefallen.

Stand 28.08.2026: alle Befunde behoben, Suite grün (699 Tests, 2715 Assertions).

# Befund Status
T1 Info-Tablet: Leeren per X wird nicht gespeichert ✅ behoben
B1 CMS-Seeder überschreibt redaktionelle Inhalte ✅ behoben
B2 Magazin-CMS nicht ans Frontend angeschlossen ✅ behoben (Variante A: Slug-Routen + 301)
B3 Ankündigungsleiste bleibt nach Umtexten unsichtbar ✅ behoben
B4 Footer hartkodiert, nicht pflegbar ✅ behoben
B5 Englische Footer-Labels auf deutscher Seite ✅ behoben
B6 19 Seiten erben alte Dubai-Beschreibung ✅ behoben (Fallback + canonical)
B7 Content-Seeder nicht registriert ✅ behoben
B8 Klartext-Passwort im Seeder ✅ behoben
B9 Medien-Picker greift bei leeren Logo-Feldern nicht ✅ behoben
B10 Alt-Begriffe im Seiteninhalt ✅ behoben in AP1
B12 /service und /portfolio lieferten 500 ✅ behoben (301)
B11 /magazin/{unbekannt} liefert 200 statt 404 ✅ behoben

T1 · Info-Tablet: Leeren der Öffnungszeiten wird nicht gespeichert ✅

Gemeldet am 28.08.2026: „Wenn ich den Inhalt weg-X, kommt nicht Geschlossen. Der Wert kommt wieder, wenn ich in ein anderes Feld klicke. Setze ich Öffnung und Schluss auf X und speichere, ist Öffnung wieder der alte Wert."

Drei Ursachen, alle in cabinet-info-tablet.blade.php bzw. CabinetInfoTablet.php:

  1. Zwei Submit-Handler auf demselben Formular. wire:submit.prevent="save" und x-on:submit.prevent="… $wire.save(hours)" waren gleichzeitig gebunden – save() lief pro Klick zweimal, einmal mit den DOM-Werten und einmal ohne. Welcher Aufruf zuletzt schrieb, war nicht deterministisch. Das erklärt „speichert nur den letzten".
  2. wire:model.live auf den Zeit-Pickern. Jede Änderung löste einen Roundtrip aus; beim erneuten Rendern hat Livewire ein zuvor geleertes Feld mit dem unveränderten Serverwert überschrieben. Das erklärt „der Wert kommt wieder, wenn ich in ein anderes Feld klicke".
  3. Geöffnet/Geschlossen wurde serverseitig gerechnet und konnte deshalb erst nach einem Roundtrip umspringen. Das erklärt „es kommt nicht Geschlossen".

Behoben: nur noch ein Submit-Pfad (das DOM ist die Quelle der Wahrheit), .live entfernt, die Geöffnet/Geschlossen-Anzeige rechnet clientseitig per Alpine und reagiert damit sofort auf das X.

Zusätzlich: save(array $hours) hat über property_exists() jede öffentliche Property aus dem Client-Payload beschreibbar gemacht – auch storeStatus und contactEmail. Jetzt greift eine Allowlist auf die 14 Zeitfelder.

Tests: 5 neue Fälle in tests/Feature/CabinetInfoTabletTest.php (15 grün) – u. a. mehrere Tage in einem Submit leeren, nur die Öffnung leeren, Fallback ohne DOM-Payload, Allowlist.

Noch am Gerät zu prüfen: die drei Ursachen liegen im Browser-Verhalten; die Tests sichern den Serverpfad ab. Bitte im Portal einmal gegenprüfen: mehrere Tage leeren → speichern → neu laden.

B1 · Der CMS-Seeder überschreibt alles, was Marcel pflegt ✅

database/seeders/CmsContentSeeder.php:76-90 – bestehende Einträge werden bedingungslos per $content->update() mit den Werten aus der Lang-Datei überschrieben. Geschützt sind nur die zwei Sektionen in $skipSections.

Wirkung: Jeder erneute Seeder-Lauf – auch der geplante nach der Umtextung – setzt sämtliche redaktionellen Änderungen zurück. Die Zusage „von Marcel selbst pflegbar" hält so nicht.

Behoben: Default ist jetzt „nur anlegen, wenn nicht vorhanden". Überschreiben nur noch über $seeder->overwriteExisting = true. Der Lauf meldet, wie viele Einträge unangetastet blieben, und leert den CMS-Cache, wenn er etwas geändert hat. Tests: 3 neue Fälle in tests/Feature/Cms/CmsContentSeederTest.php – gepflegter Inhalt bleibt, fehlende Sektionen werden trotzdem angelegt, bewusstes Überschreiben funktioniert.

B2 · Das Magazin-CMS ist nicht mit dem Frontend verbunden ✅

  • Admin admin/flux-cms/articles schreibt nach cms_articles (Model CmsArticle).
  • Frontend liest trans('b2in.articles'): app/Livewire/Web/Components/Sections/MagazinList.php:20 und MagazinDetail.php:48.
  • CmsArticle wird außerhalb des Admin-Bereichs nirgends verwendet (verifiziert per Grep).

Wirkung 1: Was Marcel im Magazin-CMS bearbeitet, erscheint nicht auf der Website. Wirkung 2 (für dieses Projekt entscheidend): Die Umsetzungsliste sagt für die Kernmarken „adaptiert vom Magazin-Muster". Das Admin-CRUD ist als Vorlage brauchbar, die Frontend-Anbindung fehlt dort aber – dieser Fehler darf nicht mitkopiert werden.

Bei der Umsetzung aufgefallen – das ist kein reiner Verdrahtungsfehler: Die Artikel liegen in beiden Quellen (5 in cms_articles, 5 in der Lang-Datei, inhaltsgleich), aber unter verschiedenen Schlüsseln: die Lang-Datei nutzt 1–5, die Datenbank die IDs 8–12. Die öffentlichen URLs lauten heute /magazin/1 … /magazin/5. Eine Umstellung auf das Model ändert damit die öffentlichen URLs und braucht eine Slug-Route plus Weiterleitungen von den alten numerischen IDs – sonst brechen bestehende Links und die Google-Indexierung.

Deshalb bewusst nicht einfach umgestellt. Entscheidung nötig:

Variante Aufwand Wirkung
A · Slug-Routen /magazin/{slug} + 301 von den alten IDs ≈3–4 Std. sauber, CMS wird wirksam, URLs werden sprechend
B · Numerische Route beibehalten, Auflösung über die DB-ID ≈2 Std. CMS wird wirksam, aber /magazin/1 … /magazin/5 werden zu 404
C · Vorerst so lassen 0 Magazin funktioniert, das Magazin-CMS bleibt aber wirkungslos

Umgesetzt: Variante A.

  • CmsArticle::forFrontend() liefert die veröffentlichten Artikel nach Slug indiziert. Das CMS gewinnt; solange keine Artikel gepflegt sind, greift die Lang-Datei als Fallback – dieselbe Reihenfolge wie bei cms_theme_section(), damit das Magazin auf einer frischen Installation nicht leer ist.
  • Die ID→Slug-Zuordnung lag als private Konstante im CmsArticleSeeder und ist jetzt CmsArticle::LEGACY_ID_TO_SLUG – eine Quelle für Seeder, Fallback und Weiterleitung.
  • MagazinList und MagazinDetail lesen aus dem Model statt aus trans('b2in.articles'); die Übersicht verlinkt über route('magazin.detail') statt über die numerische ID.
  • Neue Route /magazin/{slug}. Die alten /magazin/1 … /magazin/5 leiten dauerhaft (301) auf den Slug um; sie stehen per whereNumber() vor der Slug-Route, sonst würde diese die Ziffern selbst schlucken. Numerische IDs ohne Zuordnung liefern 404.
  • Die Detailseite fragte die Artikel zweimal ab (Artikel + verwandte Artikel) – jetzt gemerkt, eine Abfrage pro Seitenaufruf.

Tests: tests/Feature/MagazinPageTest.php auf 13 Fälle erweitert – Slug-Auslieferung, 301 von jeder alten ID, 404 bei unbekanntem Slug, CMS schlägt Lang-Datei, unveröffentlichte Artikel sind unsichtbar, Fallback bei leerer Tabelle. LiveSiteReviewTest prüft für jede alte ID die Weiterleitung und dass das Ziel lädt.

⚠️ Beim Deployment: /magazin/1 … /magazin/5 sind indexiert. Die 301 fängt sie ab, aber nach dem Livegang bitte einmal in der Search Console gegenprüfen. Externe Links, die noch auf die alten URLs zeigen, bleiben funktionsfähig.

Nebenbefund, nicht angefasst: MagazinDetail::$relatedArticles wird berechnet, aber von keiner View gerendert – entweder die Sektion „verwandte Artikel" nachziehen oder die Property entfernen. Deine Entscheidung.

B3 · Ankündigungsleiste: alte Wegklicker sehen die neue Leiste nie ✅

announcement-bar.blade.php:2 merkt sich das Wegklicken in localStorage['announcement_dismissed_<id>']. Bleibt die id beim Umtexten gleich, bleibt die Leiste für alle Wiederkehrer unsichtbar.

Behoben: Der Dismiss-Schlüssel wird jetzt aus Inhalt und id abgeleitet (AnnouncementBar::getDismissKeyProperty()). Wird die Leiste im CMS neu belegt, sehen auch Wiederkehrer die neue Meldung – ohne dass jemand daran denken muss, die id mitzuändern. Tests: 2 neue Fälle in tests/Feature/AnnouncementBarTest.php.

footer.blade.php – Claim „Connecting Design and Property" steht in Zeile 15 im Markup, Footer.php lädt überhaupt keinen Content. Es gibt keine footer-Sektion in cms_section_map.php.

Wirkung: Sowohl „Footer-Claim ersetzen" als auch „FAQ in den Footer" aus der Umsetzungsliste erfordern einen kleinen Umbau; Marcel kann den Claim danach ohne CMS-Anbindung weiterhin nicht ändern. Behoben: Neue CMS-Sektion global.footer (Claim, Byline, Linkliste) in cms_section_map.php und in beiden Lang-Dateien. Footer.php lädt sie über cms_theme_section('footer'), die View rendert Claim und Links daraus und fällt ohne CMS-Eintrag auf sinnvolle Defaults zurück. Der Claim lautet jetzt „Premium-Einrichtung. Persönlich gemacht.", FAQ ist als erster Link drin. Tests: tests/Feature/FooterTest.php (4 Fälle).

„Privacy Policy", „Terms of Service", „Cookie Policy" waren hartkodiert englisch, während „Impressum" korrekt über __('ui.legal_notice') lief. Behoben: neue Keys privacy_policy, terms_of_service, cookie_policy, faq in resources/lang/{de,en}/ui.php; die deutsche Seite zeigt jetzt Datenschutz / AGB / Cookie-Richtlinie / Häufige Fragen / Impressum.

B6 · 19 Seiten tragen die alte Dubai-Beschreibung ✅

web-master.blade.php:11 und :18 haben als Fallback fest verdrahtet: „B2in – Connecting Design and Property. Exklusive Immobilien in Dubai & europäisches Einrichtungsnetzwerk." 19 Views unter resources/views/web/ setzen kein @section('meta_description') und erben damit diesen Text – darunter die Live-Seite magazin-detail.blade.php.

Behoben: Der Fallback in web-master.blade.php (Description und OG-Description) trägt jetzt den Interior-Text aus umsetzung-inhalte.md §0. Zusätzlich <link rel="canonical"> ergänzt, das fehlte komplett. Offen als B10: Der Alt-Begriff-Bestand im Seiteninhalt („Supply-Chain", „Local-for-Local", „Ökosystem", „Investment", „Connecting Design") – 90 Treffer in 14 Dateien, u. a. der Hero-card_text. Das ist Content-Arbeit mit den freigegebenen Texten und gehört in AP1, nicht in einen Bugfix.

B7 · Content-Seeder sind nirgends registriert ✅

DatabaseSeeder::run() ruft nur TestDisplaySeeder. CmsContentSeeder, CmsArticleSeeder, CmsProjectSeeder, CmsLegalSeeder, NetzwerkPageCmsSeeder müssen von Hand aufgerufen werden; ein frisch aufgesetztes System hat keine CMS-Inhalte. Behoben: CmsContentSeeder, CmsLegalSeeder, NetzwerkPageCmsSeeder, CmsArticleSeeder und CmsProjectSeeder sind in DatabaseSeeder::run() registriert – nach dem B1-Fix gefahrlos, weil bestehende Inhalte unangetastet bleiben.

B8 · Klartext-Passwort im Seeder ✅

database/seeders/DatabaseSeeder.php enthielt ein echtes Passwort im Repo. Behoben: Zugangsdaten kommen jetzt aus config/seeding.php bzw. SEED_ADMIN_* in der .env; .env.example ist ergänzt. ⚠️ Das alte Passwort steht weiterhin in der Git-Historie – bitte rotieren, falls es irgendwo sonst benutzt wird.

B9 · Medien-Picker greift bei leeren Logo-Feldern nicht ✅

content-index.blade.php → _cmsFieldLooksLikeImage() erkennt Bildfelder an einer festen Feldnamen-Liste oder an der Dateiendung im Wert. logo steht nicht in der Liste, und ein noch leeres Feld hat keinen Wert → wird als reines Textfeld gerendert.

Wirkung: Marcel kann bei einer neu angelegten Marke bzw. bei den Sponsoring-Logos das Bild nicht per Picker setzen, sondern müsste den Dateinamen abtippen. Betrifft AP2 und AP6 direkt. Behoben: logo in die Feldnamen-Liste und _logo$ in die Suffix-Regex aufgenommen. Zusätzlich prüft _cmsFieldLooksLikeImage() den Feldnamen jetzt vor dem Wert – ein noch leeres Bildfeld wird dadurch als Bild erkannt und bekommt den Picker. (logo_width bleibt korrekt ein Textfeld.)

B11 · /magazin/{unbekannte-id} liefert 200 statt 404 ✅

MagazinDetail::loadArticle() fiel bei unbekannten IDs still auf Artikel 1 zurück ($articles[$this->articleId] ?? $articles[1]). /magazin/999 lieferte damit Status 200 mit dem Inhalt von Artikel 1 – beliebig viele URLs mit identischem Inhalt, schlecht für die Indexierung, und bei fehlendem Artikel 1 ein Fehler statt einer 404.

Behoben: abort_unless(isset($articles[$this->articleId]), 404). Tests: 2 neue Fälle in tests/Feature/MagazinPageTest.php.


3. Arbeitspakete

Gliederung folgt der Umsetzungsliste. Reihenfolge nach dem Vorschlag aus umsetzung-inhalte.md.

AP0 · Vorbereitung (im Rahmen von AP1)

Branch feature/b2in-interior, DB-Backup, Backup der Startseite (Muster dev/2026-02-27/backup-before-relaunch/), php artisan test --compact als Referenzstand.

AP1 · Website-Umstrukturierung & Umtextung — ✅ umgesetzt

Global

  • Meta-Title und Description auf Interior umgestellt; fünf Live-Seiten hatten gar keine eigene Description und erbten den Fallback – jetzt gesetzt (magazin-detail, impressum, privacy, terms, cookie-policy). OG-Tags leiten sich automatisch ab.
  • Navigation: Home · Marken · Magazin · Immobilien · Über B2in. „Netzwerk" ist raus (Seite bleibt erreichbar), FAQ steht im Footer. „Referenzen" kommt mit AP5 dazu.
  • Footer-Claim „Premium-Einrichtung. Persönlich gemacht." (siehe B4).

Startseite – neue Reihenfolge in resources/views/web/home.blade.php: header → hero → marcel-badge → weiche → kernmarken → über-marcel → sponsoring → immobilien-teaser → abschluss-cta → footer

  • Ankündigungsleiste neu belegt (Rotpunkt Küchen), inkl. neuer id.
  • Hero: neue Headline, Subline, zwei Buttons, drei Checkmarks, Bild-Badge „Einrichtung, die bleibt."
  • Marcel-Badge umgetextet.
  • Neu: Sections/AudienceSwitch (Weiche Privat/Geschäft) – ersetzt „Zwei Welten. Ein Netzwerk."
  • Neu: Sections/CoreBrands mit CABINET, Rotpunkt Küchen, Febrü (Anker #marken). Febrü bewusst mit Home-Office zuerst. Datenquelle ist vorerst das CMS-Sektionsfeld – in AP2 wird sie durch das Model CmsBrand samt Detailseiten ersetzt. Das Markup bleibt.
  • Neu: Sections/Sponsoring (AP6) und Sections/ImmobilienTeaser.
  • „Ein Ökosystem, drei Säulen" ist von der Startseite genommen, die Komponente bleibt als Archiv.

Sponsoring-Logos: je Verein eine Datei gewählt, auf max. 400 px Breite herunterskaliert (326 KB → 130 KB) und nach public/img/assets/sponsoring/ übernommen. Die drei Seitenverhältnisse sind sehr unterschiedlich (500×262, 689×689, 2443×741), deshalb eine feste Box je Logo. tc-bad-waldliesborn-horn liegt nur als JPG vor und hat damit keinen transparenten Hintergrund.

Durchgang Alt-Begriffe: auf allen Live-Seiten bereinigt – Startseite, /about, /contact, /immobilien sind sauber; /netzwerk und /faq ebenfalls. Nicht angefasst: die Magazin-Artikel, die inhaltlich über Supply-Chain und Investments handeln – das sind redaktionelle Beiträge, keine Seitentexte.

Tests: tests/Feature/HomePageStructureTest.php (11 Fälle: Reihenfolge, Hero-Buttons und -Checkmarks, Kernmarken samt Anker, Febrü-Gewichtung, Navigation, SEO, Sponsoring-Logos); AnnouncementBarTest und LiveSiteReviewTest auf die neuen Inhalte gezogen.


⚠️ Wichtig fürs Deployment: der Relaunch-Seeder

cms_theme_section() bevorzugt das CMS gegenüber der Lang-Datei. 14 der neu getexteten Sektionen lagen bereits mit den alten Texten im CMS und hätten die neuen Inhalte überdeckt – der reguläre CmsContentSeeder fasst bestehende Einträge seit B1 bewusst nicht mehr an.

Deshalb gibt es InteriorRelaunchCmsSeeder: er überschreibt genau die 19 in AP1 geänderten Sektionen aus den Lang-Dateien und lässt alles andere in Ruhe.

php artisan db:seed --class=InteriorRelaunchCmsSeeder

Auf der lokalen Datenbank bereits gelaufen (5 angelegt, 14 aktualisiert). Auf dem Live-System vor dem Lauf ein DB-Backup ziehen – er überschreibt in diesen Sektionen auch redaktionelle Änderungen. Danach npm run build:web.


Nebenbefund B12 · /service und /portfolio lieferten 500 ✅

Beide Seiten sind öffentlich erreichbar, aber ihre Content-Sektionen (hero_image, content_section_left/right, portfolio) existieren in keiner Lang-Datei mehr – vermutlich seit dem Februar-Relaunch. Sie warfen seitdem einen 500er; da sie in keinem Test vorkamen, ist es niemandem aufgefallen. Verifiziert: älter als AP1, keine Regression.

Behoben wie bei den anderen stillgelegten Seiten per 301 (/service → /netzwerk, /portfolio → /immobilien); die Views bleiben als Archiv liegen. Beide Ziele sind eine Zeile in routes/web.php, falls Du woandershin umleiten möchtest.


Was in AP1 offen bleibt

Punkt Grund
Hero-Bildmotiv, Kachelbilder, OG-Image Zulieferung offen, Bestandsbild bleibt
Navigationspunkt „Referenzen" kommt mit der Seite in AP5
Teaser „Aktuelles aus unseren Showrooms" kommt mit AP5, sonst zeigt er ins Leere

Logo mit Zusatz „Interior" ✅ (Zulieferung eingetroffen)

Marcel hat die Dateien geliefert (dev/2026-08-28/logos/) – die typografische Eigenlösung entfällt damit. Beide Varianten sind identisch aufgebaut und unterscheiden sich nur im Füllwert: positiv #314052, negativ #FFFFFF; der blaue Akzent #2C9FDA bleibt in beiden. Trotz „CMYK" im Dateinamen enthalten die SVGs RGB-Hexwerte, die Schrift liegt in Pfaden.

Zuschnitt war nötig: Die Lieferung lag auf einem A4-Artboard (595,3 × 419,5) – gemessen 74 % leerer Weißraum. Ungeschnitten hätte das Logo bei gleicher CSS-Höhe rund halb so groß gerendert wie das bisherige. Der Inhaltsrahmen wurde durch Rendern ermittelt und das viewBox auf 153 96 289.3 227.5 gezogen (98 % Füllgrad, gleichmäßige Randluft, Verhältnis 1,27:1).

Abgelegt als public/img/logos/b2in-interior-logo-{positive,negative}.{svg,png}; die alten b2in-logo-*-Dateien bleiben als Archiv liegen.

Angepasst: ThemeHelper (b2in → neue Dateien) · Header h-10 → h-12 und Footer h-14 → h-16, weil das neue Lockup gestapelt ist und die Wortmarke bei gleicher Höhe kleiner wirkt · Footer- und Mail-width/height-Attribute auf das neue Verhältnis korrigiert (standen auf 2,14:1 bzw. 3,5:1 und passten schon vorher nicht) · Mail-Header nutzt das neue PNG.

Display ebenfalls umgestellt (Marcel möchte es laut Mail einheitlich):

  • Beide Varianten liegen jetzt auch unter public/_cabinet/assets/.
  • DisplayModuleSettings: Default-Logo und Claim getauscht. Der Claim stand dort noch auf „Connecting Design & Property".
  • Variante nach Theme: Das Standard-Theme ist dark, dort liegt das Logo auf einem dunklen Scrim – bislang stand da die dunkle Variante (deshalb der drop-shadow im Player). Jetzt dunkles Theme → negativ/weiß, helles Theme → positiv.
  • Logo-Höhe im Player von 3.5vh auf 4.6vh, weil das neue Lockup gestapelt ist.
  • DisplayInteriorBrandingSeeder vereinheitlicht die bestehenden Module. Vorher liefen dort vier verschiedene Logo-Quellen nebeneinander (zwei Dateien aus der Medienbibliothek, ein relativer und ein absoluter Asset-Pfad) und zwei Module trugen den Claim „Hello".

B13 · Live-Displays zeigten auf eine lokale Entwicklungsdomain ⛔→✅

Beim Vereinheitlichen aufgefallen: Medien-URLs lagen als absolute URLs mit fest verdrahtetem Host in den Moduleinstellungen und Item-Inhalten – teils b2in.eu, teils b2in.test. Der Player gibt absolute URLs unverändert zurück, ein auf b2in.test verweisendes Medium kann auf dem Live-Display also nicht laden.

Betroffen waren vier veröffentlichte Playlists: „Display 4 – Innen", „Kundenstopper", „Display 2" und „Test-Display" – unter anderem über das Modul „Cabinet-Influencerin", das auf drei der vier Displays läuft.

Behoben: DisplayMediaUrlNormalizerSeeder schreibt Schema + Host auf einen reinen Pfad um. Relative Pfade löst der Player über resolveDisplayUrl() gegen die Portal-Basis auf und funktionieren damit auf Test- und Live-System. 23 Verweise umgeschrieben, 0 verblieben.

Ziel-Links (Footer-URL, QR-Ziel wie cabinet-bielefeld.de) bleiben bewusst absolut – der Filter greift nur bei einem B2in-Host und einem Pfad unterhalb von /storage/. Nachgeprüft: die fünf Ziel-Links stehen unverändert.

Tests: DisplayInteriorBrandingTest (6) und DisplayMediaUrlNormalizerTest (6).

⚠️ Wichtig: Ob die betroffenen Mediendateien auf dem Live-Server überhaupt vorhanden sind, kann ich von hier nicht prüfen. Wurden sie nur lokal hochgeladen, fehlen sie dort weiterhin – der relative Pfad ist dann trotzdem richtig, aber die Datei muss nachgezogen werden. Nach dem Deployment bitte die vier Displays einmal sichten.

Deployment-Reihenfolge:

php artisan db:seed --class=InteriorRelaunchCmsSeeder
php artisan db:seed --class=DisplayInteriorBrandingSeeder
php artisan db:seed --class=DisplayMediaUrlNormalizerSeeder
npm run build:web

AP2 · Kernmarken-Modul im CMS — ✅ umgesetzt

Model + Migration: CmsBrand / Tabelle cms_brands. ⚠️ Bewusst nicht Brand – das Model existiert bereits im Produkt-/Partner-Kontext (Partner::brand(), Product::brand()) und bleibt unangetastet. Ein Test hält das fest.

Felder: slug, name, tagline, teaser, description (HTML), logo, image, services, gallery, website_url, showroom, is_published, order. Übersetzt werden name, tagline, teaser, description.

Die Lehre aus B2 ist eingebaut: Das Frontend liest direkt aus dem Model, nicht aus einer Lang-Datei. CmsBrand::forFrontend() liefert die veröffentlichten Marken nach Slug indiziert; solange keine Marke gepflegt ist, greift die Lang-Sektion core_brands als Fallback. Die Fallback-Einträge tragen has_detail_page = false, damit die Kachel dann nicht auf eine Detailseite verlinkt, die es noch nicht gibt.

Admin: admin/flux-cms/brands → brands-index.blade.php (Volt), aufgebaut nach dem Magazin-Muster. Medien über den bestehenden MediaPicker (Logo, Stimmungsbild, Galerie), die Einleitung über den Flux-Editor mit CmsFluxEditorHtmlTransformer. Slug wird auf Format und Eindeutigkeit geprüft. Eigene Karte im CMS-Dashboard.

Frontend:

  • Sections/CoreBrands rendert die Kacheln aus dem Model (Anker #marken).
  • Neue Route /marken/{slug} mit Sections/BrandDetail und der Vorlage-Struktur aus umsetzung-inhalte.md §5: Header (Logo + Claim + Stimmungsbild) → Einleitung → Leistungen → Galerie → CTA, dazu eine Leiste mit den übrigen Marken.
  • Eigener Title und eigene Description je Marke.
  • Unveröffentlichte Marken liefern 404 und verschwinden von der Startseite.

Seeder: CmsBrandSeeder legt CABINET, Rotpunkt Küchen und Febrü mit den Texten aus umsetzung-inhalte.md §5 sowie je vier Leistungen an – Febrü mit Home-Office zuerst. Bestehende Marken werden nicht überschrieben, Marcel pflegt sie im Admin.

Tests: Cms/CmsBrandTest (8), BrandPageTest (11), Admin/Cms/CmsBrandsIndexTest (11) – darunter die Gegenprobe zu B2: eine im CMS neu angelegte Marke erscheint ohne Codeänderung auf der Startseite und bekommt ihre Detailseite.

Nachtrag: eigene Unterseite /marken

Auf Wunsch nach der Sichtung – Grundlage dev/2026-08-28/unterseite_marke.md. Die Startseite bleibt wie sie ist, die Kernmarken-Sektion dort ist jetzt ein Teaser.

  • Neue Seite /marken mit dem Aufbau aus der Spec: Seitenkopf mit Breadcrumb (Home → Marken) → Kernmarken → Partnernetzwerk → „Markenwelt wächst" → Abschluss-CTA. Eigener Title und eigene Description.

  • Zwei Textebenen je Marke. Dafür kamen zwei Felder dazu (summary, highlights):

    Feld Wo
    teaser Kachel auf der Startseite (gekürzt)
    summary Text auf /marken (zwei Absätze)
    highlights die drei Merkmale auf /marken
    description Einleitung der Detailseite
    services Leistungen der Detailseite
  • Startseite: Kacheln verlinken direkt auf die Detailseite, darunter der Button „Alle Marken ansehen" → /marken.

  • Navigation: „Marken" zeigt auf /marken statt auf den Anker /#marken; der Hero-Button ebenfalls. Der Anker #marken bleibt für Direktsprünge erhalten.

  • Slug korrigiert: Die Spec nennt /marken/rotpunkt, geseedet war rotpunkt-kuechen. Angepasst, solange nichts live ist – später wäre das ein Redirect gewesen.

  • /netzwerk stillgelegt. Der Inhalt geht im Abschnitt „Mehr als Marken: unser Netzwerk" auf /marken auf. /netzwerk, /interior, /partner und /service leiten jetzt direkt auf /marken – vorher liefen /interior und /partner über /netzwerk, also eine Redirect-Kette. Die View bleibt als Archiv liegen.

  • WhatsApp-Button: erscheint nur, wenn im CMS unter global.contact.whatsapp eine Nummer hinterlegt ist (whatsapp_link()). Solange die Nummer nicht bestätigt ist, zeigt der CTA nur „Termin anfragen" – kein Button auf eine erfundene Nummer.

Tests: MarkenPageTest (9), InteriorPageTest neu geschnitten (4), dazu zwei Fälle in Admin/Cms/CmsBrandsIndexTest – darunter die Gegenprobe, dass eine im Admin angelegte Marke mitsamt Text und Merkmalen auf /marken erscheint.

Notiz für spätere Reihenfolge-Tests: wire:snapshot serialisiert die Komponentendaten als JSON ins Attribut und escaped Umlaute zu \uXXXX. Ein strpos-Vergleich auf dem rohen HTML misst dann den Snapshot statt des sichtbaren Textes. In MarkenPageTest werden die Snapshots vorher entfernt.

Offen: Logos, Stimmungsbilder und Galerien fehlen als Zulieferung – Kacheln und Detailseiten zeigen so lange einen Platzhalter. Der WhatsApp-Button im CTA-Block kommt mit AP3, sobald die Nummer bestätigt ist.


Nachtrag 2: Galerie je Marke – realisierte Projekte und Inspirationen getrennt

Aus Marcels Mail: „Der Bereich Referenzen ist mir wichtig … Entscheidend ist für mich, dass dieser Bereich langfristig mitwachsen und von mir selbst gepflegt werden kann. Für den Start können wir ergänzend mit hochwertigen Visualisierungen und KI-unterstützten Darstellungen arbeiten. Dabei sollte allerdings klar zwischen tatsächlich realisierten Referenzen und Entwürfen beziehungsweise Inspirationen unterschieden werden."

Umgesetzt als Galerie auf der Detailseite jeder Kernmarke, mit zwei Rubriken.

  • Datenform. Das bestehende JSON-Feld cms_brands.gallery bekommt je Eintrag image, category (project | inspiration) und caption. Keine Migration nötig.
  • Sicherheitsnetz gegen die schädlichere Verwechslung: CmsBrand::normalizeGallery() stuft jeden Eintrag ohne oder mit unbekannter Kategorie als Inspiration ein – nie als Projekt. Ein Entwurf, der versehentlich als Referenz durchgeht, wäre der teurere Fehler. Die Normalisierung greift beim Laden, beim Speichern und beim Ausspielen.
  • Frontend (brand-detail.blade.php): Abschnitt „Referenzen und Inspirationen" mit Umschalter, sobald beide Rubriken gefüllt sind; ist nur eine gefüllt, steht sie mit eigener Überschrift da. Jede Kachel trägt ein sichtbares Label („Realisiertes Projekt" / „Visualisierung"), über den Inspirationen zusätzlich der Satz „Diese Darstellungen sind Entwürfe und Visualisierungen – keine ausgeführten Projekte." Lightbox mit Pfeiltasten und Esc, Muster übernommen aus immobilien-show.blade.php.
  • Admin (brands-index.blade.php): zwei getrennte Buttons „+ Projekt" und „+ Inspiration", je Eintrag ein Auswahlfeld und eine Bildunterschrift, dazu ein Hinweis, was in welche Rubrik gehört. Die Kategorie wird beim Anlegen entschieden, nicht per stillem Vorgabewert.
  • Abgrenzung zu AP5: AP5 bleibt die Rubrik-Seite „Referenzen" mit dem zweiten Display. Die markenbezogene Galerie hier ist die pflegbare Grundlage dafür und ersetzt AP5 nicht.

Tests: 5 Fälle in BrandPageTest (keine Galerie → kein Abschnitt; beide Rubriken beschriftet; kein Umschalter bei nur einer Rubrik; Eintrag ohne Kategorie erscheint als Visualisierung; Hinweis sichtbar), 5 in Cms/CmsBrandTest (Normalisierung, Gruppierung), 4 in Admin/Cms/CmsBrandsIndexTest (Anlegen je Rubrik, Speichern, unbekannte Kategorie, Löschen).

Offen: Bildmaterial. Marcel setzt Projektfotos und Visualisierungen selbst ein.


AP3 · Terminanfrage-Widget + WhatsApp — ✅ umgesetzt

Korrektur gegenüber Revision 1: kein Inline-Abschnitt, sondern ein schwebendes Element unten rechts auf allen Seiten → gehört ins Layout web-master.blade.php, nicht in home.blade.php.

  • Aufsetzen auf Acme\ContactForm\Livewire\ContactForm per customFields – Honeypot, Rate-Limit und SpamDetector kommen damit geschenkt.
  • Felder: Name*, E-Mail*, Telefon, Anliegen/Marke (Auswahl), Wunschtermin, Nachricht.
  • Markenauswahl speist sich aus CmsBrand → Abhängigkeit zu AP2 (Reihenfolge einhalten), plus feste Einträge „Projektausstattung" und „Sonstiges".
  • WhatsApp-Button daneben, Nummer als CMS-Wert (global.contact.whatsapp), Link https://wa.me/<nummer>.
  • Cookie-Consent beachten (packages/acme/CookieConsent).

Tests: AppointmentWidgetTest (17 Fälle: Einbindung auf allen Seiten, Markenauswahl aus CmsBrand, Validierung inkl. Wunschtermin in der Vergangenheit, Übergabe an den ContactFormService, Honeypot, WhatsApp sichtbar/unsichtbar); ContactFormTest bleibt grün.

Umsetzungsnotizen

  • Abweichung vom Plan: nicht auf Acme\ContactForm\Livewire\ContactForm per customFields aufgesetzt. Dessen mitgelieferte View bringt ein eigenes, blaues Layout mit, das nicht zum Auftritt passt. Stattdessen eine eigene Komponente wie ContactForm und ImmobilienContactForm der Seite, die ContactFormService und SpamDetector aus dem Paket nutzt – Honeypot, Rate-Limit und Spam-Erkennung sind damit trotzdem drin.
  • Platzierung: web-master.blade.php, unten rechts, z-40. Der Cookie-Button des Consent-Managers sitzt unten links (ebenfalls z-40), sein Dialog auf z-50 – die Einwilligung bleibt also immer erreichbar und wird nie verdeckt.
  • Kein eigener Consent nötig: Das Widget setzt nichts im Browser, der WhatsApp-Link ist ein ausgehender Link ohne Einbettung.
  • Die [x-cloak]-Regel liegt jetzt einmal zentral im Layout – die Seiten brachten sie bisher nur teilweise selbst mit, das Widget braucht sie überall.
  • Felder: Name*, E-Mail*, Telefon, Anliegen (Auswahl), Wunschtermin, Nachricht, Einwilligung*. Der Wunschtermin ist auf heute oder später begrenzt, im Browser über min und serverseitig über after_or_equal:today.
  • Betreff der Mail: Terminanfrage B2in Interior: <Anliegen>, Log-Kontext appointment-widget – so lassen sich Widget-Anfragen von den übrigen Formularen unterscheiden. Die Seite, von der die Anfrage kam, steht als source in der Mail.
  • WhatsApp erscheint weiterhin nur, wenn unter global.contact.whatsapp eine Nummer im CMS steht. Ohne Nummer zeigt das Widget allein den Termin-Button.

WhatsApp: was technisch geht und was nicht

Eine echte WhatsApp-Anbindung (Nachrichten empfangen, im CMS beantworten, Verlauf speichern) setzt die WhatsApp Business Platform voraus: Meta-Verifizierung des Unternehmens, ein Anbieter für den API-Zugang, laufende Kosten je Konversation. Das ist nicht Teil dieses Auftrags.

Umgesetzt ist die dafür übliche Alternative, ein Klick-zum-Chat-Link (wa.me):

  • Der Button sitzt im Terminanfrage-Panel unter dem Absenden-Button.
  • Auf dem Handy öffnet die installierte WhatsApp-App, am Rechner WhatsApp Web bzw. die Desktop-App. Ist WhatsApp nirgends vorhanden, landet man auf der Hinweisseite von wa.me.
  • Der Nachrichtentext ist vorbereitet und wird im Browser aus dem zusammengesetzt, was im Formular schon steht (Anliegen, Wunschtermin, Name). Der Einstiegssatz ist im CMS pflegbar (global.appointment_widget.whatsapp_text).
  • Abgeschickt wird die Nachricht vom Besucher selbst – das steht auch so unter dem Button.

Was das bedeutet, bewusst festgehalten:

Wo landet die Nachricht in Marcels WhatsApp, nicht im CMS und nicht per Mail
Was sehen wir davon nichts – kein Eintrag, keine Statistik, kein Spamschutz
Voraussetzung eine Mobilnummer, die WhatsApp empfangen kann (sinnvoll: WhatsApp Business)
Datenschutz ausgehender Link zu Meta. In die Datenschutzerklärung gehört ein Absatz dazu; ein Consent-Banner braucht es nicht, weil nichts eingebettet wird und nichts gesetzt wird

Wer eine Anfrage im System dokumentiert haben will, nutzt weiterhin das Formular – der WhatsApp-Weg ist die schnelle, aber unprotokollierte Abkürzung.

Nachtrag 3 · Bestandsinhalte auf den Interior-Auftritt umgestellt ✅

Nach der Sichtung: Drei verlinkte Seiten erzählten noch die Dubai-/Plattform-Geschichte. /immobilien bleibt bewusst unverändert – die Seite ist der „Außerdem"-Bereich.

/faq – die beiden Rubriken „Fokus: Immobilien-Investoren (B2C)" und „Fokus: B2B-Partner" sind ersetzt durch „Für Ihr Zuhause" und „Für Projekte und Objekte" (je vier Fragen: Einstieg mit einem einzelnen Möbelstück, Kosten der Maßanfertigung, Montage, Showroom – bzw. Projektgröße, Termintreue, Architekten, Netzwerk). Von den allgemeinen Fragen waren zwei noch Dubai-lastig, die sind jetzt Beratungsablauf und Verlässlichkeit. Die englische Fassung war noch komplett auf dem alten Stand und ist mitgezogen.

/about – das Mission-Zitat sprach von „digitalen Werkzeugen" und „Brücken zwischen Märkten". Neu: wenige ausgewählte Hersteller, Fachbetriebe aus der Region, ein Ansprechpartner. Die sechs Werte waren Plattform-Vokabular (Innovation, Konnektivität, Design-Exzellenz) und heißen jetzt Beratung statt Verkauf, Maßarbeit, Ausgewählte Marken, Regional verwurzelt, Ein Ansprechpartner, Verlässlichkeit. In der Zeitleiste sind „Die Idee", „Das Fundament" und „Die Vision" umgeschrieben.

/contact – die Betreffliste beginnt jetzt mit Beratungstermin, Maßanfertigung und Küchenplanung; „Internationale Immobilien" rutscht nach hinten, bleibt aber erhalten.

/magazin – zwei neue Beiträge, die an erster Stelle stehen: massanfertigung-warum-der-millimeter-zaehlt und kueche-planen-vom-wunsch-zum-grundriss. Die vier Dubai-Beiträge bleiben, sie gehören zum Immobilienbereich.

⚠️ Die beiden Beiträge sind Platzhalter. Bewusst ohne Referenzen, Zahlen oder Namen geschrieben, damit nichts behauptet wird, was nicht belegt ist – vor dem Livegang von Marcel gegenlesen lassen. Als Bilder dienen vorhandene Motive aus dem Altbestand.

Zwei technische Punkte dabei:

  • Neue Lang-Artikel bringen ihren Slug jetzt selbst mit ('slug' => …, aufgelöst über CmsArticle::slugForLangArticle()). LEGACY_ID_TO_SLUG bleibt damit das, was der Name sagt: die Karte für die alten /magazin/{id}-URLs. /magazin/6 ist korrekt ein 404, weil es diese URL nie gab.
  • CmsArticleSeeder sortiert nicht mehr per ksort. Die Reihenfolge in der Lang-Datei bestimmt jetzt die Reihenfolge im Magazin – im CMS über order, im Lang-Fallback über die Array-Reihenfolge. Vorher konnten beide Wege auseinanderlaufen.

Tests: vier Fälle in MagazinPageTest (Reihenfolge, beide Slugs, kein erfundener Legacy-Link, Slug-Auflösung), drei in LiveSiteReviewTest (FAQ ohne Dubai mit neuen Rubriken, About mit den neuen Werten, Kontakt mit den neuen Betreffen).

Nachkontrolle: Gegen die Live-Seiten geprüft, nicht nur gegen die Sprachdatei – dabei fiel in der FAQ eine dritte Frage auf, die noch vom „Local for Local"-Netzwerk sprach. Ebenfalls korrigiert.

Noch offen auf diesen Seiten: eigene Beitragsbilder für die zwei neuen Artikel; die vier Dubai-Beiträge bleiben inhaltlich unverändert stehen.


Nachtrag 4 · Markenseiten Rotpunkt und Febrü aus der Zulieferung ✅

Grundlage: zunächst Rotpunkt_Kuechen_Webseitentext_B2in_FINAL.pdf und Februe_Webseitentext_B2in_FINAL.pdf (Stand 31.08.2026, im selben Ordner). Beide enthalten neben dem Seitentext einen Abschnitt „UMSETZUNGSHINWEIS FÜR KEVIN – WEBSTRUKTUR UND CTA“.

Die als Aufgabenliste angekündigte Datei umsetzung-03-09-2026.md war zu diesem Zeitpunkt leer (0 Byte). Sie wurde nachgereicht – die Auswertung steht in Nachtrag 5, der auch die korrigierten Textfassungen (*_FINAL.md) einarbeitet. Für den Textstand gilt Nachtrag 5.

Was die Zulieferung verlangt:

  1. Der normale Seitentext bleibt direkt sichtbar.
  2. Die mit „AUFKLAPPBARER DETAILBEREICH“ gekennzeichneten Vertiefungen werden aufklappbar.
  3. Kein zweiter CTA-Button. Stattdessen bekommt das vorhandene schwebende Terminanfrage-Widget auf der jeweiligen Markenseite eine kontextbezogene Beschriftung.
  4. Das Widget bleibt beim Scrollen sichtbar und darf auf dem Smartphone nichts verdecken.
  5. Die internen Quellenhinweise am Ende der PDFs werden nicht veröffentlicht.

Umsetzung

Datenstruktur. description (Einleitung) und services (Kacheln) reichten für diese Texte nicht aus. Neu in cms_brands:

Feld Inhalt
sections Abschnitte der Seite: Titel, Absätze, optional Schlusszeile, Zitat und Vertiefung
widget_label Beschriftung des Terminanfrage-Widgets auf dieser Markenseite
cta_title / cta_text Abschlussblock der Seite

sections ist – wie services, highlights und gallery – bewusst nicht übersetzbar; die Texte liegen nur auf Deutsch vor. Die drei Textfelder folgen den übrigen Textfeldern und sind übersetzbar. CmsBrand::normalizeSections() bringt jeden Eintrag in eine feste Form und nimmt Absätze wahlweise als Liste oder als Textblock entgegen (Leerzeile trennt) – dadurch lässt sich im Admin einfach in ein Textfeld schreiben.

Darstellung. Die Abschnitte stehen zwischen Einleitung und Leistungen. Vertiefungen sind aufklappbar, mit aria-expanded und aria-controls. Bewusst x-transition statt x-collapse: Das Collapse-Plugin ist in diesem Projekt nicht registriert, die Bewegung entspricht jetzt dem FAQ-Akkordeon. Zitate stehen abgesetzt mit Nennung des Sprechers, Schlusszeilen hervorgehoben.

Widget-Beschriftung. Das Widget steckt im Layout, nicht in der Seite – es liest den Kontext deshalb aus der Route: Auf marken.show greift widget_label der Marke, sonst die allgemeine Beschriftung. Ein Test prüft gegen, dass dabei kein zweiter Button entsteht.

Platz auf dem Smartphone. Der Footer bekommt unten zusätzlichen Abstand (pb-28 sm:pb-16), und der Button bricht lange Beschriftungen um, statt über den Bildschirmrand zu laufen – „Persönliche Arbeitsplatzberatung vereinbaren“ ist deutlich länger als das vorherige „Termin anfragen“.

Pflegbarkeit. Die Abschnitte sind vollständig im Admin pflegbar: hinzufügen, löschen, per Pfeil sortieren, je Abschnitt Überschrift, Text, Schlusszeile, Zitat und Vertiefung. Dazu die drei neuen Felder für Abschluss und Widget-Beschriftung.

Einspielen. BrandPageTextSeeder schreibt die Texte. Er fasst nur tagline, description, sections, widget_label, cta_title und cta_text an – Bilder, Galerie, Leistungen und Reihenfolge bleiben unangetastet. Ein Test prüft genau das.

Nebenbei korrigiert: Febrüs Claim hieß „Arbeiten, wo man gerne ist.“, die Zulieferung schreibt „Arbeiten, wo man gerne bleibt.“

Tests: BrandPageSectionsTest (17 Fälle: Abschnitte beider Marken, Anzahl der aufklappbaren Bereiche, Zitat, Schlusszeilen, keine Quellenhinweise im Ausgabe-HTML, markenspezifischer Abschlussblock mit Rückfall auf den allgemeinen, Widget-Beschriftung je Marke, kein zweiter Button, Normalisierung, Redaktionsschutz des Seeders) und vier neue Fälle in Admin/Cms/CmsBrandsIndexTest.

⚠️ Vor der Veröffentlichung freigeben lassen. Die PDFs halten fest: Aussagen nur im belegten Umfang veröffentlichen, Herstellerfreigabe einholen, und die Rotpunkt-Aussage zur jährlich erhaltenen Waldfläche von ungefähr 50 Fußballfeldern nur mit ausdrücklicher Bestätigung verwenden. Sie steht als eigener Absatz in der Vertiefung „Zahlen, Zertifizierungen und Raumklima“ und lässt sich dort einzeln entfernen. Als Punkt in Abschnitt 8 aufgenommen.

Offen: CABINET hat noch keinen ausführlichen Seitentext – die Seite läuft weiter über Einleitung und Leistungskacheln. Bilder fehlen weiterhin für alle drei Marken.


AP4 · Display-Spiegelung auf der Website — ✅ umgesetzt

Korrektur gegenüber Revision 1: Revision 1 empfahl, die Slides nativ zu rendern. Bei 3 Std. ist das nicht zu halten, und die Umsetzungsliste schreibt „nutzt die vorhandene Display-/Player-API als Datenquelle". → Einbettung des vorhandenen Players im 9:16-Container.

  • Sektion Sections/DisplayFeed mit aspect-ratio: 9/16, darin der Player.
  • Vorher zu klären: Die /preview/…-Routen sind token- bzw. modulbasiert und für den internen Gebrauch gedacht. Für die öffentliche Domain braucht es eine schmale, read-only Route (z. B. /embed/display/{display}), die keine Preview-Tokens nach außen gibt.
  • Keine doppelte Pflege: Marcel pflegt weiterhin nur im Display-CMS.

Umsetzung

  • Route GET /embed/display/{display} (DisplayEmbedController), bewusst getrennt von den Preview-Routen: Die arbeiten mit preview_token und zeigen den Entwurf. Nach außen darf weder ein Token noch ein unveröffentlichter Stand gelangen. Die Embed-Route kennt nur die numerische ID, liefert 404 bei inaktivem Display, bei reinem Entwurf und bei leerer Playlist, und setzt X-Robots-Tag: noindex.
  • Kein Proxy, keine CORS-Frage: Der Player liest die ID aus dem Pfad (/display/{id} steckt in /embed/display/{id}) und holt seine Konfiguration über die bereits öffentliche Route /api/display/{display}/config – dieselbe Quelle wie das physische Display. Da die API-Routen für alle Domains registriert sind, läuft der Abruf gleichursprünglich über b2in.test. Möglich wurde das durch B13: Vorher standen absolute b2in.test-URLs in den Playlists.
  • Sektion Sections/DisplayFeed im 9:16-Rahmen. Welches Display gezeigt wird, steht im CMS (referenzen.display_feed.display_id); ohne Eintrag greift das erste aktive Display mit veröffentlichter Playlist. Gibt es keins, rendert die Sektion nichts – lieber keine Sektion als ein Rahmen mit ewigem Ladebalken. Auch die Texte bleiben dann leer, sonst stünden sie im Livewire-Snapshot der Seite.
  • Eingebunden auf der Startseite (an der dafür vorgesehenen Stelle) und auf /referenzen.

Tests: DisplayWebFeedTest (10 Fälle: Embed liefert die Datei, kein Token im Ausgabe-Inhalt, 404 bei inaktiv/Entwurf/leer/unbekannt, Einbindung auf der Startseite, CMS-Auswahl und Fallback).

AP5 · Rubrik-Seite „Referenzen" — ✅ umgesetzt

Korrektur gegenüber Revision 1: Revision 1 schlug einen neuen DisplayVersionType-Case vor. Die Umsetzungsliste sagt „zweites, separates Display im Display-CMS" – das ist ein weiterer Display-Datensatz und braucht keine Enum-Änderung. Damit entfallen die Folgeänderungen an DisplayPlaylistConfigBuilder::moduleData(), DisplayModuleSettings und den version-editor-*-Partials. Deutlich risikoärmer und billiger.

  • Zweites Display anlegen, Route /referenzen, View + Sektion, Aufnahme in die Navigation.
  • Startseiten-Teaser „Aktuelles aus unseren Showrooms" mit Button „Alle Referenzen ansehen →".
  • 2–3 Platzhalter-Einträge.
  • YouTube-Verlinkungen mit Consent-Gate.
  • Inhaltliche Regel dokumentieren: unter „Referenzen" nur realisierte Projekte; Visualisierungen und Entwürfe gehören in die Kernmarken-Detailseiten.
  • Bereits vorhanden: die markenbezogene Galerie mit den Rubriken „Realisierte Projekte" und „Inspirationen und Entwürfe" (siehe Nachtrag 2 zu AP2). AP5 baut darauf auf, statt eine zweite Pflegestelle für dieselben Bilder zu schaffen.

Umsetzung

  • Seite /referenzen: Seitenkopf → realisierte Projekte → Showroom-Display → Abschluss-CTA. Navigationspunkt zwischen „Marken" und „Magazin".
  • Kein zweites Display nötig, keine zweite Pflegestelle: Die Projekte kommen aus den Markengalerien (AP2) und werden über alle Marken hinweg gesammelt. Es erscheinen ausschließlich Einträge der Rubrik „Realisiertes Projekt" – Visualisierungen bleiben draußen, damit „Referenzen" hält, was das Wort verspricht. Jede Kachel verlinkt zurück auf ihre Markenseite.
  • Ehrlicher Leerzustand: Solange keine Projekte gepflegt sind, steht dort ein Satz dazu und ein Terminlink – keine erfundene Referenz und keine leere Fläche.
  • Der Seitenkopf ist jetzt eine wiederverwendbare Sektion (PageHero mit section und breadcrumb), vorher war er auf /marken festgelegt.

Tests: ReferenzenPageTest (6 Fälle: Seite lädt mit eigenem Titel, Navigationspunkt, Projekte aus allen Marken, Visualisierungen bleiben draußen, Rückverlinkung, Leerzustand).

AP6 · Engagement / Sponsoring — ✅ umgesetzt (im Rahmen von AP1)

Schlanke Sektion „Regional verbunden" nach der Marcel-Sektion: ein Einleitungssatz plus drei Logos in ruhiger Reihe (Graustufen/gedeckt).

Logos liegen bereits unter dev/2026-08-28/sponsoren-logos/ – 7 Dateien für 3 Vereine (Arminia, TCBWH, TCH in mehreren Varianten und Formaten). Vor der Umsetzung je Verein eine Datei auswählen, nach public/img/assets/ bzw. in die Medienbibliothek übernehmen und vereinheitlichen (gleiche Höhe, transparenter Hintergrund – die .jpg-Varianten haben keinen). Setzt B9 voraus, damit die Logos per Picker pflegbar sind.



Nachtrag 5 · Abstimmungsrunde 03.09.2026 ✅

Grundlage: dev/2026-08-28/umsetzung-03-09-2026.md. Die Datei war beim ersten Zugriff leer und wurde nachgereicht. Dazu kamen die korrigierten Textfassungen als Markdown: Rotpunkt_Webseitentext_FINAL.md und Februe_Webseitentext_FINAL.md. Sie lösen die PDFs vom 31.08. ab – die PDFs bleiben nur als Beleg im Ordner liegen.

Vier Punkte, alle umgesetzt:

# Punkt Stand
1 Markenseiten umfangreicher: Rotpunkt und Febrü korrigiert, CABINET neu ✅
2 „BEISPIEL"-Kennzeichnung bei Visualisierungen entfernen ✅
3 Hinweis zu Visualisierungen eleganter formulieren ✅
4 Abschluss-CTA hochwertiger ✅

1a · Korrigierte Fassungen Rotpunkt und Febrü

Die beiden Markdown-Dateien führen ihre Änderungen gegenüber dem 31.08. selbst auf. Übernommen:

  • Rotpunkt: „Nachhaltigkeit mit konkreter Substanz" und „Greenline hält dem Vergleich stand" sind zusammengeführt; der doppelte Satz „Nachhaltigkeit braucht keine großen Versprechen…" steht nur noch einmal. Rotpunkt hat damit 8 statt 9 Abschnitte, weiterhin 4 Vertiefungen.
  • Febrü: Der erklärende Satz zum Blauen Engel („Die Aussage bezieht sich ausdrücklich auf die offiziell gelisteten Produktgruppen…") ist aus dem Kundentext entfernt – der Produktbezug steckt jetzt im Satz selbst („Ausgewählte Febrü-Tischsysteme und Stauraummöbel"). Ein Test hält diese Formulierung fest, damit sie beim Redigieren nicht pauschal wird.
  • Die Schlusszeile „Nachhaltig ist nicht das Möbelstück mit dem längsten Werbetext…" steht laut korrigierter Fassung innerhalb der Vertiefung, nicht als Abschnitts-Claim.

Ein Prüfskript hat jeden veröffentlichten Satz des Seeders gegen die Markdown-Datei gehalten: 0 Abweichungen, alle Überschriften der Vorlage vorhanden. Einzige bewusste Abweichung sind typografische Anführungszeichen („…“ statt „…").

1b · CABINET nach demselben Muster

Für CABINET lag keine Zulieferung vor. Die Seite ist nach dem Vorbild der beiden abgestimmten Texte von uns verfasst: Einleitung mit Claim, 8 Abschnitte, 3 davon mit Vertiefung, eigener Abschlussblock „Ihr Raum. Persönlich geplant." und die Widget-Beschriftung „Persönliche Schrankplanung vereinbaren".

⚠️ Bewusste Zurückhaltung: Der Text beschreibt ausschließlich die eigene Arbeitsweise – Aufmaß, Planung, Innenausstattung, Fronten, Montage, Schnittstelle zur Tischlerei. Er enthält keine Zahlen, Zertifizierungen oder Herstelleraussagen, weil es dafür keine Quelle gibt. Die Rotpunkt- und Febrü-Seiten stützen solche Aussagen auf die Quellenhinweise der Zulieferung; für CABINET wäre das erfunden. Der Text braucht Marcels Freigabe (Punkt in Abschnitt 8).

Nebenbei korrigiert: Febrüs Claim stand in CmsBrandSeeder und b2in.php weiter als „Arbeiten, wo man gerne ist." – die Zulieferung schreibt „…gerne bleibt."

2 · „BEISPIEL"-Kennzeichnung entfernt

Die Demobilder trugen den Vermerk „BEISPIEL – " in der Bildunterschrift, damit ein Stockmotiv nicht versehentlich als echte Referenz durchgeht. Der Vermerk ist entfallen.

Zwei Stellen waren nötig, weil die Bilder auf Vorschau und Live bereits in der Datenbank stehen:

  • DemoBrandGallerySeeder – Bildunterschriften ohne Vermerk (für frische Installationen).
  • BrandGalleryCaptionNormalizerSeeder (neu) – entfernt den Vermerk in vorhandenen Daten. Fasst ausschließlich den Text an; Bild, Kategorie und Reihenfolge bleiben. Mehrfaches Ausführen ist unbedenklich.

⚠️ Die Unterscheidung hängt damit allein an der Kategorie (Badge „Realisiertes Projekt" / „Visualisierung"). Die Demobilder der Kategorie project sind Stockmotive und KI-Renderings – sie behaupten jetzt ohne den Vermerk, echte Referenzen zu sein. Vor dem Livegang ersetzen oder auf „Inspiration" umstellen (Punkt in Abschnitt 8).

3 · Hinweis zu Visualisierungen

Wo Vorher Jetzt
Markengalerie, Rubrik Inspirationen „Diese Darstellungen sind Entwürfe und Visualisierungen – keine ausgeführten Projekte." „Entwürfe und Visualisierungen. Sie zeigen gestalterische Möglichkeiten, noch bevor das erste Möbel steht."
Markengalerie, Einleitung „…und Visualisierungen, die zeigen, was möglich ist." „…und Visualisierungen, die zeigen, was in einem Raum möglich ist."
/referenzen, Seitenkopf „…keine Entwürfe und keine Visualisierungen." „Hier zeigen wir ausschließlich Arbeiten, die wir tatsächlich ausgeführt haben."
/referenzen, Projektliste „…finden Sie bewusst getrennt davon auf den Markenseiten." „…zeigen wir bewusst getrennt davon – auf den Markenseiten."

Die Trennung bleibt inhaltlich unverändert, sie wird nur nicht mehr über eine Verneinung transportiert. Der Badge am Bild („Visualisierung") trägt die Unterscheidung ohnehin.

4 · Abschluss-CTA

Der Block war eine einfarbige Fläche mit Überschrift, Text, Button und einem unterstrichenen Textlink. Neu:

  • Eine schmale Linie über der Überschrift setzt den Abschluss ab.
  • Hauptbutton weiß auf Markenfarbe, mit Pfeil, der beim Hovern mitgeht.
  • Die Herstellerseite ist jetzt ein gleichwertiger Outline-Button statt eines Textlinks.
  • Der Showroom steht als kleine Zeile mit Pin-Symbol darunter.

Nachgebessert (dritte Rückmeldung): Nach dem Build stand der Abschluss hellgrau mit weißer Schrift da. Ursache: .variante-glass-flow section in shared-styles.css färbt jede Sektion in diesem Wrapper hell ein und ist mit (0,1,1) spezifischer als .cta-band mit (0,1,0). Für .bg-secondary und .bg-accent gibt es dort längst Ausnahmen – die neue Klasse brauchte dieselbe. /marken/{slug} hängt in genau diesem Wrapper, deshalb fiel es erst dort auf. Der Verlauf steht jetzt in --cta-band-gradient und wird von beiden Regeln benutzt, damit er nicht doppelt gepflegt werden muss. Ein Test hält die Ausnahme fest.

Nachgebessert (zweite Rückmeldung): Der erste Versuch legte zwei weiche Kreise (blur-3xl) über das Hellblau – die wirkten als sichtbare Flecken, besonders der dunkle rechts unten. Ersetzt durch einen durchgehenden Verlauf über die volle Breite: .cta-band in shared-styles.css, von Anthrazit (--primary-dark) diagonal in das Dynamic Blue (--secondary-dark). Die Tiefe kommt aus dem Verlauf selbst, die Kontur aus zwei Haarlinien – eine oben über die Breite, eine als Auftakt über der Überschrift, beide zu den Seiten auslaufend. Buttons und Fußzeile dabei zurückhaltender: dünnerer Rahmen, weicherer Schatten.

Der Verlauf liegt bewusst als CSS-Klasse vor und nicht als Utility-Kette im Blade: er arbeitet mit den Theme-Variablen und bleibt so über alle Themes hinweg stimmig.

Die Textinhalte kommen unverändert aus dem CMS (cta_title / cta_text) – Punkt 4 der Liste war inhaltlich bereits durch die neuen Texte erledigt („Ihre Küche. Persönlich geplant.").

5 · Akkordeons weicher (Nachreichung aus derselben Runde)

Rückmeldung: Die Klappelemente springen zu schnell auf und zu, der Hover-Effekt ist zu stark.

Ursache war x-show mit einer Opacity-Bewegung: Das Element nimmt sofort seine volle Höhe ein und blendet nur den Inhalt ein – es sieht aus wie ein Sprung. Ersetzt durch eine Animation von grid-template-rows: 0fr → 1fr über 500 ms mit cubic-bezier(0.33, 1, 0.68, 1), die die tatsächliche Höhe bewegt. Das Alpine-Collapse-Plugin ist in diesem Projekt nicht registriert; diese Lösung kommt ohne neue Abhängigkeit aus.

  • Der zugeklappte Zustand steht auch statisch im class-Attribut, damit vor dem Start von Alpine nichts aufgeklappt aufblitzt. Die Objekt-Schreibweise von x-bind:class entfernt ihn beim Öffnen wieder.
  • aria-hidden hängt am Zustand, weil der Inhalt jetzt dauerhaft im DOM steht.
  • Hover: Die Flächenaufhellung (hover:bg-muted/30) ist raus. Geblieben ist ein feiner Farbwechsel der Überschrift auf die Sekundärfarbe. Im FAQ entfällt zusätzlich der Hintergrundwechsel am Icon-Kreis.
  • Der Pfeil dreht in derselben Dauer und Kurve wie das Aufklappen, damit beides zusammenläuft.

Betrifft beide Akkordeons – die Vertiefungen auf den Markenseiten und das FAQ; sie hatten dasselbe Verhalten.

Tests

BrandPageSectionsTest und BrandPageTest erweitert:

  • CABINET rendert seine Abschnitte und seinen eigenen Abschlussblock.
  • Widget-Beschriftung jetzt für alle drei Marken (Datensatz-Test).
  • Rotpunkt: der zusammengeführte Abschnitt existiert, der alte ist weg, der doppelte Satz steht genau einmal auf der Seite.
  • Febrü: der Blaue-Engel-Satz behält seinen Produktbezug.
  • Der Rückfall auf den generischen Abschlussblock funktioniert weiterhin – beide Sprachen müssen dafür leer sein, sonst greift die Übersetzungs-Fallbackkette und der englische Text erscheint auf der deutschen Seite.
  • Demogalerien tragen keinen „BEISPIEL"-Vermerk mehr, dafür durchgehend eine Kategorie.
  • Der Normalizer entfernt den Vermerk, ohne Bild, Kategorie oder Reihenfolge zu verändern.
  • Jede Vertiefung rendert zugeklappt und mit der Höhen-Animation; die alte Sprung-Bewegung ist weg.
  • Weder Markenseite noch FAQ verwenden noch den Flächen-Hover.
  • Der Abschlussblock trägt das Verlaufsband; die weichen Kreise sind weg.
  • Die Ausnahme für .variante-glass-flow ist vorhanden – ohne sie steht der Abschluss hellgrau da.

php artisan test --compact: 863 grün (3194 Assertions), Pint sauber, npm run build:b2in gebaut.


4. Nicht Bestandteil dieses Auftrags ⏸

Referenzbereich voll ausgebaut · 16:9-Display-Modul · Immobilien-CMS · Dubai-Map · Markenrichtlinie Fahrzeug/Print · weitere frei anlegbare Rubriken · eigene Videolösung. Zusätzlich neu zu bewerten: B2 (Magazin-CMS ans Frontend anschließen, ≈2–3 Std.) und B8 (Passwort aus dem Seeder).


5. Abhängigkeiten

AP1 (Struktur, Umtextung, B1/B3/B4/B5/B6/B7)
  ├─ AP2 (Kernmarken, B9) ──┐
  ├─ AP4 (Display-Spiegelung)│
  ├─ AP5 (Referenzen)        │
  ├─ AP6 (Sponsoring) ◄──────┤ braucht B9
  └─ AP3 (Termin-Widget) ◄───┘ Markenauswahl braucht CmsBrand

B1 zuerst. Solange der Seeder überschreibt, ist jede redaktionelle Arbeit flüchtig.


6. Restrisiken & offene Punkte

Punkt Stand Umgang
Rechtstexte nennen weiter „Connecting Design and Property", Dubai/Lissabon, Supply-Chain ⚠️ offen Formulierung freigeben lassen, dann eintragen – nicht selbst getextet
OG-Bild fehlt komplett (b2in-og-default.jpg existiert nicht) ⚠️ offen Motiv wählen, ablegen
WhatsApp-Nummer (+49 151 51002992?) offen bestätigen lassen; ohne Nummer bleibt der Button unsichtbar
Logos, Stimmungsbilder, Galeriefotos der drei Marken offen Platzhalter stehen, Marcel trägt im CMS nach
Zwei neue Magazin-Beiträge sind Platzhalter offen gegenlesen lassen
Sponsoren-Kurztexte sind Entwürfe offen gegenlesen lassen
Vier Dubai-Beiträge bleiben im Magazin bewusst gehören zum Immobilienbereich, der bestehen bleibt
ecosystem_stats behauptet „1,7K+ Partner", „510+ Projekte", „98 %" ruhend Sektion wird auf keiner erreichbaren Seite gerendert – vor einer Wiederverwendung prüfen
B1/B2 Entscheidung ✅ erledigt Redaktionsschutz im Seeder, Magazin am Frontend
Logo mit Zusatz „Interior" ✅ erledigt Zulieferung eingetroffen und zugeschnitten
Sponsoring-Logos uneinheitlich ✅ erledigt Kacheln mit weißer Bühne, JPG ohne Transparenz fällt nicht mehr auf

Spielregel aus der Umsetzungsliste: eine Korrekturschleife je Bereich ist enthalten. Weitere Runden vorher melden und beziffern.


7. Definition of Done

  • Alle Live-Seiten laden (LiveSiteReviewTest grün)
  • Marcel kann Marke anlegen, Logo per Picker setzen, Galerie pflegen – und die Änderung erscheint auf der Website (Gegenprobe zu B2, getestet in CmsBrandsIndexTest)
  • Erneuter CmsContentSeeder-Lauf löscht keine redaktionellen Inhalte (Gegenprobe zu B1)
  • ./vendor/bin/pint
  • npm run build:b2in (Build-Verzeichnis build/b2in)
  • php artisan test --compact vollständig grün — 810 Tests, 3037 Assertions
  • Keine Treffer mehr für „Supply-Chain", „Local for Local", „Ökosystem", „Connecting Design" auf den Live-Seiten. Stand: 8 statt ursprünglich 90. Davon 5 in /magazin (die vier Dubai-Beiträge, bleiben bewusst), 1 in /privacy und 2 in /terms (Rechtstexte, siehe Abschnitt 0, Schritt 2). Erst abhakbar, wenn die Rechtstexte freigegeben sind.
  • OG-Bild vorhanden
  • Rechtstexte freigegeben und eingetragen

7a. Vorschau-Instanz für die Abstimmung

Für die erste Abstimmung läuft der neue Stand als eigenständige Vorschau unter preview.b2in.eu – eigenes Verzeichnis, eigene .env, eigene Datenbank. Live bleibt unangetastet.

Ablauf Schritt für Schritt: dev/2026-08-28/preview-deployment.md. Dateien dafür: dev/2026-08-28/deploy/b2in-interior-update.tar.gz (geänderte Dateien plus gebaute Assets) und deploy/dateiliste.txt.

Zwei Punkte, die dabei leicht übersehen werden:

  • Die Vorschau braucht DOMAIN_B2IN=preview.b2in.eu in ihrer .env. Der ThemeServiceProvider setzt URL::forceRootUrl() auf die konfigurierte Domain; ein unbekannter Host fällt auf APP_URL zurück. Ohne diesen Eintrag rendert die Vorschau zwar, aber jeder interne Link springt auf die Live-Seite.
  • Eigene Datenbank, keine geteilte. InteriorRelaunchCmsSeeder überschreibt 23 CMS-Sektionen – auf der Live-DB ausgeführt wäre der alte Stand weg.

Neu dafür im Code: config('app.indexable'). Außerhalb von APP_ENV=production liefert jede Seite noindex, nofollow, damit eine Vorschau nicht mit dem Original um Suchergebnisse konkurriert. Über SITE_INDEXABLE einzeln steuerbar.


8. Deployment-Checkliste (Livegang)

Diese Liste beim Livegang von oben nach unten abarbeiten. Sie wird mit jedem Arbeitspaket ergänzt – zuletzt aktualisiert nach AP1 und der Display-Umstellung.

8.1 Vorher

  • DB-Backup ziehen. Die Seeder unter 8.2 schreiben in flux_cms_contents, display_versions und display_version_items.
  • .env prüfen: SEED_ADMIN_NAME, SEED_ADMIN_EMAIL, SEED_ADMIN_PASSWORD gesetzt (neu seit B8 – vorher stand das Passwort im Repo).
  • Wartungsfenster mit Marcel abstimmen: die Displays holen ihre Konfiguration zyklisch, ein Neustart ist nicht nötig, aber ein sichtbarer Wechsel mitten im Ladengeschäft schon.

8.2 Deployment

php artisan migrate --force   # legt u. a. cms_brands an (AP2)

# 0. Kernmarken anlegen (CABINET, Rotpunkt Küchen, Febrü).
#    Überschreibt nichts – bereits gepflegte Marken bleiben unberührt.
php artisan db:seed --class=CmsBrandSeeder --force

# 1. Neue Texte ins CMS. Überschreibt GENAU die 19 in AP1 geänderten Sektionen,
#    alles andere bleibt unangetastet.
php artisan db:seed --class=InteriorRelaunchCmsSeeder --force

# 2. Display-Branding auf das Interior-Logo vereinheitlichen (Logo je Theme + neuer Claim).
php artisan db:seed --class=DisplayInteriorBrandingSeeder --force

# 3. Medien-URLs host-unabhängig machen. Ohne diesen Schritt zeigen vier veröffentlichte
#    Playlists weiterhin auf b2in.test und laden auf dem Live-Display nicht.
php artisan db:seed --class=DisplayMediaUrlNormalizerSeeder --force

# 4. Ausführliche Markenseiten (CABINET, Rotpunkt, Febrü). Schreibt NUR tagline, description,
#    sections, widget_label, cta_title und cta_text – Bilder, Galerie, Leistungen und
#    Reihenfolge bleiben unberührt.
php artisan db:seed --class=BrandPageTextSeeder --force

# 5. Entfernt den Vermerk „BEISPIEL – “ aus vorhandenen Bildunterschriften (Nachtrag 5).
#    Fasst nur den Text an, mehrfaches Ausführen ist unbedenklich.
php artisan db:seed --class=BrandGalleryCaptionNormalizerSeeder --force

php artisan config:cache
php artisan view:clear

# Assets. WICHTIG: `build:web` baut nur die vier Frontend-Themes, NICHT das Portal.
# Fehlt public/build/portal/manifest.json, liefert das Admin-Portal 500.
npm ci
npm run build:web
npm run build:portal

Danach prüfen, dass alle fünf Manifeste liegen:

for d in b2in b2a stileigentum style2own portal; do
  test -f "public/build/$d/manifest.json" && echo "$d ok" || echo "$d FEHLT"
done

8.3 Nachher prüfen

  • Startseite: Reihenfolge, Kernmarken-Anker #marken, Sponsoring-Logos
  • Markenseite /marken – Aufbau, Merkmale, „Alle Marken ansehen" von der Startseite
  • Kernmarken-Detailseiten /marken/cabinet, /marken/rotpunkt, /marken/februe – je mit ausführlichem Text, aufklappbaren Vertiefungen und eigenem Abschlussblock
  • Auf dem Smartphone prüfen: Das Terminanfrage-Widget trägt auf der Markenseite die längere Beschriftung und darf keine Inhalte verdecken
  • /netzwerk, /interior, /partner, /service leiten auf /marken
  • Im Admin unter „Kernmarken" eine Marke ändern und prüfen, dass es auf der Seite ankommt
  • Ankündigungsleiste erscheint (Dismiss-Key hängt seit B3 am Inhalt – wer die alte Leiste weggeklickt hatte, sieht die neue wieder)
  • Logo in Header, Footer und in einer Test-Mail
  • Alte Magazin-URLs /magazin/1 … /magazin/5 leiten per 301 auf den Slug
  • /service und /portfolio leiten um statt 500 zu liefern
  • Die vier Displays sichten: „Display 4 – Innen", „Kundenstopper", „Display 2", „Test-Display". Besonders das Modul „Cabinet-Influencerin" – dessen Video lag auf b2in.test. Ob die Datei auf dem Live-Server überhaupt existiert, ließ sich lokal nicht prüfen; fehlt sie, muss sie nachgeladen werden.
  • Search Console: die alten Magazin-URLs waren indexiert, die 301 fängt sie ab
  • Search Console: /netzwerk war indexiert und leitet jetzt auf /marken

8.4 Zurückrollen

Die Seeder sind nicht umkehrbar. Rückweg ist das Backup aus 8.1 – für die betroffenen Tabellen genügt ein selektives Zurückspielen von flux_cms_contents, display_versions und display_version_items.

8.5 Nicht vergessen

  • Altes Seeder-Passwort rotieren, falls es anderswo benutzt wird – es steht weiterhin in der Git-Historie (B8).
  • WhatsApp-Nummer bestätigen lassen – erst dann erscheint der Button im Terminanfrage-Widget und in den CTA-Blöcken (CMS: global.contact.whatsapp).
  • CONTACT_FORM_RECIPIENT in der Live-.env prüfen: dorthin gehen auch die Terminanfragen aus dem Widget.
  • Datenschutzerklärung um einen Absatz zum WhatsApp-Link ergänzen, sobald die Nummer steht.
  • Rechtstexte: AGB (b2in_legal.php Zeile 117) und Datenschutzerklärung (Zeile 221) nennen weiter „Connecting Design and Property", Dubai/Lissabon und Supply-Chain-Management. Von Marcel prüfen lassen, dann eintragen.
  • Prüfen, welches Display auf der Website laufen soll (referenzen.display_feed.display_id). Ohne Eintrag zeigt die Seite das erste aktive Display mit veröffentlichter Playlist.
  • Die zwei neuen Magazin-Beiträge von Marcel gegenlesen lassen – sie sind Platzhalter.
  • Herstellerfreigabe für die Markentexte (Rotpunkt, Febrü) einholen – so verlangt es die Zulieferung selbst. Beide Fassungen nennen zusätzlich die EmpCo-Regeln (ab 27.09.2026): die Umweltaussagen vorher rechtlich prüfen lassen.
  • CABINET-Seitentext von Marcel freigeben lassen. Für CABINET lag keine Zulieferung vor; der Text ist von uns nach dem Muster der beiden abgestimmten Seiten verfasst und beschreibt bewusst nur die eigene Arbeitsweise (Nachtrag 5).
  • Febrü, Vertiefung „Konkrete Standards statt allgemeiner Versprechen“: Der Blaue Engel gilt nur für ausgewählte Tischsysteme und Stauraummöbel. Beim Redigieren darf daraus keine pauschale Aussage über das Gesamtsortiment werden.
  • Demobilder der Rubrik „Realisiertes Projekt" ersetzen. Der Vermerk „BEISPIEL" ist auf Kundenwunsch entfallen (Nachtrag 5); die Stockmotive und KI-Renderings aus DemoBrandGallerySeeder geben sich damit als echte Referenzen aus. Entweder durch echte Projektfotos ersetzen oder auf „Inspiration" umstellen.
  • Rotpunkt, Vertiefung „Zahlen, Zertifizierungen und Raumklima“: Die Aussage zur jährlich erhaltenen Waldfläche von ungefähr 50 Fußballfeldern darf laut Zulieferung nur mit ausdrücklicher Bestätigung veröffentlicht werden. Ohne Bestätigung den Absatz entfernen (Admin: Kernmarken → Rotpunkt → Abschnitte → Vertiefung).
  • OG-Bild anlegen: das Layout verweist auf img/assets/b2in-og-default.jpg, die Datei existiert nicht. Bis dahin zeigt jede geteilte Seite eine Vorschau ohne Bild.
  • Sponsoring-Logo tc-bad-waldliesborn-horn liegt nur als JPG vor, also ohne transparenten Hintergrund. Falls Marcel eine PNG-Variante hat, austauschen.
  • Markengalerien vor dem Livegang durchsehen: jedes Bild muss in der richtigen Rubrik stehen. Ohne gesetzte Kategorie erscheint ein Bild als „Visualisierung" – für ein echtes Projektfoto also bewusst „Realisiertes Projekt" wählen.