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

20 KiB
Raw Permalink Blame History

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

// 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:

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 ):

/* 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

{{-- 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:

<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:

@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


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:

@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:

{{-- 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.

// 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.

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:

<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.