presseportale/docs/konzept/Entwicklungs-Konzept - Frontend-Komponenten Multi-Brand.md

509 lines
No EOL
20 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

**Datum:** 12. Mai 2026 **Status:** Technisches Implementierungs-Konzept **Tech-Stack:** Laravel 12+, Livewire 4 / Volt, Tailwind CSS (v4), Alpine.js (über Livewire) **Bezug:** Konzept-Update 3 (Multi-Brand-Architektur), Konzept-Update 4 (Positionierung), Brand-Landing-Konzept businessportal24
> **IST-Stand 21.05.2026**: Multi-Brand-Architektur ist umgesetzt
> (`config/domains.php`, `ThemeServiceProvider`, getrennte Vite-Builds
> `portal` + `web`). Die Hub-Migration des User Backends ist als
> eigene Roadmap in `dev/frontend/hub-flux/` dokumentiert (Phasen 07
> abgeschlossen, Phase 8 in Planung). Der hier beschriebene Brand-Context
> wird ueber `View::share()` global aufgeloest.
---
## 1. Leitprinzipien
Vier Regeln, an denen sich jede technische Entscheidung in diesem Dokument messen muss:
1. **Ein Codebase, viele Brands.** Kein Branch pro Portal, keine duplizierten Views. Differenzierung über Konfiguration, CSS-Variablen und gezielte View-Overrides.
2. **Brand-Awareness zentral aufgelöst, nicht in Komponenten verteilt.** Eine Komponente fragt nicht „bin ich auf businessportal24?". Sie konsumiert eine `$brand`-Context-Variable und rendert entsprechend.
3. **Livewire/Volt nur wo nötig.** Statische Komponenten bleiben pures Blade. Reaktivität ist ein Kostenfaktor (Server-Roundtrips, Hydration, State-Management) sie muss verdient werden.
4. **Solo-tauglich heißt: jede Entscheidung muss in 6 Monaten noch verständlich sein.** Lieber explizit als clever.
## 2. Brand-Auflösung (Multi-Tenant-Pattern)
### Brand-Resolution-Pipeline
```
Request → Middleware → BrandResolver → Brand-Context im Container
→ View-Pfad-Override
→ Config-Override
→ Layout-Auswahl
```
**Schritt 1: Domain-Mapping**
Die `brands`-Tabelle aus Update 3 enthält pro Brand mindestens:
- `slug` (z.B. `businessportal24`, `presseecho`, `hub`)
- `primary_domain` (z.B. `businessportal24.com`)
- `theme_key` (z.B. `bp24`, `pe`) Verweis auf CSS-Token-Set
- `config_path` (z.B. `brands/businessportal24.php`)
- `is_publisher_hub` (boolean)
-ist zu prüfen, teils schon im System angelegt!
**Schritt 2: Middleware**
```php
// app/Http/Middleware/ResolveBrand.php
public function handle(Request $request, Closure $next): Response
{
$brand = Cache::rememberForever(
"brand.domain.{$request->getHost()}",
fn() => Brand::query()
->where('primary_domain', $request->getHost())
->orWhereJsonContains('aliases', $request->getHost())
->firstOrFail()
);
app()->instance(Brand::class, $brand);
View::share('brand', $brand);
Config::set('brand', $brand->config());
return $next($request);
}
```
Cache ist hier wichtig die Domain-zu-Brand-Auflösung passiert bei jedem Request. `rememberForever` mit explizitem Cache-Bust beim Brand-Update.
-ist zu prüfen, teils schon im System angelegt!
**Schritt 3: Brand im Container**
Jede Klasse kann via Dependency Injection auf die aktuelle Brand zugreifen:
```php
public function __construct(private Brand $brand) {}
```
In Blade-Templates ist `$brand` durch `View::share()` direkt verfügbar.
### Lokale Entwicklung
Lokal arbeiten mit `.test`-Domains in Docker (devserver) auf dem Server via Treafik:
- `businessportal24.test`
- `presseecho.test`
- `pressekonto.test`
Alle zeigen auf dieselbe Codebase, die Middleware löst per Hostname auf. Keine Subdomains, keine Port-Tricks schmerzfreies lokales Multi-Brand-Setup.
## 3. Theming-System (Tailwind v4 + CSS Custom Properties)
### Empfehlung: Tailwind v4
Falls die Migration auf v4 noch offen ist: **jetzt machen**. Die `@theme`-Direktive in v4 macht Multi-Brand-Theming dramatisch einfacher als das v3-Config-Konstrukt. Native CSS-Variablen, keine PostCSS-Akrobatik mehr.
### Token-Architektur
Drei Ebenen (Beispiel ):
```css
/* Ebene 1: Globale Design-Tokens (markenneutral) */
@theme {
--font-serif: 'Source Serif 4', Georgia, serif;
--font-sans: 'Inter', system-ui, sans-serif;
--spacing-section: 5rem;
--spacing-section-tight: 3rem;
--radius-card: 2px; /* fast keine Rundungen, editorial */
}
/* Ebene 2: Semantische Tokens (markenneutral, aber rollenbasiert) */
:root {
--color-text-primary: var(--brand-text);
--color-text-muted: var(--brand-text-muted);
--color-surface: var(--brand-surface);
--color-accent: var(--brand-accent);
--color-cta-bg: var(--brand-cta-bg);
--color-cta-fg: var(--brand-cta-fg);
--color-hub-transition: var(--brand-hub-bg);
}
/* Ebene 3: Brand-spezifische Werte */
[data-brand="businessportal24"] {
--brand-text: #1a1a1a;
--brand-text-muted: #6b6b6b;
--brand-surface: #fafaf7; /* warmer off-white */
--brand-accent: #d94e1f; /* gedämpftes Orange */
--brand-cta-bg: #d94e1f;
--brand-cta-fg: #ffffff;
--brand-hub-bg: #1a2540; /* dunkelblau, Störer */
}
[data-brand="presseecho"] {
--brand-text: #f0f0e8;
--brand-text-muted: #a0a098;
--brand-surface: #1f2620; /* dunkelgrün-anthrazit */
--brand-accent: #5a8a6b; /* gedämpftes Grün */
--brand-cta-bg: #5a8a6b;
--brand-cta-fg: #ffffff;
--brand-hub-bg: #1a2540; /* Hub-Farbe bleibt konstant! */
}
```
### Brand-Aktivierung im Layout
```blade
{{-- resources/views/layouts/brand.blade.php --}}
<!DOCTYPE html>
<html lang="de" data-brand="{{ $brand->slug }}">
<head>
@vite(['resources/css/app.css', 'resources/js/app.js'])
{{-- Brand-CSS wird im app.css via @import geladen, oder optional separat: --}}
@if($brand->has_custom_css)
<link rel="stylesheet" href="{{ asset("themes/{$brand->slug}.css") }}">
@endif
</head>
<body class="bg-[var(--color-surface)] text-[var(--color-text-primary)]">
{{ $slot }}
</body>
</html>
```
**Wichtig:** Der `data-brand`-Attribut auf `<html>` ist der einzige Hebel, der den gesamten Look umschaltet. Alle Tailwind-Utilities, die brand-spezifische Werte nutzen, greifen über CSS-Variablen darauf zu.
### Pragmatische Tailwind-Nutzung
Die Komponenten schreiben **nicht** `bg-orange-600` (das wäre brand-spezifisch im Markup festgenagelt). Stattdessen:
```blade
<button class="bg-[var(--color-cta-bg)] text-[var(--color-cta-fg)] hover:opacity-90">
Mitteilung einreichen
</button>
```
Oder noch sauberer mit eigenen Tailwind-Utility-Klassen, die in `app.css` definiert werden:
```css
@layer components {
.btn-cta {
@apply bg-[var(--color-cta-bg)] text-[var(--color-cta-fg)]
px-6 py-3 rounded-sm font-medium hover:opacity-90 transition;
}
.btn-hub {
@apply bg-[var(--color-hub-transition)] text-white
px-8 py-6 block;
}
}
```
So bleibt das Markup brand-agnostisch und die Stilfragen zentralisiert.
## 4. Komponenten-Hierarchie und Engine-Wahl
### Drei Render-Modi, drei Anwendungsbereiche
|Modus|Wann verwenden|Performance|Beispiele|
|---|---|---|---|
|**Blade Component**|Statisches Markup, keine Interaktion|⚡⚡⚡|TopBar, Footer, PressItem, StatsRow|
|**Volt (Single-File)**|Lokaler State, einfache Reaktivität, Lifecycle einfach|⚡⚡|AdHocTicker, HeroSlider, Search|
|**Klassisches Livewire**|Komplexe Komponenten mit Services, Events, mehrere Methoden|⚡|PressEditor, NewsroomDashboard|
### Volt: konkrete Empfehlung
Volt ist für dieses Projekt **die richtige Wahl als Default für reaktive Komponenten** aber nicht für statische. Die Gründe:
**Pro Volt:**
- Single-File-Komponenten: PHP-Logik + Blade-Template + Tailwind-Klassen in einer Datei. Solo-Entwickler-Freundlichkeit ist hoch.
- Funktionale API ist deutlich weniger Boilerplate als klassische Livewire-Klassen.
- Volt-Komponenten lassen sich genau wie Livewire-Komponenten lazy-laden (`<livewire:lazy ...>`), was für Above-the-fold-Performance wichtig ist.
**Kontra Volt:**
- Für reine Display-Komponenten ist Volt overkill. Eine `<x-press-item>` ohne State soll keine Livewire-Komponente sein Hydration und Wire-Tracking sind unnötige Kosten.
- Wenn eine Komponente Services injiziert, ein eigenes Test-Setup braucht oder mehr als ~150 Zeilen wächst, ist eine klassische Livewire-Klasse besser strukturierbar.
**Faustregel:**
> Renderst du HTML ohne Server-Interaktion? → Blade Component. Brauchst du `wire:model`, `wire:click`, Polling oder reaktiven State? → Volt. Wird die Komponente komplex, hat Services, eigene Tests? → Klassisches Livewire.
### Konkrete Komponenten-Inventur aus den Screens
Aufgeschlüsselt nach Engine. Das ist die direkte Übersetzung der Screens in technische Bausteine:
#### Blade-Komponenten (statisch, hochfrequent wiederverwendet)
```
<x-brand.top-bar> -- Wirtschafts-Ticker, Sprachen, Newsletter/RSS
<x-brand.header> -- Logo, Suche, CTAs
<x-brand.rubriken-nav> -- Hauptnavigation (Wirtschaft, Tech, Finanzen...)
<x-brand.footer> -- Footer mit Cross-Brand-Hinweis
<x-press.item> -- Standard-Listen-Eintrag mit Slots für Varianten
<x-press.item-hero> -- Große Hero-Variante
<x-press.item-sidebar> -- Kompakte Sidebar-Variante (mit Nummerierung)
<x-press.byline> -- Quelle · Zeit · Lesezeit (wiederverwendbar)
<x-ui.section-header> -- "§ 01" + Label + H2 (das Editorial-Pattern)
<x-ui.stats-row> -- Drei-/Vier-Spalten-Statistik
<x-ui.button> -- CTA-Button mit Varianten (primary, secondary, hub)
<x-ui.badge> -- Branchen-Marker, "Geprüft"-Label
<x-hub.transition-block> -- DER dunkelblaue Störer (siehe Briefing)
<x-hub.cta> -- Inline Hub-CTA (Variante des Störers)
<x-quality.standard-footer> -- "Alle Pressemitteilungen werden geprüft..."
```
#### Volt-Komponenten (reaktiv, isolierter State)
```
<livewire:ad-hoc-ticker /> -- Auto-refresh alle 30s, Polling
<livewire:hero-slider /> -- 3 Top-Meldungen, auto-rotate mit Alpine
<livewire:press-search /> -- Suche im Header
<livewire:press-list-filter /> -- "Alle · Heute · Diese Woche" Tabs
<livewire:newsroom-list /> -- Newsroom-Sidebar mit "heute aktiv" Polling
<livewire:branchen-index /> -- Live-Werte mit ± Indikatoren
<livewire:termine-week /> -- Termine-Karussell mit Wochen-Navigation
```
#### Klassisches Livewire (komplex, services)
```
PressSubmissionForm -- Mehrstufige Einreichung (auf Hub)
NewsroomManager -- Profil-Verwaltung (auf Hub)
AdminReviewQueue -- Redaktions-Tool (auf Hub)
```
Auffällig: die **Brand-Portale brauchen kaum klassisches Livewire**. Das ist konsistent zur Architektur aus Update 3 Brand-Portale sind primär Lese-Oberflächen, der State liegt im Hub.
## 5. Brand-Differenzierung in Komponenten
Drei Mechanismen, in aufsteigender Eingriffstiefe:
### 5a. Konfiguration über Brand-Config
Der einfachste Fall: eine Komponente verhält sich anders je nach Brand-Konfiguration. (Beispiel)
siehe: config/domains.php
```php
return [
'name' => 'businessportal24',
'tagline' => 'Pressemitteilungen · DACH',
'press_item_layout' => 'timeline', // vs. 'topic'
'show_market_ticker' => true,
'show_branchen_index' => true,
'hero_variant' => 'top-meldung', // vs. 'topic-cluster'
'rubriken' => ['Wirtschaft', 'Technologie', /* ... */],
];
return [
'name' => 'presseecho',
'tagline' => 'Branchen-Pressearchiv',
'press_item_layout' => 'topic',
'show_market_ticker' => false,
'show_branchen_index' => false,
'hero_variant' => 'topic-cluster',
'rubriken' => [/* andere Reihenfolge, andere Schwerpunkte */],
];
```
Komponenten lesen daraus:
```blade
@if($brand->config('show_market_ticker'))
<x-brand.market-ticker />
@endif
<x-press.item :layout="$brand->config('press_item_layout')" :item="$item" />
```
**Das ist die häufigste Form der Differenzierung** und sie reicht für ~80 % aller Fälle.
### 5b. Slots und Defaults in Komponenten
Wenn eine Komponente strukturell gleich ist, aber Inhalte/Sprache abweichen:
```blade
{{-- resources/views/components/hub/transition-block.blade.php --}}
@props([
'title' => $brand->config('hub_cta.title') ?? 'Pressemitteilung einreichen',
'description' => $brand->config('hub_cta.description'),
'buttonText' => $brand->config('hub_cta.button') ?? 'Zum Publisher-Bereich',
])
<aside class="btn-hub flex flex-col gap-4">
<h3 class="text-xl font-medium">{{ $title }}</h3>
<p class="text-white/80">
{{ $description ?? $slot }}
</p>
<a href="{{ route('hub.submit', ['brand' => $brand->slug]) }}"
class="btn-cta inline-flex items-center gap-2 w-fit">
{{ $buttonText }}
<span aria-hidden="true">↗</span>
</a>
</aside>
```
Brand-Texte stehen in Config, Komponente bleibt eine.
### 5c. View-Override pro Brand (Eskalations-Pfad)
Für die seltenen Fälle, in denen eine Brand wirklich ein anderes Markup braucht: Laravel kann View-Pfade brand-spezifisch erweitern.
```php
// app/Providers/BrandServiceProvider.php
public function boot(): void
{
$this->app['view']->prependLocation(
resource_path("views/themes/{$brand->slug}")
);
}
```
Dann sucht Laravel View-Dateien zuerst unter `resources/views/themes/presseecho/components/press/item.blade.php`, dann unter dem Standard-Pfad. **Nur** für die Komponenten, die wirklich anders sein müssen, wird eine Override-Datei angelegt.
> **Disziplin-Regel:** View-Overrides sind die letzte Eskalationsstufe. Erst versuchen, mit Config + Slots auszukommen. Override-Dateien verdoppeln Wartungsaufwand jeder Bugfix muss mehrfach gemacht werden.
## 6. Datei-Struktur (Beispiel, siehe akutelle Struktur und optimiere falls nötig )
```
app/
├── Brand/
│ ├── Brand.php # Eloquent Model
│ ├── BrandManager.php # Service, im Container
│ └── BrandResolver.php # Domain → Brand
├── Http/
│ └── Middleware/
│ └── ResolveBrand.php
├── Livewire/
│ ├── Brand/ # Brand-Portal-spezifisch
│ │ ├── AdHocTicker.php
│ │ ├── HeroSlider.php
│ │ └── PressSearch.php
│ └── Hub/ # Hub-spezifisch
│ ├── PressSubmissionForm.php
│ └── NewsroomManager.php
├── View/
│ └── Components/
│ ├── Brand/
│ ├── Press/
│ ├── Hub/
│ ├── Ui/
│ └── Quality/
└── Providers/
└── BrandServiceProvider.php
config/
└── brands/
├── businessportal24.php
├── presseecho.php
└── hub.php
resources/
├── css/
│ ├── app.css # Tailwind base + semantische Tokens
│ └── themes/
│ ├── businessportal24.css # Brand-Tokens (optional separat)
│ └── presseecho.css
├── js/
│ └── app.js
└── views/
├── layouts/
│ ├── brand.blade.php # Brand-Portal-Layout
│ └── hub.blade.php # Hub-Layout
├── components/ # Standard-Komponenten
│ ├── brand/
│ ├── press/
│ ├── hub/
│ ├── ui/
│ └── quality/
├── livewire/ # Volt-Komponenten
│ ├── ad-hoc-ticker.blade.php
│ ├── hero-slider.blade.php
│ └── press-search.blade.php
├── pages/ # Konkrete Seiten-Templates
│ ├── home.blade.php
│ └── veroeffentlichen.blade.php
└── themes/ # NUR Brand-Overrides
└── presseecho/
└── components/
└── ... # nur was wirklich anders ist
```
## 7. Performance-Strategie
Vier konkrete Hebel, die in dieser Reihenfolge ausgeschöpft werden:
**1. Aggressive View-Caching für statisches Markup.** Press-Listen, Newsroom-Sidebars, Statistik-Zeilen können mit Tag-basiertem Cache gepuffert werden. Neue Mitteilung → relevante Tags invalidieren.
```php
Cache::tags(['press_list', "brand.{$brand->slug}"])
->remember('home.aktuelle-meldungen', now()->addMinutes(5), fn() => /* ... */);
```
**2. Volt-Komponenten lazy laden, wo sinnvoll.** Below-the-fold-Komponenten (Branchen-Index, Termine, Newsroom-Liste) als `lazy`:
```blade
<livewire:branchen-index lazy />
```
Sie laden erst beim Scroll, blockieren nicht das initiale Render.
**3. Asset-Pipeline: ein Bundle, alle Brands.** Über die CSS-Variablen-Strategie ist kein Per-Brand-Build nötig. Ein Vite-Build, der für alle Brands gilt. Spart Komplexität und Cache-Invalidierung.
**4. Brand-Resolution cachen.** Die Domain-zu-Brand-Auflösung ist `rememberForever` (siehe Middleware). Cache-Bust nur beim Brand-Update über Model-Observer.
## 8. Migration der Bestands-Inhalte
Quer zu allem oben: die ~100.000 Bestandsmitteilungen sind im neuem System migriert! Drei Punkte, die das Komponenten-Design beeinflussen:
- **`<x-press.item>` muss tolerant gegenüber unvollständigen Daten sein.** Alte Mitteilungen haben evtl. keine Lesezeit-Schätzung, keine Branchen-Zuordnung, keine sauberen Bilder. Komponente rendert auch dann sauber.
- **Permalink-Stabilität.** Die alte URL-Struktur muss erhalten bleiben (Strategie-Dokument: Tombstone-Modell). Das ist ein Routing-Thema, kein Komponenten-Thema aber die Komponenten dürfen keine URLs hardcoden, sondern nur `route()`-Helpers nutzen.
- **Brand-Zuordnung der Bestände.** Wie in Update 3 festgelegt: am Start ist jede Mitteilung beiden Brands zugewiesen. Komponenten brauchen dafür keine Sonderlogik sie filtern nach Brand-Kontext, und der Pool ist eben (am Anfang) für beide Brands derselbe.
## 9. Entwicklungs-Reihenfolge (Empfehlung)
Konkrete Bauplan-Sequenz, die früh nutzbare Ergebnisse liefert:
**Sprint 1 Fundament**
- Brand-Model, Middleware, Resolver
- Theming-Setup (Tailwind v4, CSS-Variablen, zwei Brand-Themes)
- Layout `brand.blade.php`
- Grundlegende UI-Komponenten (`button`, `badge`, `section-header`)
**Sprint 2 Statisches Markup für businessportal24**
- TopBar, Header, RubrikenNav, Footer
- `<x-press.item>` und seine Varianten
- StatsRow, QualityStandardFooter
- Statische Version der Veröffentlichen-Landing (ohne Reaktivität)
**Sprint 3 Reaktive Komponenten**
- AdHocTicker (Volt + Polling)
- HeroSlider (Volt + Alpine)
- PressList mit Filter-Tabs
- Hub-Transition-Block mit Cross-Domain-Auth-Übergabe
**Sprint 4 Hub-Anbindung**
- Sanctum-Setup für Cross-Domain
- Hub-Routing für `?brand=businessportal24`-Parameter
- Einreichungs-Flow im Hub (klassisches Livewire)
**Sprint 5 Zweite Brand aufschalten**
- `presseecho.test` lokal
- Brand-Config für presseecho
- Erste Override-Komponente: `topic-cluster`-Hero
- Testen: was funktioniert ohne Override, was braucht eines?
Ab Sprint 5 wird die eigentliche Stresstest-Frage beantwortet: **Hält die Architektur, wenn die zweite Brand wirklich anders aussehen soll?** Wenn an Sprint 5 viele Overrides nötig werden, ist die Config-Schicht zu dünn dann iterieren.
## 10. technische Punkte
- **Tailwind v4:** wenn das Projekt noch nicht migriert ist, sollte das _vor_ Sprint 1 entschieden werden. v4 macht das CSS-Variablen-Setup deutlich eleganter.
- **Sanctum-Cookie-Domain für Cross-Domain-Auth:** Detail aus Update 3, muss vor Sprint 4 final geklärt sein. Same-Site-Strategie, SPA-Mode oder klassischer Token-Flow?
- **CDN/Asset-Hosting:** Brand-Bilder, Press-Item-Fotos kommen vom Hub
- **Translations:** DACH-Sprachschalter auf der Startseite (de (ohne parameter) / de-at / de-ch / en) ist eine reine Inhalts-Filterung. Bei Mehrsprachigkeit de/en: i18n-Setup
- **Polling-Frequenzen:** AdHocTicker, Newsroom-Liste wie oft refreshen, ohne dass die Server-Last bei wachsendem Traffic problematisch wird? Anfangswerte: Ticker 30s, Newsroom 60s, Branchen-Index 5 min.
---
_Dieses Konzept ist die technische Brücke zwischen Architektur (Update 3), Positionierung (Update 4) und Implementation. Es legt fest, wie Komponenten strukturiert werden, damit die Brand-Differenzierung skaliert ohne in eine Codebase-Duplikation zu kippen. Anpassungen sollten dokumentiert und mit den Update-Dokumenten abgeglichen werden._