# 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
<?php
use function Livewire\Volt\{state, computed, on, layout};
layout('components.layouts.cms');

state([...]);
$items = computed(fn() => ...);
$save = function () { ... };
?>

<div>
    {{-- Blade/Flux UI Template --}}
</div>
```

### 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          │
│                                           │
│  <livewire:admin.cms.media-picker         │
│      :value="$imageMediaId"               │
│      field="news_image"                   │
│      type="image"                         │
│      profile="news" />                    │
└──────────────┬────────────────────────────┘
               │ 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)
    <x-download-article-card :article="$download->toFrontendArray()" :index="$loop->index" />
@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 (`<p>`, `<br>`, `<strong>`, etc.) | `html` |
| Dateiendung `.jpg`, `.png`, `.svg`, etc. | `image` |
| Dateiendung `.pdf`, `.doc`, etc. | `link` |
| Alles andere | `text` |

## HTML-Bereinigung (Seeder)

### font-semibold → `<strong>`

```html
<!-- Vorher (aus Lang-Datei) -->
<span class="font-semibold">Wichtig</span>

<!-- Nachher (in DB) -->
<strong>Wichtig</strong>
```

### text-gradient-premium → :highlight Pattern

```php
// Vorher:
'heading' => 'Wie unsere Ingenieurstrategen <span class="text-gradient-premium">die drei Säulen sichern</span>',

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