# Frontend-Entwicklung - Payload CMS Multi-Tenant

> **Server:** sv-frontend (LXC 704) - 10.10.181.104
> **Backend API:** https://cms.c2sgmbh.de/api (Production)
> **Shared Types:** `@c2s/payload-contracts` (Git-Dependency)

## Übersicht

Jedes Frontend ist ein separates Next.js-Projekt und nutzt Payload CMS als Headless CMS über die REST-API. Alle Frontends teilen sich TypeScript-Typen und den API-Client über das `payload-contracts` Package.

**Architektur:**
```
CMS (payload-cms)          payload-contracts              Frontends (Next.js)
━━━━━━━━━━━━━━━━          ━━━━━━━━━━━━━━━━━             ━━━━━━━━━━━━━━━━━━
Collections + Blocks  →    Shared Types                 →  frontend.porwoll.de
payload-types.ts      →    API Client                   →  frontend.blogwoman.de
                            Block Registry               →  (7 weitere)
```

---

## payload-contracts (Shared Package)

Alle Frontends verwenden `@c2s/payload-contracts` als Git-Dependency für TypeScript-Typen, den API-Client und die Block-Registry.

### Installation

```json
// package.json
{
  "dependencies": {
    "@c2s/payload-contracts": "github:complexcaresolutions/payload-contracts#main"
  }
}
```

### Imports

```typescript
// Types
import type { Page, Post, Media } from '@c2s/payload-contracts/types'
import type { Block, BlockByType } from '@c2s/payload-contracts/types'

// API Client
import { createPayloadClient } from '@c2s/payload-contracts/api-client'

// Block Renderer
import { createBlockRenderer } from '@c2s/payload-contracts/blocks'

// Constants
import { TENANTS } from '@c2s/payload-contracts/constants'
```

### API Client erstellen

```typescript
// src/lib/api.ts
import { createPayloadClient } from '@c2s/payload-contracts/api-client'

export const api = createPayloadClient({
  baseUrl: process.env.NEXT_PUBLIC_PAYLOAD_URL!,
  tenantId: Number(process.env.NEXT_PUBLIC_TENANT_ID),
})

// Verwendung
const page = await api.pages.getBySlug('home')
const posts = await api.posts.getAll({ limit: 10 })
const nav = await api.navigation.get()
const settings = await api.settings.get()
```

### Block Renderer

```typescript
// src/components/blocks/index.tsx
import { createBlockRenderer } from '@c2s/payload-contracts/blocks'
import { HeroBlock } from './HeroBlock'
import { TextBlock } from './TextBlock'
// ... weitere Block-Imports

export const BlockRenderer = createBlockRenderer({
  'hero-block': HeroBlock,
  'text-block': TextBlock,
  // ... nur die Blocks registrieren, die das Frontend braucht
})
```

### Block-Komponente implementieren

```typescript
// src/components/blocks/HeroBlock.tsx
import type { BlockByType } from '@c2s/payload-contracts/types'

type HeroBlockData = BlockByType<'hero-block'>

interface HeroBlockProps {
  block: HeroBlockData
}

export function HeroBlock({ block }: HeroBlockProps) {
  return (
    <section>
      <h1>{block.headline}</h1>
      {block.subline && <p>{block.subline}</p>}
    </section>
  )
}
```

### Types aktualisieren

Wenn sich CMS-Collections oder Blocks ändern:

```bash
# Auf sv-payload:
cd ~/payload-cms && pnpm payload generate:types
cd ~/payload-contracts && pnpm extract
git add -A && git commit -m "types: update from CMS" && git push

# Auf sv-frontend (pro Projekt):
cd ~/frontend.porwoll.de
pnpm update @c2s/payload-contracts
```

---

## Umgebungskonfiguration

### Environment Variables (.env.local)

```env
# API-Endpunkte (PRODUKTION)
NEXT_PUBLIC_PAYLOAD_URL=https://cms.c2sgmbh.de
NEXT_PUBLIC_API_URL=https://cms.c2sgmbh.de/api

# Analytics (optional)
NEXT_PUBLIC_UMAMI_HOST=https://analytics.c2sgmbh.de
NEXT_PUBLIC_UMAMI_WEBSITE_ID=<website-id>

# Tenant-Konfiguration (je nach Projekt)
NEXT_PUBLIC_TENANT_ID=4
NEXT_PUBLIC_TENANT_SLUG=c2s
```

### Tenant-IDs

| ID | Name | Slug | Domain | Status |
|----|------|------|--------|--------|
| 1 | porwoll.de | porwoll | porwoll.de | ✅ Live |
| 4 | Complex Care Solutions GmbH | c2s | complexcaresolutions.de | Geplant |
| 5 | Gunshin | gunshin | gunshin.de | Geplant |
| 9 | BlogWoman | blogwoman | blogwoman.de | ✅ Live |

