Add Multi-Server Orchestration (Phase 1-8) to all docs: - INFRASTRUCTURE.md: Hetzner 1/2 production servers, SSH infrastructure, payload-contracts, deployment workflows, port-forwarding - PROJECT_STATUS.md: Orchestration changelog, production URLs, SSH commands - FRONTEND.md: payload-contracts usage, CI/CD pipelines, staging/production deploy, work order system, ESLint config, updated tenant IDs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
12 KiB
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
// package.json
{
"dependencies": {
"@c2s/payload-contracts": "github:complexcaresolutions/payload-contracts#main"
}
}
Imports
// 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
// 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
// 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
// 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:
# 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)
# 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.
# .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.
# .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
- sv-payload: CMS ändern → Types extrahieren → Work Order schreiben
- sv-frontend: Work Order lesen →
pnpm update @c2s/payload-contracts→ Block implementieren
Referenz
Vollständige Dokumentation im payload-contracts Repo:
work-orders/_template.md— Vorlagescripts/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:
// 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:
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
// 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
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
ssh frontend@10.10.181.104
Projekt starten
cd ~/frontend.porwoll.de
pnpm dev
# Läuft auf Port 3000 → https://porwoll-dev.porwoll.tech
AI-Tools
claude # Claude Code CLI (v2.1.37)
codex # Codex CLI
gemini # Gemini CLI
ESLint-Konfiguration
Alle Frontends verwenden die gleiche ESLint-Konfiguration:
// 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