# 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 () { ... }; ?>
`, `
`, ``, 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