### Warum Production-Daten?

Die Frontend-Entwicklung verwendet die Produktions-API, um mit echten Inhalten zu arbeiten. SEO-Einstellungen und Cookie-Consent-Konfigurationen werden ebenfalls aus der Produktionsumgebung geladen.

---

## CI/CD Pipeline

### CI (Lint + Build)

Jedes Frontend hat eine GitHub Actions CI-Pipeline, die bei Push auf `develop` und `main` läuft.

```yaml
# .github/workflows/ci.yml
name: CI
on:
  push:
    branches: [develop, main]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with: { node-version: 22, cache: pnpm }
      - run: pnpm install --frozen-lockfile
      - run: pnpm lint
      - run: pnpm build
```

### Staging-Deploy (sv-frontend)

Push auf `develop` → GitHub Actions → SSH via UDM Pro SE Port-Forward → Build auf sv-frontend.

```yaml
# .github/workflows/deploy-staging.yml
# SSH-Credentials als Repository-Secrets: SSH_HOST, SSH_PORT, SSH_USER, SSH_PRIVATE_KEY
```

### Production-Deploy (Plesk)

Push auf `main` → GitHub Webhook → Plesk Git Pull → `pnpm install && pnpm build` → Passenger Restart.

Production-Deployment wird NICHT über GitHub Actions gesteuert, sondern über Plesk Git-Integration mit Webhooks.

### Deployment-Flow

```
develop (sv-frontend)  →  CI ✅  →  Staging-Test (*-dev.porwoll.tech)
         │
         ▼
main (merge)           →  CI ✅  →  Webhook → Plesk → Production ✅
```

---

## Work-Order-System

Wenn ein neuer Block oder eine neue Collection im CMS erstellt wird, koordiniert das Work-Order-System die Frontend-Implementierung.

### Ablauf

1. **sv-payload:** CMS ändern → Types extrahieren → Work Order schreiben
2. **sv-frontend:** Work Order lesen → `pnpm update @c2s/payload-contracts` → Block implementieren

### Referenz

Vollständige Dokumentation im `payload-contracts` Repo:
- `work-orders/_template.md` — Vorlage
- `scripts/create-work-order.sh` — Work Order erstellen (auf sv-payload)
- `scripts/execute-work-order.sh` — Work Order ausführen (auf sv-frontend)

---

## API-Dokumentation

| Ressource | URL |
|-----------|-----|
| **Swagger UI** | https://cms.c2sgmbh.de/api/docs |
| **OpenAPI JSON** | https://cms.c2sgmbh.de/api/openapi.json |
| **REST API Base** | https://cms.c2sgmbh.de/api |

---

## Frontend-Status

### Live

| Frontend | Production | Blocks | Contracts |
|----------|-----------|--------|-----------|
| porwoll.de | ✅ Hetzner 2 | 9 implementiert | Direkte Types |
| blogwoman.de | ✅ Hetzner 1 | Alle implementiert | Bridge-Pattern (lokale types.ts) |

### Geplant

| Frontend | Server | Priorität |
|----------|--------|-----------|
| complexcaresolutions.de | Hetzner 1 | Hoch |
| caroline-porwoll.com | Hetzner 2 | Mittel |
| caroline-porwoll.de | Hetzner 2 | Mittel |
| gunshin.de | - | Niedrig |
| zweitmeinu.ng | Hetzner 1 | Niedrig |

### Offene Tasks

- [ ] porwoll.de: Fehlende Blocks implementieren (~6 via Work Orders)
- [ ] blogwoman.de: Bridge-Pattern durch direkte Contracts-Imports ersetzen
- [ ] Cookie-Banner implementieren (DSGVO)
- [ ] Newsletter-Formular-Komponente

---

## API-Endpoints

### Collections

| Collection | Endpoint | Beschreibung |
|------------|----------|--------------|
| Pages | `GET /api/pages` | Seiten mit Blocks |
| Posts | `GET /api/posts` | Blog, News, Presse |
| Categories | `GET /api/categories` | Post-Kategorien |
| Testimonials | `GET /api/testimonials` | Kundenbewertungen |
| Team | `GET /api/team` | Team-Mitglieder |
| Services | `GET /api/services` | Leistungen |
| FAQs | `GET /api/faqs` | FAQ-Einträge |
| Portfolios | `GET /api/portfolios` | Portfolio-Projekte |
| Media | `GET /api/media` | Medien/Bilder |
| Videos | `GET /api/videos` | Video-Bibliothek |
| Timelines | `GET /api/timelines` | Chronologische Events |
| Workflows | `GET /api/workflows` | Prozess-Darstellungen |
| Favorites | `GET /api/favorites` | Affiliate-Produkte (BlogWoman) |
| Series | `GET /api/series` | YouTube-Serien (BlogWoman) |

