# Flux CMS — Technische Architektur ## Datenfluss ``` ┌───────────────────────────────────────────────────────┐ │ Blade Template │ │ {{ cms('welcome.hero.heading') }} │ │ {{ cms_media_url('welcome.hero.image', 'hero') }} │ │ {{ media_url('file.webp', 'card') }} │ └────────────────────┬──────────────────────────────────┘ │ ┌───────────▼───────────────┐ │ Helper-Funktionen │ │ cms(), cms_media_url(), │ │ media_url(), tcms() │ │ (app/helpers.php) │ └───────────┬───────────────┘ │ ┌───────────▼───────────────┐ │ CmsContentService │ Singleton via ServiceProvider │ get($key, $replace) │ └───────────┬───────────────┘ │ ┌─────────▼─────────┐ │ Cache Layer │ Cache::remember() pro Gruppe │ (config TTL) │ Auto-Invalidierung bei Admin-Save └─────────┬─────────┘ │ ┌─────────▼─────────┐ ┌──────────────────┐ │ CmsContent Model │ │ __($key) │ │ (DB Lookup) ├─NO──► Fallback zu │ │ Übersetzbar │ │ Laravel Lang │ └───────────────────┘ └──────────────────┘ ``` ## Medien-Datenfluss ``` ┌────────────────────────────────────────────────────────────┐ │ Blade Template │ │ {{ media_url('datei.webp', 'hero') }} │ └─────────────────────┬──────────────────────────────────────┘ │ ┌───────────▼────────────────┐ │ media_url() Helper │ │ 1. In-Memory-Cache prüfen │ │ 2. CmsMedia::where(...) │ │ 3. Conversion vorhanden? │ │ 4. Fallback: asset(...) │ └───────────┬────────────────┘ │ ┌────────────▼────────────────┐ │ CmsMedia Model │ │ - getUrl() │ │ - getConversionUrl($name) │ │ - hasThumbnail() │ │ - isPdf() / isImage() │ └────────────┬────────────────┘ │ ┌────────────▼────────────────┐ │ Storage (public Disk) │ │ cms/media/originals/ │ │ cms/media/conversions/ │ │ cms/media/thumbnails/ │ └─────────────────────────────┘ ``` ### media_url() im Detail ```php function media_url(?string $filename, string $profile = ''): string { // 1. Null/Leer → leerer String // 2. In-Memory-Cache: $resolved[$cacheKey] // 3. CmsMedia::where('filename', $filename)->first() // 4. Wenn $profile und Conversion existiert → getConversionUrl($profile) // 5. Ohne Profil → getUrl() (Original) // 6. Fallback: asset('assets/images/' . $filename) } ``` ### cms_media_url() im Detail ```php function cms_media_url(string $key, string $profile = ''): string { // 1. cms($key) → Dateiname aus CmsContent // 2. media_url($filename, $profile) } ``` ## Key-Resolution Der `cms()`-Aufruf `cms('welcome.hero.heading')` wird so aufgelöst: 1. **Parsing**: `welcome` → `group`, `hero.heading` → `key` 2. **Cache-Lookup**: `flux_cms.content.welcome` (alle Einträge der Gruppe) 3. **Key-Match**: `firstWhere('key', 'hero.heading')` 4. **Übersetzung**: `getTranslation('value', app()->getLocale())` mit Fallback auf `config('app.fallback_locale')` 5. **Platzhalter**: `:highlight` → Ersetzung aus `$replace` Array 6. **Fallback**: Wenn nichts in DB → `__('welcome.hero.heading', $replace, $locale)` ## Datenbankschema ### flux_cms_contents ```sql CREATE TABLE flux_cms_contents ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, `group` VARCHAR(255) NOT NULL, `key` VARCHAR(255) NOT NULL, type VARCHAR(255) DEFAULT 'text', -- text, html, image, json, link value JSON NOT NULL, -- {"de": "...", "en": "..."} `order` INT DEFAULT 0, created_at TIMESTAMP NULL, updated_at TIMESTAMP NULL, UNIQUE INDEX (group, key) ); ``` ### flux_cms_media ```sql CREATE TABLE flux_cms_media ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, filename VARCHAR(255) NOT NULL, -- Original-Dateiname path VARCHAR(512) NOT NULL, -- Storage-Pfad type VARCHAR(50) DEFAULT 'image', -- image, pdf, document mime_type VARCHAR(255) NULL, file_size BIGINT UNSIGNED NULL, original_width INT UNSIGNED NULL, original_height INT UNSIGNED NULL, disk VARCHAR(50) DEFAULT 'public', collection VARCHAR(255) NULL, -- Optionale Sammlung conversions JSON NULL, -- {"hero": "path/...", "card": "path/..."} title JSON NULL, -- Übersetzbar alt_text JSON NULL, -- Übersetzbar is_published BOOLEAN DEFAULT TRUE, created_at TIMESTAMP NULL, updated_at TIMESTAMP NULL, INDEX (filename), INDEX (type), INDEX (collection) ); ``` ### flux_cms_downloads ```sql CREATE TABLE flux_cms_downloads ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, title JSON NOT NULL, -- Übersetzbar description JSON NULL, -- Übersetzbar category VARCHAR(255) NOT NULL, -- case_study, capability, success_story file_path JSON NOT NULL, -- Übersetzbar: {"de": "file-de.pdf", "en": "file-en.pdf"} thumbnail VARCHAR(255) NULL, -- CmsMedia-Dateiname icon VARCHAR(255) NULL, -- Heroicon-Name sub_category VARCHAR(255) NULL, -- Detail-Kategorie type_label JSON NULL, -- Übersetzbar alt JSON NULL, -- Übersetzbar: Alt-Text open_text JSON NULL, -- Übersetzbar: Button-Text download_text JSON NULL, -- Übersetzbar: Button-Text highlights JSON NULL, -- [{"value": "100%", "label": "..."}] checkpoints JSON NULL, -- [{"value": "..."}] is_published BOOLEAN DEFAULT TRUE, `order` INT DEFAULT 0, created_at TIMESTAMP NULL, updated_at TIMESTAMP NULL ); ``` ### flux_cms_news_items ```sql CREATE TABLE flux_cms_news_items ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, icon VARCHAR(255) NULL, text JSON NULL, -- Ticker-Text (übersetzbar) title JSON NULL, excerpt JSON NULL, content JSON NULL, -- Voller HTML-Body image VARCHAR(255) NULL, -- CmsMedia-Dateiname date DATE NULL, author VARCHAR(255) NULL, link VARCHAR(255) NULL, pdf_path VARCHAR(255) NULL, -- CmsMedia-Dateiname (PDF) pdf_open_text JSON NULL, pdf_download_text JSON NULL, is_published BOOLEAN DEFAULT TRUE, `order` INT DEFAULT 0, created_at TIMESTAMP NULL, updated_at TIMESTAMP NULL ); ``` ### flux_cms_industries ```sql CREATE TABLE flux_cms_industries ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, name JSON NOT NULL, -- Übersetzbar is_published BOOLEAN DEFAULT TRUE, `order` INT DEFAULT 0, created_at TIMESTAMP NULL, updated_at TIMESTAMP NULL ); ``` ### flux_cms_faqs ```sql CREATE TABLE flux_cms_faqs ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, category VARCHAR(255) NOT NULL, question JSON NOT NULL, answer JSON NOT NULL, help JSON NULL, is_published BOOLEAN DEFAULT TRUE, `order` INT DEFAULT 0, created_at TIMESTAMP NULL, updated_at TIMESTAMP NULL ); ``` ### flux_cms_linkedin_posts ```sql CREATE TABLE flux_cms_linkedin_posts ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, linkedin_id VARCHAR(255) NULL UNIQUE, title JSON NULL, excerpt JSON NULL, content JSON NULL, author VARCHAR(255) NULL, date DATE NULL, url VARCHAR(255) NULL, image VARCHAR(255) NULL, -- CmsMedia-Dateiname tags JSON NULL, source VARCHAR(255) DEFAULT 'manual', is_published BOOLEAN DEFAULT TRUE, `order` INT DEFAULT 0, created_at TIMESTAMP NULL, updated_at TIMESTAMP NULL ); ``` ### flux_cms_search_index ```sql CREATE TABLE flux_cms_search_index ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, item_id VARCHAR(255) NOT NULL UNIQUE, -- z.B. 'home', 'leistungen' route VARCHAR(255) NOT NULL, -- Named Route route_params JSON NULL, -- Route-Parameter category JSON NULL, -- Übersetzbar: Kategorie title_key VARCHAR(255) NULL, -- CMS-Key für Titel title_fallback JSON NULL, -- Übersetzbar: Fallback-Titel description_key VARCHAR(255) NULL, -- CMS-Key für Beschreibung description_fallback_key VARCHAR(255) NULL, -- Sekundärer CMS-Key description_fallback_text JSON NULL, -- Übersetzbar: Statischer Text keywords JSON NULL, -- Übersetzbar: String-Array is_published BOOLEAN DEFAULT TRUE, `order` INT DEFAULT 0, created_at TIMESTAMP NULL, updated_at TIMESTAMP NULL, INDEX (item_id) ); ``` ## Admin UI — Komponenten-Architektur Alle Admin-Views sind **Livewire Volt Functional Components**: ```php ...); $save = function () { ... }; ?>
{{-- Blade/Flux UI Template --}}
``` ### Ausnahmen: Class-based Livewire File-Uploads und modale Auswahl benötigen class-based Livewire-Komponenten: | Komponente | Grund | |------------|-------| | `MediaLibraryUploader` | `WithFileUploads` Trait für Multi-File Upload | | `MediaPicker` | `WithFileUploads` Trait für Quick-Upload + Event-System | ### MediaPicker — Event-Architektur ``` ┌───────────────────────────────────────────┐ │ Admin-View (Volt Functional) │ │ state: $imageMediaId, $image │ │ │ │ on('media-selected', fn($field, ...)) │ │ → setzt MediaId + Dateiname │ │ │ │ │ └──────────────┬────────────────────────────┘ │ dispatches 'media-selected' ┌──────────────▼────────────────────────────┐ │ MediaPicker (Class-based) │ │ - Modal mit Medienauswahl │ │ - Filterung nach Typ │ │ - Quick-Upload │ │ - $this->dispatch('media-selected', ...) │ └───────────────────────────────────────────┘ ``` ### Medien-Speicherung in Modellen Alle Medien werden über ihren **CmsMedia-Dateiname** referenziert: ``` CmsContent (type=image) → value = "keyvisual.webp" CmsNewsItem → image = "news-header.webp", pdf_path = "report.pdf" CmsDownload → thumbnail = "case-study.webp", file_path = {"de": "cs-de.pdf", "en": "cs-en.pdf"} CmsLinkedinPost → image = "linkedin-post.webp" Team (JSON in CmsContent) → image = "person.jpg" ``` Die Auflösung zum tatsächlichen URL erfolgt immer über `media_url($filename, $profile)`. ## toFrontendArray() Pattern Für die Frontend-Ausgabe bieten `CmsNewsItem` und `CmsDownload` eine `toFrontendArray()` Methode: ```php // CmsDownload::toFrontendArray() public function toFrontendArray(): array { return [ 'title' => $this->getTranslation('title', app()->getLocale()), 'description' => $this->getTranslation('description', app()->getLocale()), 'image' => media_url($this->thumbnail ?? '', 'card'), 'pdf_path' => media_url( $this->getTranslation('file_path', app()->getLocale()) ?? '' ), 'highlights' => $this->highlights ?? [], 'checkpoints' => $this->checkpoints ?? [], 'icon' => $this->icon, // ... weitere Felder ]; } ``` Frontend-Verwendung: ```blade @foreach (CmsDownload::published()->byCategory('case_study')->ordered()->get() as $download) @endforeach ``` ## Bildoptimierung — MediaConversionService ``` ┌─────────────────────────────────────────────────────┐ │ MediaConversionService (Singleton) │ │ │ │ Abhängigkeiten: intervention/image v3 │ │ │ │ storeUploadedFile($file, $collection) │ │ ├── Speichert Original in cms/media/originals/ │ │ ├── Erstellt CmsMedia-Eintrag (mit Dimensionen) │ │ ├── Generiert Thumbnail automatisch │ │ └── Gibt CmsMedia zurück │ │ │ │ convert($media, $profileName) │ │ ├── Liest Profil aus Config │ │ ├── Resize (cover fit) via intervention/image │ │ ├── Konvertiert Format (webp, jpg, png) │ │ ├── Setzt Qualität │ │ ├── Speichert in cms/media/conversions/ │ │ └── Aktualisiert $media->conversions JSON │ │ │ │ generateThumbnail($media) │ │ ├── 200x200 Cover, WebP, Q70 │ │ ├── Speichert in cms/media/thumbnails/ │ │ └── Setzt $media->conversions['thumb'] │ │ │ │ generateAllConversions($media) │ │ └── Iteriert alle Profiles aus Config │ └─────────────────────────────────────────────────────┘ ``` ### Conversion-Profile Konfigurierbar in `config/flux-cms.php`: ```php 'media' => [ 'max_upload_size' => 20480, // KB 'allowed_types' => ['jpg', 'jpeg', 'png', 'webp', 'gif', 'svg', 'pdf', 'doc', 'docx'], 'storage_disk' => 'public', 'profiles' => [ 'hero' => ['width' => 1920, 'height' => 800, 'format' => 'webp', 'quality' => 85], 'card' => ['width' => 768, 'height' => 512, 'format' => 'webp', 'quality' => 80], 'thumbnail' => ['width' => 400, 'height' => 300, 'format' => 'webp', 'quality' => 75], 'avatar' => ['width' => 400, 'height' => 400, 'format' => 'webp', 'quality' => 80], 'news' => ['width' => 1200, 'height' => 630, 'format' => 'webp', 'quality' => 80], 'thumb' => ['width' => 200, 'height' => 200, 'format' => 'webp', 'quality' => 70], ], ], ``` ## Content-Typ-Erkennung (Seeder) | Bedingung | Typ | |-----------|-----| | PHP-Array (assoziativ → flatten; indexed → direkt) | `json` | | Enthält HTML-Tags (`