### Site Settings & Navigation (Tenant-isoliert)

| Collection | Endpoint | Beschreibung |
|------------|----------|--------------|
| Site Settings | `GET /api/site-settings?where[tenant][equals]=4` | Logo, Name, Kontakt, Adresse |
| Navigation | `GET /api/navigations?where[tenant][equals]=4` | Menü-Struktur |
| Privacy Policy | `GET /api/privacy-policy-settings?where[tenant][equals]=4` | Datenschutz |

### Spezielle Endpoints

| Endpoint | Methode | Beschreibung |
|----------|---------|--------------|
| `/api/search` | GET | Volltextsuche |
| `/api/search/suggestions` | GET | Auto-Complete |
| `/api/newsletter/subscribe` | POST | Newsletter-Anmeldung |

---

## Tenant-Filterung

Alle Collection-Anfragen sollten nach Tenant gefiltert werden:

```typescript
// Mit payload-contracts API Client (empfohlen):
const posts = await api.posts.getAll({ limit: 10 })
// → Tenant-Filter wird automatisch angewendet

// Manuell:
fetch('https://cms.c2sgmbh.de/api/posts?where[tenant][equals]=4&locale=de')
```

---

## Bild-Optimierung

Media-Objekte enthalten mehrere Größen:

```typescript
interface Media {
  url: string           // Original
  sizes: {
    thumbnail: { url, width, height }    // 150x150
    small: { url, width, height }        // 300x300
    medium: { url, width, height }       // 600x600
    large: { url, width, height }        // 1200x1200
    xlarge: { url, width, height }       // 1920x1920
    '2k': { url, width, height }         // 2560x2560
    og: { url, width, height }           // 1200x630 (Social)
    // + AVIF-Varianten
    thumbnail_avif: { url, width, height }
    small_avif: { url, width, height }
    // ...
  }
  focalX?: number       // Fokuspunkt X (0-100)
  focalY?: number       // Fokuspunkt Y (0-100)
}
```

---

## Lokalisierung

Unterstützte Locales: `de` (default), `en`

```typescript
// Deutsch (default)
fetch('https://cms.c2sgmbh.de/api/posts?locale=de')

// Englisch
fetch('https://cms.c2sgmbh.de/api/posts?locale=en')

// Fallback: Wenn EN nicht vorhanden, wird DE zurückgegeben
```

---

## Newsletter-Integration

```typescript
const response = await fetch('https://cms.c2sgmbh.de/api/newsletter/subscribe', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    email: 'user@example.com',
    firstName: 'Max',           // optional
    tenantId: 4,                // Pflicht
    source: 'footer'            // optional: Herkunft
  })
})
```

**Flow:** Subscribe → Double Opt-In E-Mail → Bestätigung → Willkommens-E-Mail

---

## Rate-Limits

| Endpoint | Limit |
|----------|-------|
| Öffentliche API | 60/min |
| Suche | 30/min |
| Newsletter | 5/10min |
| Formulare | 5/10min |

---

## Development Server (sv-frontend)

### SSH-Zugang

```bash
ssh frontend@10.10.181.104
```

### Projekt starten

```bash
cd ~/frontend.porwoll.de
pnpm dev
# Läuft auf Port 3000 → https://porwoll-dev.porwoll.tech
```

### AI-Tools

```bash
claude      # Claude Code CLI (v2.1.37)
codex       # Codex CLI
gemini      # Gemini CLI
```

---

## ESLint-Konfiguration

Alle Frontends verwenden die gleiche ESLint-Konfiguration:

```javascript
// eslint.config.mjs
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";

const eslintConfig = defineConfig([
  ...nextVitals,
  ...nextTs,
  {
    rules: {
      "@typescript-eslint/no-unused-vars": ["warn", {
        argsIgnorePattern: "^_",
        varsIgnorePattern: "^_",
        destructuredArrayIgnorePattern: "^_",
      }],
    },
  },
  globalIgnores([".next/**", "out/**", "build/**", "next-env.d.ts", "server.js"]),
]);

export default eslintConfig;
```

`server.js` wird ignoriert, da es CJS für Phusion Passenger verwendet.

---

## Ressourcen

| Ressource | URL/Pfad |
|-----------|----------|
| Payload CMS Docs | https://payloadcms.com/docs |
| API-Dokumentation | https://cms.c2sgmbh.de/api/docs |
| payload-contracts | https://github.com/complexcaresolutions/payload-contracts |
| Infrastruktur-Docs | `docs/INFRASTRUCTURE.md` |
| Analytics | https://analytics.c2sgmbh.de |

---

*Letzte Aktualisierung: 15.02.2026*