`, `
`, ``, etc.) | `html` | | Dateiendung `.jpg`, `.png`, `.svg`, etc. | `image` | | Dateiendung `.pdf`, `.doc`, etc. | `link` | | Alles andere | `text` | ## HTML-Bereinigung (Seeder) ### font-semibold → `` ```html Wichtig Wichtig ``` ### text-gradient-premium → :highlight Pattern ```php // Vorher: 'heading' => 'Wie unsere Ingenieurstrategen die drei Säulen sichern', // Nachher: 'heading' => ':highlight die drei Säulen sichern', 'heading_highlight' => 'Wie unsere Ingenieurstrategen', ``` ## Caching-Strategie - **Content-Cache**: Pro Gruppe gecached (z.B. `flux_cms.content.welcome`) - **media_url() Cache**: In-Memory-Cache pro Request (statische Variable) - **TTL**: Konfigurierbar (`flux-cms.cache.ttl`, Standard: 3600s) - **Invalidierung**: Automatisch bei Save im Admin via `CmsContentService::clearCache($group)` - **Cache-Store**: Konfigurierbar (`flux-cms.cache.store`) - **Seeder**: Ruft `clearCache()` nach dem Seeding auf ## Sortierung - Inhalte werden in der Reihenfolge der `lang/`-Datei gespeichert (chronologisch) - `order`-Feld wird beim Seeding sequentiell vergeben - Industries, Downloads, News haben Up/Down-Buttons + numerisches Order-Feld - Downloads können zusätzlich nach Kategorie gefiltert werden ## Mehrsprachigkeit Alle Modelle nutzen `Spatie\Translatable\HasTranslations`: ```php $content->getTranslation('value', 'de'); $content->setTranslation('value', 'en', 'New value'); $content->save(); ``` Besonderheit bei `CmsDownload`: - `file_path` ist ebenfalls übersetzbar (separates PDF pro Sprache) - `toFrontendArray()` löst automatisch die aktuelle Locale auf Im Admin kann zwischen Sprachen gewechselt werden via `switchLocale()`. ## Infrastruktur-Hinweise ### HTTPS / Proxy - `trustProxies(at: '*')` in `bootstrap/app.php` für korrekte URL-Generierung - `URL::forceScheme('https')` in `AppServiceProvider` ### File-Uploads - `flux:file-upload` Komponente (Flux UI) für Livewire-Uploads - Maximale Upload-Größe konfigurierbar in `config/flux-cms.php` - Erlaubte MIME-Types in Validation-Rules der Upload-Komponenten