mirror of
https://github.com/complexcaresolutions/cms.c2sgmbh.git
synced 2026-03-18 06:14:11 +00:00
Merge branch 'develop'
This commit is contained in:
commit
cb0d23af0e
12 changed files with 2857 additions and 781 deletions
126
BUG_REPORT_CUSTOM_VIEWS.md
Normal file
126
BUG_REPORT_CUSTOM_VIEWS.md
Normal file
|
|
@ -0,0 +1,126 @@
|
||||||
|
# Bug Report: Custom Admin Views cause TypeError with path-to-regexp
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
Custom admin views registered via `admin.components.views` cause a `TypeError: Missing parameter name at 5` error from path-to-regexp when used together with `@payloadcms/plugin-multi-tenant`.
|
||||||
|
|
||||||
|
## Environment
|
||||||
|
- **Payload Version:** 3.68.4
|
||||||
|
- **Next.js Version:** 15.5.9
|
||||||
|
- **React Version:** 19.2.3
|
||||||
|
- **Node.js Version:** 22.x
|
||||||
|
- **Database:** PostgreSQL 17.6
|
||||||
|
- **Plugin:** @payloadcms/plugin-multi-tenant 3.68.4
|
||||||
|
|
||||||
|
## Steps to Reproduce
|
||||||
|
|
||||||
|
1. Create a Payload 3.x project with the multi-tenant plugin
|
||||||
|
2. Add a custom view to the admin config:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// payload.config.ts
|
||||||
|
export default buildConfig({
|
||||||
|
admin: {
|
||||||
|
user: Users.slug,
|
||||||
|
components: {
|
||||||
|
views: {
|
||||||
|
MyCustomView: {
|
||||||
|
Component: '@/components/admin/MyCustomView#MyCustomView',
|
||||||
|
path: '/my-custom-view',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
multiTenantPlugin({
|
||||||
|
tenantsSlug: 'tenants',
|
||||||
|
collections: { /* ... */ },
|
||||||
|
}),
|
||||||
|
// other plugins...
|
||||||
|
],
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Create a simple client component:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// src/components/admin/MyCustomView.tsx
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export const MyCustomView: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '2rem' }}>
|
||||||
|
<h1>Custom View</h1>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MyCustomView
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Build and run the project
|
||||||
|
5. Navigate to `/admin/my-custom-view`
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
The custom view should render correctly.
|
||||||
|
|
||||||
|
## Actual Behavior
|
||||||
|
A server-side error occurs with the following message in production:
|
||||||
|
|
||||||
|
```
|
||||||
|
Uncaught Error: An error occurred in the Server Components render.
|
||||||
|
The specific message is omitted in production builds to avoid leaking sensitive details.
|
||||||
|
```
|
||||||
|
|
||||||
|
When running in development or checking server logs, the actual error is:
|
||||||
|
|
||||||
|
```
|
||||||
|
⨯ TypeError: Missing parameter name at 5
|
||||||
|
at <unknown> (.next/server/chunks/XXXX.js:5:9989)
|
||||||
|
at <unknown> (.next/server/chunks/XXXX.js:5:10666)
|
||||||
|
at g (.next/server/chunks/XXXX.js:5:11896)
|
||||||
|
at e (.next/server/chunks/824.js:96:517138)
|
||||||
|
at <unknown> (.next/server/app/(payload)/admin/[[...segments]]/page.js:1:31665)
|
||||||
|
at Array.find (<anonymous>)
|
||||||
|
at <unknown> (.next/server/app/(payload)/admin/[[...segments]]/page.js:1:31644)
|
||||||
|
at ax (.next/server/app/(payload)/admin/[[...segments]]/page.js:1:34637) {
|
||||||
|
digest: '3964718924'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Additional Context
|
||||||
|
|
||||||
|
### What works:
|
||||||
|
- Admin panel without custom views
|
||||||
|
- Components in `afterNavLinks` (e.g., TenantBreadcrumb)
|
||||||
|
- Components in `beforeNavLinks` (e.g., DashboardNavLink)
|
||||||
|
|
||||||
|
### What fails:
|
||||||
|
- ANY custom view registered via `admin.components.views`, even the simplest component without any dependencies
|
||||||
|
|
||||||
|
### Investigation findings:
|
||||||
|
- The error originates from `path-to-regexp` (version 6.3.0)
|
||||||
|
- The error occurs during route matching in `handleEndpoints.js`
|
||||||
|
- The error message "Missing parameter name at 5" suggests an invalid route pattern like `/:` is being generated somewhere
|
||||||
|
- This happens regardless of the view path used (tested with `/dashboard`, `/tenant-dashboard`, `/test-dashboard`)
|
||||||
|
- The issue appears to be triggered by the combination of custom views and the multi-tenant plugin
|
||||||
|
|
||||||
|
### Workaround
|
||||||
|
Disable custom views and use only `afterNavLinks`/`beforeNavLinks` components:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterNavLinks: ['@/components/admin/TenantBreadcrumb#TenantBreadcrumb'],
|
||||||
|
// views disabled due to bug
|
||||||
|
},
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
## Related Plugins
|
||||||
|
- `@payloadcms/plugin-multi-tenant` (may be related to the conflict)
|
||||||
|
- `@payloadcms/plugin-redirects`
|
||||||
|
- `@payloadcms/plugin-seo`
|
||||||
|
- `@payloadcms/plugin-form-builder`
|
||||||
|
- `@payloadcms/plugin-nested-docs`
|
||||||
68
CLAUDE.md
68
CLAUDE.md
|
|
@ -11,31 +11,55 @@ Multi-Tenant CMS für 4 Websites unter einer Payload CMS 3.x Instanz:
|
||||||
|
|
||||||
## Tech Stack
|
## Tech Stack
|
||||||
|
|
||||||
- **CMS:** Payload CMS 3.x
|
- **CMS:** Payload CMS 3.68.4
|
||||||
- **Framework:** Next.js 15.4.7
|
- **Framework:** Next.js 15.5.9
|
||||||
|
- **React:** 19.2.3
|
||||||
- **Sprache:** TypeScript
|
- **Sprache:** TypeScript
|
||||||
- **Datenbank:** PostgreSQL 17 (separater Server)
|
- **Runtime:** Node.js 22.x
|
||||||
|
- **Datenbank:** PostgreSQL 17.6 (separater Server)
|
||||||
- **Connection Pool:** PgBouncer 1.24.1 (Transaction-Mode)
|
- **Connection Pool:** PgBouncer 1.24.1 (Transaction-Mode)
|
||||||
- **Reverse Proxy:** Caddy 2.10.2 mit Let's Encrypt
|
- **Reverse Proxy:** Caddy 2.9.x mit Let's Encrypt (Dev) / Nginx (Prod)
|
||||||
- **Process Manager:** PM2
|
- **Process Manager:** PM2
|
||||||
- **Package Manager:** pnpm
|
- **Package Manager:** pnpm
|
||||||
- **Cache:** Redis (optional, mit In-Memory-Fallback)
|
- **Cache:** Redis 7.x (optional, mit In-Memory-Fallback)
|
||||||
- **Job Queue:** BullMQ (Redis-basiert)
|
- **Job Queue:** BullMQ (Redis-basiert)
|
||||||
|
- **Analytics:** Umami 3.x
|
||||||
|
- **AI Tools:** Claude Code, Codex CLI, Gemini CLI
|
||||||
|
|
||||||
## Architektur
|
## Architektur
|
||||||
|
|
||||||
|
### Development (VLAN 181 - porwoll.tech)
|
||||||
|
|
||||||
```
|
```
|
||||||
Internet → 37.24.237.181 → Caddy (443) → Payload (3000)
|
Internet → Cloudflare → 37.24.237.181 → Caddy (sv-caddy) → Services
|
||||||
↓
|
↓
|
||||||
|
┌───────────────────────────┴───────────────────────────┐
|
||||||
|
│ │
|
||||||
|
sv-payload:3000 sv-frontend:3000-3008
|
||||||
|
(Payload CMS) (9 Frontend-Projekte)
|
||||||
|
│
|
||||||
PgBouncer (6432)
|
PgBouncer (6432)
|
||||||
↓
|
│
|
||||||
PostgreSQL (10.10.181.101:5432)
|
PostgreSQL (sv-postgres:5432)
|
||||||
```
|
```
|
||||||
|
|
||||||
| Server | IP | Funktion |
|
| LXC | Hostname | IP | Service | Status |
|
||||||
| --------------------- | ------------- | ---------- |
|
|-----|----------|-----|---------|--------|
|
||||||
| sv-payload (LXC 700) | 10.10.181.100 | App Server |
|
| 699 | sv-caddy | 10.10.181.99 | Caddy Reverse Proxy | ✅ Running |
|
||||||
| sv-postgres (LXC 701) | 10.10.181.101 | Datenbank |
|
| 700 | sv-payload | 10.10.181.100 | Payload CMS + Redis | ✅ Running |
|
||||||
|
| 701 | sv-postgres | 10.10.181.101 | PostgreSQL 17 + Redis Commander | ✅ Running |
|
||||||
|
| 702 | sv-dev-payload | 10.10.181.102 | Payload Experimental | ⏸️ Stopped |
|
||||||
|
| 703 | sv-analytics | 10.10.181.103 | Umami Analytics | ✅ Running |
|
||||||
|
| 704 | sv-frontend | 10.10.181.104 | Multi-Frontend Dev (9 Projekte) | ✅ Running |
|
||||||
|
|
||||||
|
### Production (Hetzner 3)
|
||||||
|
|
||||||
|
| Service | URL | Port |
|
||||||
|
|---------|-----|------|
|
||||||
|
| Payload CMS | https://cms.c2sgmbh.de | 3001 |
|
||||||
|
| Umami Analytics | https://analytics.c2sgmbh.de | 3000 |
|
||||||
|
| PostgreSQL 17 | localhost | 5432 |
|
||||||
|
| Redis Cache | localhost | 6379 |
|
||||||
|
|
||||||
## Wichtige Pfade
|
## Wichtige Pfade
|
||||||
|
|
||||||
|
|
@ -758,6 +782,8 @@ Vollwertiger Hero-Slider mit:
|
||||||
| Bookings | bookings | Fotografie-Buchungen (porwoll.de) |
|
| Bookings | bookings | Fotografie-Buchungen (porwoll.de) |
|
||||||
| Certifications | certifications | Zertifizierungen (C2S) |
|
| Certifications | certifications | Zertifizierungen (C2S) |
|
||||||
| Projects | projects | Game-Development-Projekte (gunshin.de) |
|
| Projects | projects | Game-Development-Projekte (gunshin.de) |
|
||||||
|
| Videos | videos | Video-Bibliothek mit YouTube/Vimeo/Uploads |
|
||||||
|
| VideoCategories | video-categories | Kategorien für Videos |
|
||||||
|
|
||||||
## Timeline Collection
|
## Timeline Collection
|
||||||
|
|
||||||
|
|
@ -985,11 +1011,21 @@ Automatisches Deployment auf Staging-Server bei Push auf `develop`:
|
||||||
|
|
||||||
## Dokumentation
|
## Dokumentation
|
||||||
|
|
||||||
- `CLAUDE.md` - Diese Datei (Projekt-Übersicht)
|
### Hauptdokumentation
|
||||||
- `docs/INFRASTRUCTURE.md` - Server-Architektur & Deployment
|
- `CLAUDE.md` - Diese Datei (Projekt-Übersicht für AI-Assistenten)
|
||||||
|
- `docs/INFRASTRUCTURE.md` - Infrastruktur-Übersicht (Dev + Prod)
|
||||||
|
- `docs/DEPLOYMENT.md` - Deployment-Prozess & Checklisten
|
||||||
|
- `docs/PROJECT_STATUS.md` - Aktueller Projektstatus & Roadmap
|
||||||
- `docs/STAGING-DEPLOYMENT.md` - Staging Deployment Workflow
|
- `docs/STAGING-DEPLOYMENT.md` - Staging Deployment Workflow
|
||||||
- `docs/anleitungen/TODO.md` - Task-Liste & Roadmap
|
|
||||||
|
### Anleitungen
|
||||||
|
- `docs/anleitungen/TODO.md` - Task-Liste & Changelog
|
||||||
- `docs/anleitungen/SECURITY.md` - Sicherheitsrichtlinien
|
- `docs/anleitungen/SECURITY.md` - Sicherheitsrichtlinien
|
||||||
|
- `docs/anleitungen/FRONTEND.md` - Frontend-Entwicklung (sv-frontend)
|
||||||
|
- `docs/anleitungen/API_ANLEITUNG.md` - API-Dokumentation
|
||||||
|
- `docs/anleitungen/framework-monitoring.md` - Framework-Updates beobachten
|
||||||
|
|
||||||
|
### Scripts & Backup
|
||||||
- `scripts/backup/README.md` - Backup-System Dokumentation
|
- `scripts/backup/README.md` - Backup-System Dokumentation
|
||||||
|
|
||||||
*Letzte Aktualisierung: 14.12.2025*
|
*Letzte Aktualisierung: 18.12.2025*
|
||||||
|
|
|
||||||
525
docs/DEPLOYMENT.md
Normal file
525
docs/DEPLOYMENT.md
Normal file
|
|
@ -0,0 +1,525 @@
|
||||||
|
# Deployment Guide - Payload CMS Multi-Tenant
|
||||||
|
|
||||||
|
*Letzte Aktualisierung: 18. Dezember 2025*
|
||||||
|
|
||||||
|
## Übersicht
|
||||||
|
|
||||||
|
Dieses Dokument beschreibt den Deployment-Prozess für das Payload CMS Multi-Tenant-System.
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||||
|
│ DEVELOPMENT │ │ STAGING │ │ PRODUCTION │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ sv-frontend │────▶│ sv-payload │────▶│ Hetzner 3 │
|
||||||
|
│ Local Dev │ │ pl.porwoll.tech │ │ cms.c2sgmbh.de │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ develop branch │ │ develop branch │ │ main branch │
|
||||||
|
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Umgebungen
|
||||||
|
|
||||||
|
| Umgebung | Server | URL | Branch | Zweck |
|
||||||
|
|----------|--------|-----|--------|-------|
|
||||||
|
| **Development** | sv-payload (LXC 700) | https://pl.porwoll.tech | `develop` | Entwicklung & Testing |
|
||||||
|
| **Production** | Hetzner 3 | https://cms.c2sgmbh.de | `main` | Live-System |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Staging Deployment (Development → pl.porwoll.tech)
|
||||||
|
|
||||||
|
### Automatisch via GitHub Actions
|
||||||
|
|
||||||
|
Bei jedem Push auf `develop` wird automatisch deployed:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# .github/workflows/deploy-staging.yml
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [develop]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ablauf:**
|
||||||
|
1. Pre-deployment Checks (Lint, Tests)
|
||||||
|
2. SSH-Verbindung zu sv-payload
|
||||||
|
3. `git pull origin develop`
|
||||||
|
4. `pnpm install`
|
||||||
|
5. `pnpm payload migrate`
|
||||||
|
6. `pnpm build`
|
||||||
|
7. `pm2 restart payload`
|
||||||
|
8. Health Check
|
||||||
|
|
||||||
|
### Manuell auf sv-payload
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# SSH zum Server
|
||||||
|
ssh payload@10.10.181.100
|
||||||
|
|
||||||
|
# Ins Projektverzeichnis
|
||||||
|
cd /home/payload/payload-cms
|
||||||
|
|
||||||
|
# Änderungen holen
|
||||||
|
git pull origin develop
|
||||||
|
|
||||||
|
# Dependencies aktualisieren
|
||||||
|
pnpm install
|
||||||
|
|
||||||
|
# Migrationen ausführen (falls vorhanden)
|
||||||
|
pnpm payload migrate
|
||||||
|
|
||||||
|
# Build erstellen
|
||||||
|
pnpm build
|
||||||
|
|
||||||
|
# PM2 neustarten
|
||||||
|
pm2 restart payload
|
||||||
|
|
||||||
|
# Logs prüfen
|
||||||
|
pm2 logs payload --lines 20
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mit Deploy-Script
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Auf sv-payload
|
||||||
|
./scripts/deploy-staging.sh
|
||||||
|
|
||||||
|
# Optionen:
|
||||||
|
./scripts/deploy-staging.sh --skip-build # Nur Code-Update
|
||||||
|
./scripts/deploy-staging.sh --skip-migrations # Ohne Migrationen
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Production Deployment (main → cms.c2sgmbh.de)
|
||||||
|
|
||||||
|
### Schritt 1: Merge zu main
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Auf dem Development-Server oder lokal
|
||||||
|
git checkout main
|
||||||
|
git pull origin main
|
||||||
|
git merge develop
|
||||||
|
git push origin main
|
||||||
|
```
|
||||||
|
|
||||||
|
### Schritt 2: Deploy auf Hetzner 3
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# SSH zum Production-Server
|
||||||
|
ssh payload@162.55.85.18
|
||||||
|
|
||||||
|
# Deploy-Script ausführen
|
||||||
|
~/deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deploy-Script (~/deploy.sh)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
cd ~/payload-cms
|
||||||
|
|
||||||
|
echo "📥 Pulling latest changes..."
|
||||||
|
git pull origin main
|
||||||
|
|
||||||
|
echo "📦 Installing dependencies..."
|
||||||
|
pnpm install
|
||||||
|
|
||||||
|
echo "🔄 Running migrations..."
|
||||||
|
pnpm payload migrate
|
||||||
|
|
||||||
|
echo "🏗️ Building..."
|
||||||
|
pnpm build
|
||||||
|
|
||||||
|
echo "🔄 Restarting PM2..."
|
||||||
|
pm2 restart payload
|
||||||
|
|
||||||
|
echo "✅ Deployment complete!"
|
||||||
|
pm2 status
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manuelles Deployment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh payload@162.55.85.18
|
||||||
|
cd ~/payload-cms
|
||||||
|
git pull origin main
|
||||||
|
pnpm install
|
||||||
|
pnpm payload migrate
|
||||||
|
pnpm build
|
||||||
|
pm2 restart payload
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rollback
|
||||||
|
|
||||||
|
### Auf vorherigen Commit zurücksetzen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Letzten funktionierenden Commit finden
|
||||||
|
git log --oneline -10
|
||||||
|
|
||||||
|
# Zurücksetzen (VORSICHT: Ändert History nicht)
|
||||||
|
git checkout <commit-hash>
|
||||||
|
pnpm install
|
||||||
|
pnpm build
|
||||||
|
pm2 restart payload
|
||||||
|
|
||||||
|
# Oder: Hard Reset (VORSICHT: Ändert History)
|
||||||
|
git reset --hard <commit-hash>
|
||||||
|
git push --force origin main # Nur im Notfall!
|
||||||
|
```
|
||||||
|
|
||||||
|
### Datenbank-Rollback
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Backup wiederherstellen (nur bei Datenverlust)
|
||||||
|
gunzip -c ~/backups/payload_db_YYYY-MM-DD_HH-MM-SS.sql.gz | \
|
||||||
|
psql -h localhost -U payload -d payload_db
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Migrationen
|
||||||
|
|
||||||
|
### Migration erstellen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Auf Development-Server
|
||||||
|
pnpm payload migrate:create
|
||||||
|
|
||||||
|
# Generiert: src/migrations/YYYYMMDD_HHMMSS_<name>.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
### Migration ausführen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Normale Ausführung
|
||||||
|
pnpm payload migrate
|
||||||
|
|
||||||
|
# Bei PgBouncer-Problemen: Direkte Verbindung
|
||||||
|
./scripts/db-direct.sh migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
### Migration-Status prüfen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm payload migrate:status
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Build-Konfiguration
|
||||||
|
|
||||||
|
### Memory-Limits
|
||||||
|
|
||||||
|
Der Server hat 8GB RAM. Build-Einstellungen:
|
||||||
|
|
||||||
|
```json
|
||||||
|
// package.json
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"build": "cross-env NODE_OPTIONS='--no-deprecation --max-old-space-size=2048' next build"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Bei Memory-Problemen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# PM2 stoppen um Speicher freizugeben
|
||||||
|
pm2 stop payload
|
||||||
|
|
||||||
|
# Build mit reduziertem Memory
|
||||||
|
NODE_OPTIONS="--no-deprecation --max-old-space-size=1024" pnpm build
|
||||||
|
|
||||||
|
# PM2 wieder starten
|
||||||
|
pm2 start payload
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PM2 Konfiguration
|
||||||
|
|
||||||
|
### ecosystem.config.cjs
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
module.exports = {
|
||||||
|
apps: [
|
||||||
|
{
|
||||||
|
name: 'payload',
|
||||||
|
script: 'node_modules/.bin/next',
|
||||||
|
args: 'start',
|
||||||
|
cwd: '/home/payload/payload-cms',
|
||||||
|
instances: 1,
|
||||||
|
max_memory_restart: '2G',
|
||||||
|
env: {
|
||||||
|
NODE_ENV: 'production',
|
||||||
|
PORT: 3000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'queue-worker',
|
||||||
|
script: './scripts/run-queue-worker.ts',
|
||||||
|
interpreter: 'node',
|
||||||
|
interpreter_args: '--import tsx',
|
||||||
|
cwd: '/home/payload/payload-cms',
|
||||||
|
instances: 1,
|
||||||
|
max_memory_restart: '500M'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### PM2 Befehle
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pm2 status # Status aller Prozesse
|
||||||
|
pm2 logs payload # Logs anzeigen
|
||||||
|
pm2 logs queue-worker # Queue-Worker Logs
|
||||||
|
pm2 restart payload # Neustart
|
||||||
|
pm2 restart all # Alle neustarten
|
||||||
|
pm2 save # Autostart-Konfiguration speichern
|
||||||
|
pm2 startup # Systemd-Integration
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Health Checks
|
||||||
|
|
||||||
|
### Nach Deployment prüfen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. PM2 Status
|
||||||
|
pm2 status
|
||||||
|
|
||||||
|
# 2. Logs auf Fehler prüfen
|
||||||
|
pm2 logs payload --lines 50
|
||||||
|
|
||||||
|
# 3. API erreichbar?
|
||||||
|
curl -I https://cms.c2sgmbh.de/api/users
|
||||||
|
|
||||||
|
# 4. Admin Panel erreichbar?
|
||||||
|
curl -I https://cms.c2sgmbh.de/admin
|
||||||
|
|
||||||
|
# 5. Redis verbunden?
|
||||||
|
redis-cli ping
|
||||||
|
```
|
||||||
|
|
||||||
|
### Automatischer Health Check
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# health-check.sh
|
||||||
|
|
||||||
|
API_URL="https://cms.c2sgmbh.de/api"
|
||||||
|
ADMIN_URL="https://cms.c2sgmbh.de/admin"
|
||||||
|
|
||||||
|
# API Check
|
||||||
|
if curl -sf "$API_URL/users" > /dev/null; then
|
||||||
|
echo "✅ API OK"
|
||||||
|
else
|
||||||
|
echo "❌ API FAILED"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Admin Check
|
||||||
|
if curl -sf "$ADMIN_URL" > /dev/null; then
|
||||||
|
echo "✅ Admin OK"
|
||||||
|
else
|
||||||
|
echo "❌ Admin FAILED"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ All checks passed"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Umgebungsvariablen
|
||||||
|
|
||||||
|
### Production (.env)
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Datenbank
|
||||||
|
DATABASE_URI=postgresql://payload:***@localhost:5432/payload_db
|
||||||
|
PAYLOAD_SECRET=***
|
||||||
|
|
||||||
|
# URLs
|
||||||
|
PAYLOAD_PUBLIC_SERVER_URL=https://cms.c2sgmbh.de
|
||||||
|
NEXT_PUBLIC_SERVER_URL=https://cms.c2sgmbh.de
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
NODE_ENV=production
|
||||||
|
PORT=3001
|
||||||
|
|
||||||
|
# Redis
|
||||||
|
REDIS_URL=redis://localhost:6379
|
||||||
|
|
||||||
|
# Security
|
||||||
|
CSRF_SECRET=***
|
||||||
|
TRUST_PROXY=true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Staging (.env)
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Datenbank (via PgBouncer)
|
||||||
|
DATABASE_URI=postgresql://payload:***@127.0.0.1:6432/payload_db
|
||||||
|
PAYLOAD_SECRET=***
|
||||||
|
|
||||||
|
# URLs
|
||||||
|
PAYLOAD_PUBLIC_SERVER_URL=https://pl.porwoll.tech
|
||||||
|
NEXT_PUBLIC_SERVER_URL=https://pl.porwoll.tech
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
NODE_ENV=production
|
||||||
|
PORT=3000
|
||||||
|
|
||||||
|
# Redis
|
||||||
|
REDIS_URL=redis://localhost:6379
|
||||||
|
|
||||||
|
# Security
|
||||||
|
CSRF_SECRET=***
|
||||||
|
TRUST_PROXY=true
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CI/CD Pipeline
|
||||||
|
|
||||||
|
### GitHub Actions Workflows
|
||||||
|
|
||||||
|
| Workflow | Trigger | Aktion |
|
||||||
|
|----------|---------|--------|
|
||||||
|
| `ci.yml` | Push/PR auf main, develop | Lint, Test, Build |
|
||||||
|
| `security.yml` | Push/PR, Schedule | Security Scanning |
|
||||||
|
| `deploy-staging.yml` | Push auf develop | Auto-Deploy zu Staging |
|
||||||
|
|
||||||
|
### Secrets (GitHub)
|
||||||
|
|
||||||
|
| Secret | Beschreibung |
|
||||||
|
|--------|--------------|
|
||||||
|
| `STAGING_SSH_KEY` | SSH Private Key für sv-payload |
|
||||||
|
|
||||||
|
### Manuelles Deployment triggern
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Via GitHub CLI
|
||||||
|
gh workflow run deploy-staging.yml
|
||||||
|
|
||||||
|
# Mit skip_tests Option
|
||||||
|
gh workflow run deploy-staging.yml -f skip_tests=true
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Backup vor Deployment
|
||||||
|
|
||||||
|
### Automatisches Backup
|
||||||
|
|
||||||
|
Backups laufen täglich um 03:00 Uhr. Vor großen Deployments manuell:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Auf Production
|
||||||
|
~/backup.sh
|
||||||
|
|
||||||
|
# Auf Staging
|
||||||
|
/home/payload/backups/postgres/backup-db.sh --verbose
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backup-Speicherorte
|
||||||
|
|
||||||
|
| Server | Lokal | Offsite |
|
||||||
|
|--------|-------|---------|
|
||||||
|
| Production | ~/backups/ | - |
|
||||||
|
| Staging | /home/payload/backups/postgres/ | s3://c2s/backups/postgres/ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Build schlägt fehl
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Cache löschen
|
||||||
|
rm -rf .next
|
||||||
|
pnpm build
|
||||||
|
|
||||||
|
# Mit mehr Speicher
|
||||||
|
NODE_OPTIONS="--max-old-space-size=4096" pnpm build
|
||||||
|
```
|
||||||
|
|
||||||
|
### PM2 startet nicht
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Logs prüfen
|
||||||
|
pm2 logs payload --lines 100
|
||||||
|
|
||||||
|
# Prozess komplett entfernen und neu starten
|
||||||
|
pm2 delete payload
|
||||||
|
pm2 start ecosystem.config.cjs
|
||||||
|
```
|
||||||
|
|
||||||
|
### Datenbank-Verbindung fehlgeschlagen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# PgBouncer Status (Staging)
|
||||||
|
sudo systemctl status pgbouncer
|
||||||
|
|
||||||
|
# PostgreSQL Status (Production)
|
||||||
|
sudo systemctl status postgresql
|
||||||
|
|
||||||
|
# Verbindung testen
|
||||||
|
psql -h localhost -U payload -d payload_db -c "SELECT 1"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Migration fehlgeschlagen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Status prüfen
|
||||||
|
pnpm payload migrate:status
|
||||||
|
|
||||||
|
# Bei PgBouncer-Problemen
|
||||||
|
./scripts/db-direct.sh migrate
|
||||||
|
|
||||||
|
# Einzelne Migration manuell
|
||||||
|
pnpm payload migrate --name 20251216_073000_add_video_collections
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Checkliste
|
||||||
|
|
||||||
|
### Vor dem Deployment
|
||||||
|
|
||||||
|
- [ ] Alle Tests grün (`pnpm test`)
|
||||||
|
- [ ] Build erfolgreich (`pnpm build`)
|
||||||
|
- [ ] Migrationen getestet auf Staging
|
||||||
|
- [ ] Backup erstellt (bei Datenbank-Änderungen)
|
||||||
|
- [ ] CLAUDE.md/Docs aktualisiert (bei neuen Features)
|
||||||
|
|
||||||
|
### Nach dem Deployment
|
||||||
|
|
||||||
|
- [ ] `pm2 status` - Prozesse online
|
||||||
|
- [ ] Admin Panel erreichbar
|
||||||
|
- [ ] API funktioniert
|
||||||
|
- [ ] Logs auf Fehler geprüft
|
||||||
|
- [ ] Neue Features manuell getestet
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Kontakte
|
||||||
|
|
||||||
|
| Rolle | Kontakt |
|
||||||
|
|-------|---------|
|
||||||
|
| Server Admin | Martin Porwoll |
|
||||||
|
| Repository | https://github.com/complexcaresolutions/cms.c2sgmbh |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Dokumentation: Complex Care Solutions GmbH | 18.12.2025*
|
||||||
|
|
@ -1,85 +1,174 @@
|
||||||
# Payload CMS Multi-Tenant Infrastructure
|
# Infrastruktur Dokumentation
|
||||||
|
|
||||||
> Letzte Aktualisierung: 09.12.2025
|
*Letzte Aktualisierung: 18. Dezember 2025*
|
||||||
|
|
||||||
## Übersicht
|
## Gesamtübersicht
|
||||||
|
|
||||||
Diese Dokumentation beschreibt die Infrastruktur eines Payload CMS 3.x Multi-Tenant-Systems für den Betrieb mehrerer Websites unter einer zentralen CMS-Instanz.
|
|
||||||
|
|
||||||
## Gesamtarchitektur
|
|
||||||
|
|
||||||
```
|
```
|
||||||
┌─────────────────────────────────────────────────────────────────────────────────────┐
|
┌─────────────────────────────────────────────────────────────────────────────────────┐
|
||||||
│ GESAMTARCHITEKTUR │
|
│ INFRASTRUKTUR ÜBERSICHT │
|
||||||
│ │
|
│ │
|
||||||
│ ┌───────────────────────────────────────────────────────────────────────────────┐ │
|
|
||||||
│ │ LOKALE ENTWICKLUNGSUMGEBUNG │ │
|
|
||||||
│ │ (Proxmox VE Cluster) │ │
|
|
||||||
│ │ LAN: 10.10.181.0/24 │ │
|
|
||||||
│ │ │ │
|
|
||||||
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
|
|
||||||
│ │ │ sv-payload │ │ sv-postgres │ │sv-dev-payload│ │sv-analytics │ │ │
|
|
||||||
│ │ │ LXC 700 │ │ LXC 701 │ │ LXC 702 │ │ LXC 703 │ │ │
|
|
||||||
│ │ │ Payload CMS │ │ PostgreSQL │ │ Next.js │ │ Umami │ │ │
|
|
||||||
│ │ │10.10.181.100│ │10.10.181.101│ │10.10.181.102│ │10.10.181.103│ │ │
|
|
||||||
│ │ │ + Redis │ │ │ │ │ │ │ │ │
|
|
||||||
│ │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ │
|
|
||||||
│ └───────────────────────────────────────────────────────────────────────────────┘ │
|
|
||||||
│ │ │
|
|
||||||
│ ┌───────────────────┴───────────────────┐ │
|
|
||||||
│ │ LOKALER INTERNETZUGANG │ │
|
|
||||||
│ │ 850 Mbps ↓ / 50 Mbps ↑ │ │
|
|
||||||
│ │ │ │
|
|
||||||
│ │ Feste IP-Adressen: │ │
|
|
||||||
│ │ 37.24.237.178 - Router │ │
|
|
||||||
│ │ 37.24.237.179 - complexcaresolutions │ │
|
|
||||||
│ │ 37.24.237.180 - Nginx Proxy Manager │ │
|
|
||||||
│ │ 37.24.237.181 - pl.c2sgmbh.de │ │
|
|
||||||
│ │ 37.24.237.182 - frei │ │
|
|
||||||
│ └───────────────────┬───────────────────┘ │
|
|
||||||
│ │ │
|
|
||||||
│ INTERNET │
|
│ INTERNET │
|
||||||
│ │ │
|
│ │ │
|
||||||
│ ┌──────────────────────────────────┼──────────────────────────────────┐ │
|
│ │ Vodafone Business │
|
||||||
│ │ │ │ │
|
│ │ 5 öffentliche IPs │
|
||||||
│ ▼ ▼ ▼ │
|
│ │ │
|
||||||
│ ┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐ │
|
│ ▼ │
|
||||||
│ │ HETZNER 1 │ │ HETZNER 2 │ │ HETZNER 3 │ │
|
│ ┌─────────────────────────────────────────────────────────────────────────────┐ │
|
||||||
│ │ CCS GmbH │ │ Martin Porwoll │ │ Backend/Analytics │ │
|
│ │ UBIQUITI DREAM MACHINE PRO SE │ │
|
||||||
│ │ │ │ │ │ │ │
|
│ │ │ │
|
||||||
│ │ 78.46.87.137 │ │ 94.130.141.114 │ │ 162.55.85.18 │ │
|
│ │ 37.24.237.178 │ Internetzugang │ │
|
||||||
│ │ Debian 12.12 │ │ Ubuntu 24.04 │ │ Debian 13 │ │
|
│ │ 37.24.237.179 │ cloud.complexcaresolutions.de → 10.10.179.100 │ │
|
||||||
│ │ Plesk │ │ Plesk │ │ Native │ │
|
│ │ 37.24.237.180 │ zh3.de (Nginx PM) → 10.10.180.100 │ │
|
||||||
│ │ │ │ │ │ │ │
|
│ │ 37.24.237.181 │ porwoll.tech (Caddy) → 10.10.181.99 │ │
|
||||||
│ │ Next.js Frontends │ │ Next.js Frontends │ │ ✅ Payload CMS │ │
|
│ │ 37.24.237.182 │ FREI (Reserve) │ │
|
||||||
│ │ • complexcare... │ │ • porwoll.de │ │ ✅ Umami │ │
|
│ │ │ │
|
||||||
│ │ • gunshin.de │ │ • caroline-... │ │ ✅ PostgreSQL 17 │ │
|
│ └──────────────────────────────────┬──────────────────────────────────────────┘ │
|
||||||
│ └─────────────────────┘ └─────────────────────┘ │ ✅ Redis Cache │ │
|
│ │
|
||||||
│ │ ✅ Claude Code │ │
|
│ CLOUDFLARE (Proxy) │
|
||||||
│ └─────────────────────┘ │
|
│ ├── zh3.de + Subdomains → 37.24.237.180 │
|
||||||
|
│ ├── porwoll.tech + *.porwoll.tech → 37.24.237.181 │
|
||||||
|
│ └── porwoll.org (intern DNS only) │
|
||||||
|
│ │
|
||||||
|
│ HETZNER (Extern) │
|
||||||
|
│ ├── 78.46.87.137 (Hetzner 1 - zweitmeinu.ng) │
|
||||||
|
│ ├── 94.130.141.114 (Hetzner 2 - Porwoll) │
|
||||||
|
│ └── 162.55.85.18 (Hetzner 3 - Payload Prod) │
|
||||||
│ │
|
│ │
|
||||||
└─────────────────────────────────────────────────────────────────────────────────────┘
|
└─────────────────────────────────────────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Server-Details
|
## Öffentliche IP-Adressen
|
||||||
|
|
||||||
### HETZNER 3 - Backend & Analytics (Produktion)
|
| IP | Verwendung | Ziel (intern) |
|
||||||
|
|----|------------|---------------|
|
||||||
|
| 37.24.237.178 | Internetzugang (Default) | - |
|
||||||
|
| 37.24.237.179 | cloud.complexcaresolutions.de | 10.10.179.100 (Nextcloud) |
|
||||||
|
| 37.24.237.180 | zh3.de (via Cloudflare) | 10.10.180.100 (Nginx PM) |
|
||||||
|
| 37.24.237.181 | porwoll.tech (Cloudflare) | 10.10.181.99 (Caddy) |
|
||||||
|
| 37.24.237.182 | FREI (Reserve) | - |
|
||||||
|
|
||||||
| Eigenschaft | Wert |
|
---
|
||||||
|-------------|------|
|
|
||||||
| **Hostname** | sv-hz03-backend |
|
|
||||||
| **IP-Adresse** | 162.55.85.18 |
|
|
||||||
| **Betriebssystem** | Debian 13 "Trixie" |
|
|
||||||
| **CPU** | AMD Ryzen 5 3600 (6 Cores / 12 Threads) |
|
|
||||||
| **RAM** | 64 GB DDR4 ECC |
|
|
||||||
| **Storage** | 2x 512 GB NVMe SSD (Software RAID 1) |
|
|
||||||
| **Netzwerk** | 1 Gbit/s (garantiert) |
|
|
||||||
| **Traffic** | Unbegrenzt |
|
|
||||||
| **Kosten** | ~€52/Monat |
|
|
||||||
|
|
||||||
#### Services auf Hetzner 3
|
## VLANs
|
||||||
|
|
||||||
|
| VLAN | Name | Subnetz | Zweck |
|
||||||
|
|------|------|---------|-------|
|
||||||
|
| 40 | c2s-prd | 10.10.40.0/24 | Produktion |
|
||||||
|
| 90 | c2s-mgt | 10.10.90.0/24 | Management (Proxmox) |
|
||||||
|
| 179 | c2s-179 | 10.10.179.0/24 | Cloud Services |
|
||||||
|
| 180 | c2s-180 | 10.10.180.0/24 | Web Services (zh3.de) |
|
||||||
|
| 181 | c2s-181 | 10.10.181.0/24 | Development (porwoll.tech) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## VLAN 181 - Development (porwoll.tech)
|
||||||
|
|
||||||
|
| ID | Hostname | IP | Service | Status |
|
||||||
|
|----|----------|-----|---------|--------|
|
||||||
|
| 699 | sv-caddy | 10.10.181.99 | Caddy Reverse Proxy | ✅ Running |
|
||||||
|
| 700 | sv-payload | 10.10.181.100 | Payload CMS Dev | ✅ Running |
|
||||||
|
| 701 | sv-postgres | 10.10.181.101 | PostgreSQL 17 + Redis Commander | ✅ Running |
|
||||||
|
| 702 | sv-dev-payload | 10.10.181.102 | Payload Test | ⏸️ Stopped |
|
||||||
|
| 703 | sv-analytics | 10.10.181.103 | Umami Analytics | ✅ Running |
|
||||||
|
| 704 | sv-frontend | 10.10.181.104 | Frontend Dev (9 Projekte) | ✅ Running |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## sv-frontend (LXC 704) - Frontend Development
|
||||||
|
|
||||||
|
**SSH:** `ssh frontend@10.10.181.104`
|
||||||
|
|
||||||
|
### Software Stack
|
||||||
|
- Node.js 22.x
|
||||||
|
- pnpm
|
||||||
|
- Next.js 16.0.10
|
||||||
|
- Claude Code 2.0.72
|
||||||
|
- Codex CLI 0.73.0
|
||||||
|
- Gemini CLI 0.21.2
|
||||||
|
|
||||||
|
### Projekte & Ports
|
||||||
|
|
||||||
|
| Port | Service | Repository | URL |
|
||||||
|
|------|---------|------------|-----|
|
||||||
|
| 3000 | frontend-porwoll | frontend.porwoll.de | porwoll-dev.porwoll.tech |
|
||||||
|
| 3001 | frontend-blogwoman | frontend.blogwoman.de | blogwoman-dev.porwoll.tech |
|
||||||
|
| 3002 | frontend-caroline-com | frontend.caroline-porwoll.com | caroline-com-dev.porwoll.tech |
|
||||||
|
| 3003 | frontend-caroline-de | frontend.caroline-porwoll.de | caroline-de-dev.porwoll.tech |
|
||||||
|
| 3004 | frontend-ccs | frontend.complexcaresolutions.de | ccs-dev.porwoll.tech |
|
||||||
|
| 3005 | frontend-gunshin | frontend.gunshin.de | gunshin-dev.porwoll.tech |
|
||||||
|
| 3006 | frontend-sensual | frontend.sensualmoment.de | sensual-dev.porwoll.tech |
|
||||||
|
| 3007 | frontend-zweitmeinu | frontend.zweitmeinu.ng | zweitmeinu-dev.porwoll.tech |
|
||||||
|
| 3008 | frontend-zytoskandal | frontend.zytoskandal.de | zytoskandal-dev.porwoll.tech |
|
||||||
|
|
||||||
|
### Service-Verwaltung
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Service starten
|
||||||
|
systemctl start frontend-porwoll
|
||||||
|
|
||||||
|
# Service stoppen
|
||||||
|
systemctl stop frontend-porwoll
|
||||||
|
|
||||||
|
# Alle Status
|
||||||
|
systemctl status frontend-*
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## sv-caddy (LXC 699) - Reverse Proxy
|
||||||
|
|
||||||
|
- **IP:** 10.10.181.99
|
||||||
|
- **Software:** Caddy 2.9.x + Cloudflare DNS Plugin
|
||||||
|
- **SSL:** Wildcard *.porwoll.tech via Let's Encrypt DNS-Challenge
|
||||||
|
- **Config:** `/etc/caddy/Caddyfile`
|
||||||
|
|
||||||
|
### Routing
|
||||||
|
|
||||||
|
| URL | Backend |
|
||||||
|
|-----|---------|
|
||||||
|
| pl.porwoll.tech | 10.10.181.100:3000 |
|
||||||
|
| redis.porwoll.tech | 10.10.181.101:8081 |
|
||||||
|
| umami.porwoll.tech | 10.10.181.103:3000 |
|
||||||
|
| *-dev.porwoll.tech | 10.10.181.104:300x |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## GitHub Organisation: complexcaresolutions
|
||||||
|
|
||||||
|
| Repository | Beschreibung | Visibility |
|
||||||
|
|------------|--------------|------------|
|
||||||
|
| cms.c2sgmbh | Payload CMS Backend | Internal |
|
||||||
|
| frontend.porwoll.de | porwoll.de Frontend | Internal |
|
||||||
|
| frontend.blogwoman.de | blogwoman.de Frontend | Internal |
|
||||||
|
| frontend.caroline-porwoll.com | caroline-porwoll.com Frontend | Internal |
|
||||||
|
| frontend.caroline-porwoll.de | caroline-porwoll.de Frontend | Internal |
|
||||||
|
| frontend.complexcaresolutions.de | CCS Website Frontend | Internal |
|
||||||
|
| frontend.gunshin.de | gunshin.de Frontend | Internal |
|
||||||
|
| frontend.sensualmoment.de | sensualmoment.de Frontend | Internal |
|
||||||
|
| frontend.zweitmeinu.ng | zweitmeinu.ng Frontend | Internal |
|
||||||
|
| frontend.zytoskandal.de | zytoskandal.de Frontend | Internal |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Hetzner 3 - Payload Production
|
||||||
|
|
||||||
|
- **IP:** 162.55.85.18
|
||||||
|
- **Domain:** cms.c2sgmbh.de
|
||||||
|
- **User:** payload
|
||||||
|
- **SSH:** `ssh payload@162.55.85.18`
|
||||||
|
|
||||||
|
### Software
|
||||||
|
- Payload CMS 3.68.4
|
||||||
|
- Next.js 15.5.9
|
||||||
|
- React 19.2.3
|
||||||
|
- PostgreSQL 17
|
||||||
|
- Redis
|
||||||
|
- Umami Analytics
|
||||||
|
|
||||||
|
### Services
|
||||||
|
|
||||||
| Service | User | Port | URL | Status |
|
| Service | User | Port | URL | Status |
|
||||||
|---------|------|------|-----|--------|
|
|---------|------|------|-----|--------|
|
||||||
|
|
@ -88,501 +177,66 @@ Diese Dokumentation beschreibt die Infrastruktur eines Payload CMS 3.x Multi-Ten
|
||||||
| Umami Analytics | umami | 3000 | https://analytics.c2sgmbh.de | ✅ Läuft |
|
| Umami Analytics | umami | 3000 | https://analytics.c2sgmbh.de | ✅ Läuft |
|
||||||
| Redis Cache | redis | 6379 | localhost | ✅ Läuft |
|
| Redis Cache | redis | 6379 | localhost | ✅ Läuft |
|
||||||
| Nginx | root | 80/443 | Reverse Proxy | ✅ Läuft |
|
| Nginx | root | 80/443 | Reverse Proxy | ✅ Läuft |
|
||||||
| Claude Code | claude | - | CLI Tool | ✅ Installiert |
|
|
||||||
|
|
||||||
#### System-User
|
|
||||||
|
|
||||||
| User | Zweck | Home-Verzeichnis |
|
|
||||||
|------|-------|------------------|
|
|
||||||
| root | System-Administration | /root |
|
|
||||||
| payload | Payload CMS | /home/payload |
|
|
||||||
| umami | Umami Analytics | /home/umami |
|
|
||||||
| claude | Claude Code / Server-Admin | /home/claude |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### HETZNER 1 - Complex Care Solutions GmbH
|
|
||||||
|
|
||||||
| Eigenschaft | Wert |
|
|
||||||
|-------------|------|
|
|
||||||
| **Eigentümer** | Complex Care Solutions GmbH |
|
|
||||||
| **IP-Adresse** | 78.46.87.137 |
|
|
||||||
| **Betriebssystem** | Debian 12.12 |
|
|
||||||
| **Control Panel** | Plesk Web Pro Edition 18.0.73 |
|
|
||||||
| **CPU** | AMD Ryzen 7 Pro 8700GE |
|
|
||||||
| **RAM** | 64 GB |
|
|
||||||
| **Storage** | 2x 512 GB NVMe SSD (Software RAID 1) |
|
|
||||||
|
|
||||||
#### Domains auf Hetzner 1
|
|
||||||
|
|
||||||
| Domain | Zweck |
|
|
||||||
|--------|-------|
|
|
||||||
| **complexcaresolutions.de** | Hauptdomain |
|
|
||||||
| **gunshin.de** | Portfolio/Holding |
|
|
||||||
| c2sgmbh.de | Kurzform → Redirect |
|
|
||||||
| zweitmeinung-*.de | Fachgebiete → Redirect |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### HETZNER 2 - Martin Porwoll (privat)
|
|
||||||
|
|
||||||
| Eigenschaft | Wert |
|
|
||||||
|-------------|------|
|
|
||||||
| **Eigentümer** | Martin Porwoll (privat) |
|
|
||||||
| **IP-Adresse** | 94.130.141.114 |
|
|
||||||
| **Betriebssystem** | Ubuntu 24.04 LTS |
|
|
||||||
| **Control Panel** | Plesk Web Pro Edition 18.0.73 |
|
|
||||||
| **CPU** | Intel Xeon E3-1275v6 |
|
|
||||||
| **RAM** | 64 GB |
|
|
||||||
| **Storage** | 2x 512 GB NVMe SSD (Software RAID 1) |
|
|
||||||
|
|
||||||
#### Domains auf Hetzner 2
|
|
||||||
|
|
||||||
| Domain | Zweck |
|
|
||||||
|--------|-------|
|
|
||||||
| **porwoll.de** | Hauptdomain |
|
|
||||||
| **caroline-porwoll.de** | Dr. Caroline Porwoll |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Lokale Infrastruktur (Proxmox) - Entwicklung
|
|
||||||
|
|
||||||
| Server | IP | Port | Funktion | OS |
|
|
||||||
|--------|-----|------|----------|-----|
|
|
||||||
| sv-payload | 10.10.181.100 | 3000 | Payload CMS (Dev) + Redis | Debian 13 |
|
|
||||||
| sv-postgres | 10.10.181.101 | 5432 | PostgreSQL (Dev) | Debian 13 |
|
|
||||||
| sv-dev-payload | 10.10.181.102 | 3001 | Next.js Frontend | Debian 13 |
|
|
||||||
| sv-analytics | 10.10.181.103 | 3000 | Umami (Dev) | Debian 13 |
|
|
||||||
|
|
||||||
#### Feste IP-Adressen (Lokal → Internet)
|
|
||||||
|
|
||||||
| IP | Verwendung |
|
|
||||||
|----|------------|
|
|
||||||
| 37.24.237.178 | Router / Gateway |
|
|
||||||
| 37.24.237.179 | complexcaresolutions.cloud |
|
|
||||||
| 37.24.237.180 | Nginx Proxy Manager |
|
|
||||||
| 37.24.237.181 | pl.c2sgmbh.de (Payload Dev) |
|
|
||||||
| 37.24.237.182 | **Frei** |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Credentials
|
|
||||||
|
|
||||||
### Produktion (sv-hz03-backend)
|
|
||||||
|
|
||||||
#### PostgreSQL
|
|
||||||
|
|
||||||
| Datenbank | User | Passwort |
|
|
||||||
|-----------|------|----------|
|
|
||||||
| payload_db | payload | Suchen55 |
|
|
||||||
| umami_db | umami | Suchen55 |
|
|
||||||
|
|
||||||
#### Redis
|
|
||||||
|
|
||||||
|
### Deploy
|
||||||
```bash
|
```bash
|
||||||
redis-cli -h localhost -p 6379
|
~/deploy.sh
|
||||||
# Kein Passwort (nur localhost)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Environment Variables - Payload (.env)
|
|
||||||
|
|
||||||
```env
|
|
||||||
DATABASE_URI=postgresql://payload:Suchen55@localhost:5432/payload_db
|
|
||||||
PAYLOAD_SECRET=hxPARlMkmv+ZdCOAMw+N4o2x4mNbERB237iDQTYXALY=
|
|
||||||
PAYLOAD_PUBLIC_SERVER_URL=https://cms.c2sgmbh.de
|
|
||||||
NEXT_PUBLIC_SERVER_URL=https://cms.c2sgmbh.de
|
|
||||||
NODE_ENV=production
|
|
||||||
PORT=3001
|
|
||||||
REDIS_HOST=localhost
|
|
||||||
REDIS_PORT=6379
|
|
||||||
```
|
|
||||||
|
|
||||||
### Entwicklung (pl.c2sgmbh.de)
|
|
||||||
|
|
||||||
#### PostgreSQL (sv-postgres)
|
|
||||||
|
|
||||||
| Datenbank | User | Passwort |
|
|
||||||
|-----------|------|----------|
|
|
||||||
| payload_db | payload | Finden55 |
|
|
||||||
|
|
||||||
#### Environment Variables (.env)
|
|
||||||
|
|
||||||
```env
|
|
||||||
DATABASE_URI=postgresql://payload:Finden55@10.10.181.101:5432/payload_db
|
|
||||||
PAYLOAD_SECRET=a53b254070d3fffd2b5cfcc3
|
|
||||||
PAYLOAD_PUBLIC_SERVER_URL=https://pl.c2sgmbh.de
|
|
||||||
NEXT_PUBLIC_SERVER_URL=https://pl.c2sgmbh.de
|
|
||||||
NODE_ENV=production
|
|
||||||
PORT=3000
|
|
||||||
REDIS_HOST=localhost
|
|
||||||
REDIS_PORT=6379
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Multi-Tenant Konzept
|
## Development Workflow
|
||||||
|
|
||||||
Das System verwendet `@payloadcms/plugin-multi-tenant` für die Mandantenfähigkeit.
|
|
||||||
|
|
||||||
### Tenants (Mandanten)
|
|
||||||
|
|
||||||
Jeder Tenant repräsentiert eine separate Website:
|
|
||||||
|
|
||||||
| Tenant | Slug | Domains |
|
|
||||||
|--------|------|---------|
|
|
||||||
| porwoll.de | porwoll | porwoll.de, www.porwoll.de |
|
|
||||||
| Complex Care Solutions GmbH | c2s | complexcaresolutions.de |
|
|
||||||
| Gunshin | gunshin | gunshin.de |
|
|
||||||
|
|
||||||
### Datenisolation
|
|
||||||
|
|
||||||
- Jeder Content (Media, Pages, Posts etc.) gehört zu genau einem Tenant
|
|
||||||
- User werden Tenants zugewiesen und sehen nur deren Inhalte
|
|
||||||
- Die Domain-Erkennung erfolgt automatisch durch das Plugin
|
|
||||||
|
|
||||||
### Datenbank-Tabellen
|
|
||||||
|
|
||||||
```
|
```
|
||||||
tenants - Mandanten-Stammdaten
|
DEVELOPMENT STAGING PRODUCTION
|
||||||
tenants_domains - Domain-Zuordnungen
|
sv-frontend → sv-payload → Hetzner 03
|
||||||
users_tenants - User-Mandanten-Beziehung (N:M)
|
porwoll.tech pl.porwoll.tech cms.c2sgmbh.de
|
||||||
|
develop branch main branch
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
**AI Tools:** Claude Code, Codex CLI, Gemini CLI, VS Code Remote-SSH
|
||||||
|
|
||||||
## Redis Caching
|
|
||||||
|
|
||||||
### Architektur
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────────────────────────────────────────┐
|
|
||||||
│ REDIS CACHING STRATEGIE │
|
|
||||||
│ │
|
|
||||||
│ Request → Payload CMS → Redis Cache? │
|
|
||||||
│ │ │
|
|
||||||
│ ┌────┴────┐ │
|
|
||||||
│ HIT MISS │
|
|
||||||
│ │ │ │
|
|
||||||
│ ▼ ▼ │
|
|
||||||
│ Return PostgreSQL → Cache in Redis → Return │
|
|
||||||
│ │
|
|
||||||
│ Cache-Typen: │
|
|
||||||
│ • API Response Cache (GET /api/pages, /api/posts) │
|
|
||||||
│ • Automatische Invalidierung bei Content-Änderungen │
|
|
||||||
│ │
|
|
||||||
│ Konfiguration: │
|
|
||||||
│ • Max Memory: 2GB (Prod) / 512MB (Dev) │
|
|
||||||
│ • Eviction: allkeys-lru │
|
|
||||||
│ • TTL: 5 Minuten (Standard) │
|
|
||||||
│ │
|
|
||||||
└─────────────────────────────────────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
### Redis Befehle
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Status prüfen
|
|
||||||
redis-cli ping
|
|
||||||
|
|
||||||
# Statistiken
|
|
||||||
redis-cli info stats
|
|
||||||
|
|
||||||
# Cache-Keys anzeigen
|
|
||||||
redis-cli keys "*"
|
|
||||||
|
|
||||||
# Cache leeren
|
|
||||||
redis-cli flushdb
|
|
||||||
|
|
||||||
# Live-Monitoring
|
|
||||||
redis-cli monitor
|
|
||||||
```
|
|
||||||
|
|
||||||
### Background Jobs (geplant)
|
|
||||||
|
|
||||||
**Evaluation (09.12.2025): BullMQ vs Agenda.js**
|
|
||||||
|
|
||||||
| Kriterium | BullMQ | Agenda.js |
|
|
||||||
|-----------|--------|-----------|
|
|
||||||
| Database | Redis ✅ | MongoDB ❌ |
|
|
||||||
| TypeScript | Native ✅ | Begrenzt ⚠️ |
|
|
||||||
| Priority Jobs | Ja ✅ | Nein ❌ |
|
|
||||||
| Rate Limiting | Ja ✅ | Nein ❌ |
|
|
||||||
| UI Dashboard | @bull-board ✅ | Keine ❌ |
|
|
||||||
|
|
||||||
**Entscheidung: BullMQ** (Redis bereits vorhanden, TypeScript-native, @bull-board UI)
|
|
||||||
|
|
||||||
**Geplante Konfiguration:**
|
|
||||||
```bash
|
|
||||||
# Environment Variables (wenn implementiert)
|
|
||||||
QUEUE_REDIS_URL=redis://localhost:6379/1 # Separate DB für Jobs
|
|
||||||
QUEUE_CONCURRENCY=5 # Parallele Worker
|
|
||||||
QUEUE_DEFAULT_RETRY=3 # Wiederholungsversuche
|
|
||||||
```
|
|
||||||
|
|
||||||
**PM2 Worker-Konfiguration (geplant):**
|
|
||||||
```javascript
|
|
||||||
// ecosystem.config.cjs - Erweiterung
|
|
||||||
{
|
|
||||||
name: 'queue-worker',
|
|
||||||
script: './scripts/run-queue-worker.ts',
|
|
||||||
instances: 1,
|
|
||||||
max_memory_restart: '500M'
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Deployment Workflow
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────────────────────────────────────────┐
|
|
||||||
│ DEPLOYMENT WORKFLOW │
|
|
||||||
│ │
|
|
||||||
│ ┌──────────────────────────────────┐ ┌──────────────────────────────────┐ │
|
|
||||||
│ │ ENTWICKLUNG (DEV) │ │ PRODUKTION (PROD) │ │
|
|
||||||
│ │ pl.c2sgmbh.de │ │ cms.c2sgmbh.de │ │
|
|
||||||
│ │ 37.24.237.181 │ │ 162.55.85.18 │ │
|
|
||||||
│ └──────────────────────────────────┘ └──────────────────────────────────┘ │
|
|
||||||
│ │
|
|
||||||
│ Step 1: CODE ENTWICKELN │
|
|
||||||
│ ┌────────────────────────────────────────────────────────────────────────┐ │
|
|
||||||
│ │ cd /home/payload/payload-cms │ │
|
|
||||||
│ │ pnpm dev # Lokal testen │ │
|
|
||||||
│ │ pnpm build # Build-Test │ │
|
|
||||||
│ │ pm2 restart payload # Auf Dev-Server deployen │ │
|
|
||||||
│ └────────────────────────────────────────────────────────────────────────┘ │
|
|
||||||
│ ↓ │
|
|
||||||
│ │
|
|
||||||
│ Step 2: ZU GITHUB PUSHEN │
|
|
||||||
│ ┌────────────────────────────────────────────────────────────────────────┐ │
|
|
||||||
│ │ git add . # Alle Änderungen stagen │ │
|
|
||||||
│ │ git commit -m "feat: XYZ" # Commit erstellen │ │
|
|
||||||
│ │ git push origin main # Zu GitHub pushen │ │
|
|
||||||
│ └────────────────────────────────────────────────────────────────────────┘ │
|
|
||||||
│ ↓ │
|
|
||||||
│ │
|
|
||||||
│ ┌────────────────────────────────────────────────────┐ │
|
|
||||||
│ │ GITHUB REPOSITORY (PRIVAT) │ │
|
|
||||||
│ │ https://github.com/c2s-admin/cms.c2sgmbh │ │
|
|
||||||
│ └────────────────────────────────────────────────────┘ │
|
|
||||||
│ ↓ │
|
|
||||||
│ │
|
|
||||||
│ Step 3: AUF PRODUKTION DEPLOYEN │
|
|
||||||
│ ┌────────────────────────────────────────────────────────────────────────┐ │
|
|
||||||
│ │ ssh payload@162.55.85.18 │ │
|
|
||||||
│ │ ~/deploy.sh # Automatisches Deployment │ │
|
|
||||||
│ │ │ │
|
|
||||||
│ │ Das deploy.sh Script macht: │ │
|
|
||||||
│ │ ├─ git pull origin main # Code von GitHub holen │ │
|
|
||||||
│ │ ├─ pnpm install # Dependencies aktualisieren │ │
|
|
||||||
│ │ ├─ pnpm build # Produktions-Build │ │
|
|
||||||
│ │ └─ pm2 restart payload # Service neustarten │ │
|
|
||||||
│ └────────────────────────────────────────────────────────────────────────┘ │
|
|
||||||
│ │
|
|
||||||
└─────────────────────────────────────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
### Git-Setup auf Servern
|
|
||||||
|
|
||||||
| Server | User | Remote | Auth-Methode | Status |
|
|
||||||
|--------|------|--------|--------------|--------|
|
|
||||||
| pl.c2sgmbh.de (Dev) | payload | HTTPS | GitHub CLI (`gh auth`) | ✅ Konfiguriert |
|
|
||||||
| cms.c2sgmbh.de (Prod) | payload | SSH | SSH-Key | ✅ Eingerichtet |
|
|
||||||
|
|
||||||
### Deployment-Befehle
|
|
||||||
|
|
||||||
**Entwicklungsserver → GitHub:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /home/payload/payload-cms
|
|
||||||
git status
|
|
||||||
pnpm build
|
|
||||||
pm2 restart payload
|
|
||||||
git add .
|
|
||||||
git commit -m "feat: Beschreibung der Änderung"
|
|
||||||
git push origin main
|
|
||||||
```
|
|
||||||
|
|
||||||
**GitHub → Produktionsserver:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Option A: SSH + Deploy-Script (empfohlen)
|
|
||||||
ssh payload@162.55.85.18 '~/deploy.sh'
|
|
||||||
|
|
||||||
# Option B: Manuelles SSH-Login
|
|
||||||
ssh payload@162.55.85.18
|
|
||||||
cd ~/payload-cms
|
|
||||||
git pull origin main
|
|
||||||
pnpm install
|
|
||||||
pnpm build
|
|
||||||
pm2 restart payload
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Backup
|
|
||||||
|
|
||||||
### Backup-Script (~/backup.sh)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
BACKUP_DIR=~/backups
|
|
||||||
DATE=$(date +%Y-%m-%d_%H-%M-%S)
|
|
||||||
RETENTION_DAYS=7
|
|
||||||
|
|
||||||
mkdir -p $BACKUP_DIR
|
|
||||||
|
|
||||||
# PostgreSQL Backup
|
|
||||||
PGPASSWORD=Suchen55 pg_dump -h localhost -U payload payload_db > $BACKUP_DIR/payload_db_$DATE.sql
|
|
||||||
gzip $BACKUP_DIR/payload_db_$DATE.sql
|
|
||||||
|
|
||||||
# Alte Backups löschen
|
|
||||||
find $BACKUP_DIR -name "*.sql.gz" -mtime +$RETENTION_DAYS -delete
|
|
||||||
```
|
|
||||||
|
|
||||||
### Cronjob (täglich 3:00 Uhr)
|
|
||||||
|
|
||||||
```
|
|
||||||
0 3 * * * /home/payload/backup.sh >> /home/payload/backups/backup.log 2>&1
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Service-Management
|
|
||||||
|
|
||||||
### PM2 Befehle
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pm2 status # Status
|
|
||||||
pm2 logs payload # Logs
|
|
||||||
pm2 restart payload # Neustart
|
|
||||||
pm2 save # Autostart speichern
|
|
||||||
```
|
|
||||||
|
|
||||||
### Systemd Services
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# PostgreSQL
|
|
||||||
systemctl status postgresql
|
|
||||||
systemctl restart postgresql
|
|
||||||
|
|
||||||
# Nginx
|
|
||||||
systemctl status nginx
|
|
||||||
systemctl restart nginx
|
|
||||||
nginx -t # Config testen
|
|
||||||
|
|
||||||
# Redis
|
|
||||||
systemctl status redis-server
|
|
||||||
systemctl restart redis-server
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## URLs Übersicht
|
## URLs Übersicht
|
||||||
|
|
||||||
| Service | Entwicklung | Produktion |
|
### Development (porwoll.tech)
|
||||||
|---------|-------------|------------|
|
|
||||||
| Payload Admin | https://pl.c2sgmbh.de/admin | https://cms.c2sgmbh.de/admin |
|
| Service | URL |
|
||||||
| Payload API | https://pl.c2sgmbh.de/api | https://cms.c2sgmbh.de/api |
|
|---------|-----|
|
||||||
| Umami | - | https://analytics.c2sgmbh.de |
|
| Portal | https://porwoll.tech |
|
||||||
|
| Payload CMS | https://pl.porwoll.tech |
|
||||||
|
| Redis Commander | https://redis.porwoll.tech |
|
||||||
|
| Umami Analytics | https://umami.porwoll.tech |
|
||||||
|
| Frontend porwoll.de | https://porwoll-dev.porwoll.tech |
|
||||||
|
| (8 weitere) | https://*-dev.porwoll.tech |
|
||||||
|
|
||||||
|
### Production (Hetzner)
|
||||||
|
|
||||||
|
| Service | URL |
|
||||||
|
|---------|-----|
|
||||||
|
| Payload Admin | https://cms.c2sgmbh.de/admin |
|
||||||
|
| Payload API | https://cms.c2sgmbh.de/api |
|
||||||
|
| Umami Analytics | https://analytics.c2sgmbh.de |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## SSH Schnellzugriff
|
## Quick Reference
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Produktion (Hetzner 3)
|
# Frontend-Server
|
||||||
ssh root@162.55.85.18 # Root
|
ssh frontend@10.10.181.104
|
||||||
ssh payload@162.55.85.18 # Payload User
|
|
||||||
ssh umami@162.55.85.18 # Umami User
|
|
||||||
ssh claude@162.55.85.18 # Claude Code
|
|
||||||
|
|
||||||
# Hetzner Server
|
# Hetzner 3 Production
|
||||||
ssh root@78.46.87.137 # Hetzner 1 (CCS)
|
ssh payload@162.55.85.18
|
||||||
ssh root@94.130.141.114 # Hetzner 2 (Porwoll)
|
|
||||||
|
|
||||||
# Entwicklung (Proxmox)
|
# Caddy neu laden
|
||||||
ssh payload@10.10.181.100 # sv-payload
|
ssh root@10.10.181.99 "systemctl reload caddy"
|
||||||
ssh root@10.10.181.101 # sv-postgres
|
|
||||||
|
# Frontend Service starten
|
||||||
|
systemctl start frontend-porwoll
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Netzwerk & Firewall
|
*Dokumentation: Martin Porwoll | Complex Care Solutions GmbH | 18.12.2025*
|
||||||
|
|
||||||
### UFW Regeln auf sv-payload (Dev)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
22/tcp ALLOW 10.10.181.0/24 # SSH aus VLAN
|
|
||||||
80/tcp ALLOW Anywhere # HTTP (ACME)
|
|
||||||
443/tcp ALLOW Anywhere # HTTPS
|
|
||||||
```
|
|
||||||
|
|
||||||
### UFW Regeln auf sv-postgres (Dev)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
22/tcp ALLOW 10.10.181.0/24 # SSH aus VLAN
|
|
||||||
5432/tcp ALLOW 10.10.181.100 # PostgreSQL nur von Payload
|
|
||||||
```
|
|
||||||
|
|
||||||
### UFW Regeln auf sv-hz03-backend (Prod)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
22/tcp ALLOW Anywhere # SSH
|
|
||||||
80/tcp ALLOW Anywhere # HTTP
|
|
||||||
443/tcp ALLOW Anywhere # HTTPS
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## SSL Zertifikate
|
|
||||||
|
|
||||||
| Domain | Anbieter | Status |
|
|
||||||
|--------|----------|--------|
|
|
||||||
| pl.c2sgmbh.de | Let's Encrypt (Caddy) | Auto-Renewal |
|
|
||||||
| cms.c2sgmbh.de | Let's Encrypt (Certbot) | Auto-Renewal |
|
|
||||||
| analytics.c2sgmbh.de | Let's Encrypt (Certbot) | Auto-Renewal |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Tech Stack
|
|
||||||
|
|
||||||
| Komponente | Technologie | Version |
|
|
||||||
|------------|-------------|---------|
|
|
||||||
| CMS | Payload CMS | 3.x |
|
|
||||||
| Framework | Next.js | 15.4.7 |
|
|
||||||
| Runtime | Node.js | 22.x |
|
|
||||||
| Datenbank | PostgreSQL | 17 |
|
|
||||||
| Cache | Redis | 7.x |
|
|
||||||
| Analytics | Umami | 3.x |
|
|
||||||
| Process Manager | PM2 | Latest |
|
|
||||||
| Package Manager | pnpm | Latest |
|
|
||||||
| Reverse Proxy (Dev) | Caddy | 2.10.2 |
|
|
||||||
| Reverse Proxy (Prod) | Nginx | Latest |
|
|
||||||
| SSL | Let's Encrypt | - |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## DSGVO-Konformität
|
|
||||||
|
|
||||||
Die Architektur wurde bewusst ohne Cloudflare designed:
|
|
||||||
|
|
||||||
- Keine US-Dienste im Datenpfad für Admin-Zugriffe
|
|
||||||
- Direkte öffentliche IP statt Proxy
|
|
||||||
- Keine Auftragsverarbeiter-Verträge für CDN nötig
|
|
||||||
- Redakteur-IPs und Sessions bleiben in DE
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Checkliste nach Deployment
|
|
||||||
|
|
||||||
- [ ] `pm2 status` - Alle Prozesse online?
|
|
||||||
- [ ] `redis-cli ping` - Redis antwortet?
|
|
||||||
- [ ] Admin Panel erreichbar?
|
|
||||||
- [ ] `pm2 logs payload --lines 10` - Keine Fehler?
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Stand: 09. Dezember 2025*
|
|
||||||
|
|
|
||||||
206
docs/PROJECT_STATUS.md
Normal file
206
docs/PROJECT_STATUS.md
Normal file
|
|
@ -0,0 +1,206 @@
|
||||||
|
# Projekt Status - Dezember 2025
|
||||||
|
|
||||||
|
**Stand:** 18. Dezember 2025
|
||||||
|
|
||||||
|
## Zusammenfassung
|
||||||
|
|
||||||
|
Die komplette Entwicklungsinfrastruktur ist eingerichtet und funktionsfähig:
|
||||||
|
- Payload CMS Multi-Tenant (Dev + Prod)
|
||||||
|
- Multi-Frontend Development Environment
|
||||||
|
- AI-gestützte Entwicklungstools
|
||||||
|
- Reverse Proxy Stack (Caddy + Nginx)
|
||||||
|
- Analytics (Umami)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Abgeschlossen
|
||||||
|
|
||||||
|
### Infrastruktur VLAN 181 (Development)
|
||||||
|
|
||||||
|
| LXC | Hostname | IP | Service | Status |
|
||||||
|
|-----|----------|-----|---------|--------|
|
||||||
|
| 699 | sv-caddy | 10.10.181.99 | Caddy Reverse Proxy | ✅ Running |
|
||||||
|
| 700 | sv-payload | 10.10.181.100 | Payload CMS + Redis | ✅ Running |
|
||||||
|
| 701 | sv-postgres | 10.10.181.101 | PostgreSQL 17 + Redis Cmd | ✅ Running |
|
||||||
|
| 702 | sv-dev-payload | 10.10.181.102 | Payload Experimental | ⏸️ Stopped |
|
||||||
|
| 703 | sv-analytics | 10.10.181.103 | Umami Analytics | ✅ Running |
|
||||||
|
| 704 | sv-frontend | 10.10.181.104 | Multi-Project Next.js | ✅ Running |
|
||||||
|
|
||||||
|
### Hetzner 3 (Production)
|
||||||
|
|
||||||
|
- [x] Debian 13 Installation
|
||||||
|
- [x] PostgreSQL 17 mit payload_db und umami_db
|
||||||
|
- [x] Redis Cache
|
||||||
|
- [x] Payload CMS Production (cms.c2sgmbh.de)
|
||||||
|
- [x] Umami Analytics Production (analytics.c2sgmbh.de)
|
||||||
|
- [x] Nginx Reverse Proxy mit Let's Encrypt
|
||||||
|
- [x] PM2 Process Management
|
||||||
|
- [x] Claude Code CLI
|
||||||
|
- [x] Backup-Scripts (täglich)
|
||||||
|
- [x] CVE-2025-55182 Hotfix
|
||||||
|
|
||||||
|
### Caddy Reverse Proxy (sv-caddy)
|
||||||
|
|
||||||
|
- [x] Caddy 2.9.x mit Cloudflare DNS Plugin
|
||||||
|
- [x] Wildcard SSL für *.porwoll.tech
|
||||||
|
- [x] Cloudflare DNS-Challenge
|
||||||
|
- [x] Routing für alle Services
|
||||||
|
- [x] Security Headers
|
||||||
|
|
||||||
|
### Payload CMS
|
||||||
|
|
||||||
|
- [x] Multi-Tenant Plugin
|
||||||
|
- [x] Redis Caching
|
||||||
|
- [x] Package-Versionen synchronisiert:
|
||||||
|
- Next.js 15.5.9
|
||||||
|
- React 19.2.3
|
||||||
|
- Payload 3.68.4
|
||||||
|
- [x] GitHub Repository (complexcaresolutions/cms.c2sgmbh)
|
||||||
|
|
||||||
|
### sv-frontend (Multi-Project)
|
||||||
|
|
||||||
|
- [x] Node.js 22.x + pnpm
|
||||||
|
- [x] AI-Tools installiert:
|
||||||
|
- Claude Code 2.0.72
|
||||||
|
- Codex CLI 0.73.0
|
||||||
|
- Gemini CLI 0.21.2
|
||||||
|
- [x] 9 Frontend-Projekte initialisiert
|
||||||
|
- [x] Systemd Services (Ports 3000-3008)
|
||||||
|
- [x] SSH-Zugriff mit Key
|
||||||
|
- [x] VS Code Remote-SSH kompatibel
|
||||||
|
|
||||||
|
### GitHub Repositories
|
||||||
|
|
||||||
|
Organisation: **complexcaresolutions** (Internal)
|
||||||
|
|
||||||
|
- [x] cms.c2sgmbh (Payload CMS)
|
||||||
|
- [x] frontend.porwoll.de
|
||||||
|
- [x] frontend.blogwoman.de
|
||||||
|
- [x] frontend.caroline-porwoll.com
|
||||||
|
- [x] frontend.caroline-porwoll.de
|
||||||
|
- [x] frontend.complexcaresolutions.de
|
||||||
|
- [x] frontend.gunshin.de
|
||||||
|
- [x] frontend.sensualmoment.de
|
||||||
|
- [x] frontend.zweitmeinu.ng
|
||||||
|
- [x] frontend.zytoskandal.de
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ Kritische Aufgaben
|
||||||
|
|
||||||
|
| Priorität | Aufgabe | Status |
|
||||||
|
|-----------|---------|--------|
|
||||||
|
| 🔴 | Umami Dev Admin-Passwort ändern | Offen |
|
||||||
|
| 🔴 | auth.zh3.de - 502 Bad Gateway beheben | Offen |
|
||||||
|
| 🟡 | pve04/pve05/Backup: Enterprise Repo | Offen |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔜 Nächste Schritte
|
||||||
|
|
||||||
|
### Kurzfristig (diese Woche)
|
||||||
|
|
||||||
|
1. [ ] Porwoll.de Frontend-Entwicklung starten
|
||||||
|
2. [ ] VS Code Remote-SSH testen
|
||||||
|
3. [ ] Claude Code auf sv-frontend authentifizieren
|
||||||
|
|
||||||
|
### Mittelfristig (Januar 2025)
|
||||||
|
|
||||||
|
4. [ ] Frontend-Staging auf Hetzner 3 einrichten
|
||||||
|
5. [ ] GitHub Actions für Deployment
|
||||||
|
6. [ ] Content Collections in Payload erweitern
|
||||||
|
7. [ ] Design-System (Tailwind + Shadcn/UI)
|
||||||
|
|
||||||
|
### Langfristig (Q1 2025)
|
||||||
|
|
||||||
|
8. [ ] Alle 9 Frontends entwickeln
|
||||||
|
9. [ ] Migration von Plesk-Domains zu neuer Infra
|
||||||
|
10. [ ] CI/CD Pipeline komplett
|
||||||
|
11. [ ] Monitoring & Alerting
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Service-URLs
|
||||||
|
|
||||||
|
### Development (porwoll.tech)
|
||||||
|
|
||||||
|
| Service | URL | Status |
|
||||||
|
|---------|-----|--------|
|
||||||
|
| Portal | https://porwoll.tech | ✅ |
|
||||||
|
| Payload CMS | https://pl.porwoll.tech | ✅ |
|
||||||
|
| Redis Commander | https://redis.porwoll.tech | ✅ |
|
||||||
|
| Umami Analytics | https://umami.porwoll.tech | ✅ |
|
||||||
|
| Frontend porwoll.de | https://porwoll-dev.porwoll.tech | ✅ |
|
||||||
|
| (8 weitere) | https://*-dev.porwoll.tech | ⏸️ On-Demand |
|
||||||
|
|
||||||
|
### Production (Hetzner)
|
||||||
|
|
||||||
|
| Service | URL | Status |
|
||||||
|
|---------|-----|--------|
|
||||||
|
| Payload CMS | https://cms.c2sgmbh.de | ✅ |
|
||||||
|
| Umami Analytics | https://analytics.c2sgmbh.de | ✅ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Quick Commands
|
||||||
|
|
||||||
|
### sv-frontend
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# SSH Zugang
|
||||||
|
ssh frontend@10.10.181.104
|
||||||
|
|
||||||
|
# Service starten/stoppen
|
||||||
|
systemctl start frontend-porwoll
|
||||||
|
systemctl stop frontend-porwoll
|
||||||
|
|
||||||
|
# AI-Tools
|
||||||
|
claude # Claude Code
|
||||||
|
codex # Codex CLI
|
||||||
|
gemini # Gemini CLI
|
||||||
|
```
|
||||||
|
|
||||||
|
### Hetzner 3
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# SSH Zugang
|
||||||
|
ssh payload@162.55.85.18
|
||||||
|
|
||||||
|
# Deploy
|
||||||
|
~/deploy.sh
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
pm2 logs payload
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Änderungsprotokoll
|
||||||
|
|
||||||
|
### 18.12.2025
|
||||||
|
- sv-frontend (LXC 704) komplett eingerichtet
|
||||||
|
- 9 GitHub Repositories erstellt
|
||||||
|
- Alle Next.js Projekte initialisiert
|
||||||
|
- Systemd Services für alle Frontends
|
||||||
|
- Caddy Routing für *-dev.porwoll.tech
|
||||||
|
- AI-Tools (Claude Code, Codex, Gemini) installiert
|
||||||
|
- Infrastruktur-Dokumentation aktualisiert
|
||||||
|
|
||||||
|
### 12.12.2025
|
||||||
|
- sv-caddy mit Cloudflare DNS-Challenge
|
||||||
|
- Wildcard SSL für porwoll.tech
|
||||||
|
- Redis Commander auf sv-postgres
|
||||||
|
- pgAdmin4 entfernt (MIME-Type Probleme)
|
||||||
|
|
||||||
|
### 11.12.2025
|
||||||
|
- Hetzner 3 Production Setup
|
||||||
|
- Payload CMS Migration
|
||||||
|
- Umami Analytics Production
|
||||||
|
- Redis Caching (Dev + Prod)
|
||||||
|
- Package-Versionen synchronisiert
|
||||||
|
|
||||||
|
### 26.11.2025
|
||||||
|
- Initial Payload CMS Setup
|
||||||
|
- Multi-Tenant Plugin
|
||||||
|
- PostgreSQL auf sv-postgres
|
||||||
|
- Erste Tenants angelegt
|
||||||
981
docs/anleitungen/Analytics.md
Normal file
981
docs/anleitungen/Analytics.md
Normal file
|
|
@ -0,0 +1,981 @@
|
||||||
|
# ANALYTICS-LÖSUNG: Implementierungsübersicht für Payload CMS
|
||||||
|
|
||||||
|
*Letzte Aktualisierung: 18. Dezember 2025*
|
||||||
|
|
||||||
|
## Kontext
|
||||||
|
|
||||||
|
Du entwickelst das Multi-Tenant Payload CMS Backend und Next.js Frontend für 4 Websites. Diese Dokumentation beschreibt die Analytics-Lösung, die in das Frontend integriert werden muss.
|
||||||
|
|
||||||
|
**Wichtig:** Frontends verwenden die **Production-API** (cms.c2sgmbh.de) und **Production-Analytics** (analytics.c2sgmbh.de).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architektur-Übersicht
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ ANALYTICS ARCHITEKTUR │
|
||||||
|
│ │
|
||||||
|
│ ┌─────────────────────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ OHNE CONSENT (immer aktiv) │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ ┌─────────────────────────────────────────────────────────────────────┐ │ │
|
||||||
|
│ │ │ UMAMI ANALYTICS │ │ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ │ │ Server: sv-analytics (10.10.181.103:3000) │ │ │
|
||||||
|
│ │ │ Dashboard: http://10.10.181.103:3000 │ │ │
|
||||||
|
│ │ │ Script: /custom.js (Anti-Adblock) │ │ │
|
||||||
|
│ │ │ Endpoint: /api/send │ │ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ │ │ Features: │ │ │
|
||||||
|
│ │ │ • Cookieless Tracking (DSGVO-konform ohne Einwilligung) │ │ │
|
||||||
|
│ │ │ • Pageviews, Sessions, Referrer, UTM-Parameter │ │ │
|
||||||
|
│ │ │ • Custom Events (Newsletter, Formulare, CTAs, Downloads) │ │ │
|
||||||
|
│ │ │ • 100% Erfassung aller Besucher │ │ │
|
||||||
|
│ │ └─────────────────────────────────────────────────────────────────────┘ │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌─────────────────────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ MIT CONSENT (Kategorie: "marketing") │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ ┌─────────────────────────────────────────────────────────────────────┐ │ │
|
||||||
|
│ │ │ GOOGLE ADS CONVERSION │ │ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ │ │ Client-Side (bei Consent): │ │ │
|
||||||
|
│ │ │ • Google Ads Tag (gtag.js) │ │ │
|
||||||
|
│ │ │ • Conversion Tracking │ │ │
|
||||||
|
│ │ │ • Remarketing Audiences │ │ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ │ │ Server-Side (immer, anonymisiert): │ │ │
|
||||||
|
│ │ │ • Google Ads Conversion API │ │ │
|
||||||
|
│ │ │ • Enhanced Conversions (gehashte E-Mail) │ │ │
|
||||||
|
│ │ │ • GCLID-basierte Attribution │ │ │
|
||||||
|
│ │ └─────────────────────────────────────────────────────────────────────┘ │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ ┌─────────────────────────────────────────────────────────────────────┐ │ │
|
||||||
|
│ │ │ GOOGLE CONSENT MODE v2 │ │ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ │ │ Integration mit bestehendem Orestbida Consent-Banner │ │ │
|
||||||
|
│ │ │ Kategorie "marketing" steuert: │ │ │
|
||||||
|
│ │ │ • ad_storage │ │ │
|
||||||
|
│ │ │ • ad_user_data │ │ │
|
||||||
|
│ │ │ • ad_personalization │ │ │
|
||||||
|
│ │ └─────────────────────────────────────────────────────────────────────┘ │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Infrastruktur
|
||||||
|
|
||||||
|
### Umami Server
|
||||||
|
|
||||||
|
| Umgebung | Server | URL | Dashboard |
|
||||||
|
|----------|--------|-----|-----------|
|
||||||
|
| **Production** | Hetzner 3 | https://analytics.c2sgmbh.de | https://analytics.c2sgmbh.de |
|
||||||
|
| **Development** | sv-analytics (LXC 703) | https://umami.porwoll.tech | https://umami.porwoll.tech |
|
||||||
|
|
||||||
|
| Eigenschaft | Production | Development |
|
||||||
|
|-------------|------------|-------------|
|
||||||
|
| **Tracking Script** | https://analytics.c2sgmbh.de/script.js | https://umami.porwoll.tech/script.js |
|
||||||
|
| **API Endpoint** | https://analytics.c2sgmbh.de/api/send | https://umami.porwoll.tech/api/send |
|
||||||
|
| **Datenbank** | umami_db auf Hetzner 3 | umami_db auf sv-postgres |
|
||||||
|
|
||||||
|
### Website-IDs (Multi-Tenant)
|
||||||
|
|
||||||
|
Die Website-IDs werden in Umami für jeden Tenant erstellt:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/config/analytics.ts
|
||||||
|
|
||||||
|
export const UMAMI_WEBSITE_IDS: Record<string, string> = {
|
||||||
|
'porwoll.de': 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
|
||||||
|
'complexcaresolutions.de': 'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy',
|
||||||
|
'gunshin.de': 'zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz',
|
||||||
|
'zweitmeinu.ng': 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
|
||||||
|
}
|
||||||
|
|
||||||
|
// Production URL für Frontends
|
||||||
|
export const UMAMI_HOST = process.env.NEXT_PUBLIC_UMAMI_HOST || 'https://analytics.c2sgmbh.de'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Frontend-Integration
|
||||||
|
|
||||||
|
### 1. Umami Script Komponente
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/components/analytics/UmamiScript.tsx
|
||||||
|
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import Script from 'next/script'
|
||||||
|
|
||||||
|
interface UmamiScriptProps {
|
||||||
|
websiteId: string
|
||||||
|
host?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function UmamiScript({
|
||||||
|
websiteId,
|
||||||
|
host = 'https://analytics.c2sgmbh.de' // Production default
|
||||||
|
}: UmamiScriptProps) {
|
||||||
|
if (!websiteId) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Script
|
||||||
|
defer
|
||||||
|
src={`${host}/script.js`}
|
||||||
|
data-website-id={websiteId}
|
||||||
|
data-host-url={host}
|
||||||
|
strategy="afterInteractive"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Layout Integration (Multi-Tenant)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/app/layout.tsx
|
||||||
|
|
||||||
|
import { UmamiScript } from '@/components/analytics/UmamiScript'
|
||||||
|
import { UMAMI_WEBSITE_IDS, UMAMI_HOST } from '@/config/analytics'
|
||||||
|
import { getCurrentTenant } from '@/lib/tenant'
|
||||||
|
|
||||||
|
export default async function RootLayout({
|
||||||
|
children
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
const tenant = await getCurrentTenant()
|
||||||
|
const umamiWebsiteId = UMAMI_WEBSITE_IDS[tenant.domain]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<html lang="de">
|
||||||
|
<body>
|
||||||
|
{children}
|
||||||
|
|
||||||
|
{/* Umami Analytics - Läuft OHNE Consent (cookieless) */}
|
||||||
|
{umamiWebsiteId && (
|
||||||
|
<UmamiScript
|
||||||
|
websiteId={umamiWebsiteId}
|
||||||
|
host={UMAMI_HOST}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Analytics Hook für Custom Events
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/hooks/useAnalytics.ts
|
||||||
|
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useCallback } from 'react'
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
umami?: {
|
||||||
|
track: (eventName: string, eventData?: Record<string, unknown>) => void
|
||||||
|
}
|
||||||
|
gtag?: (...args: unknown[]) => void
|
||||||
|
CookieConsent?: {
|
||||||
|
acceptedCategory: (category: string) => boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAnalytics() {
|
||||||
|
/**
|
||||||
|
* Generisches Event-Tracking
|
||||||
|
*/
|
||||||
|
const trackEvent = useCallback((
|
||||||
|
eventName: string,
|
||||||
|
eventData?: Record<string, unknown>
|
||||||
|
) => {
|
||||||
|
if (typeof window !== 'undefined' && window.umami) {
|
||||||
|
window.umami.track(eventName, eventData)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Newsletter Anmeldung
|
||||||
|
*/
|
||||||
|
const trackNewsletterSubscribe = useCallback((source: string = 'unknown') => {
|
||||||
|
trackEvent('newsletter_subscribe', { source })
|
||||||
|
}, [trackEvent])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Newsletter Bestätigung (Double Opt-In)
|
||||||
|
*/
|
||||||
|
const trackNewsletterConfirm = useCallback(() => {
|
||||||
|
trackEvent('newsletter_confirm')
|
||||||
|
}, [trackEvent])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kontaktformular abgesendet
|
||||||
|
*/
|
||||||
|
const trackContactFormSubmit = useCallback((formType: string = 'contact') => {
|
||||||
|
trackEvent('contact_form_submit', { form_type: formType })
|
||||||
|
}, [trackEvent])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CTA Klick
|
||||||
|
*/
|
||||||
|
const trackCtaClick = useCallback((
|
||||||
|
ctaName: string,
|
||||||
|
ctaLocation: string = 'unknown'
|
||||||
|
) => {
|
||||||
|
trackEvent('cta_click', { cta_name: ctaName, location: ctaLocation })
|
||||||
|
}, [trackEvent])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download
|
||||||
|
*/
|
||||||
|
const trackDownload = useCallback((
|
||||||
|
fileName: string,
|
||||||
|
fileType: string = 'unknown'
|
||||||
|
) => {
|
||||||
|
trackEvent('download', { file_name: fileName, file_type: fileType })
|
||||||
|
}, [trackEvent])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Funnel-Step (für Conversion-Funnels)
|
||||||
|
*/
|
||||||
|
const trackFunnelStep = useCallback((
|
||||||
|
funnelName: string,
|
||||||
|
stepNumber: number,
|
||||||
|
stepName: string
|
||||||
|
) => {
|
||||||
|
trackEvent('funnel_step', {
|
||||||
|
funnel: funnelName,
|
||||||
|
step: stepNumber,
|
||||||
|
step_name: stepName
|
||||||
|
})
|
||||||
|
}, [trackEvent])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scroll-Tiefe
|
||||||
|
*/
|
||||||
|
const trackScrollDepth = useCallback((depth: number) => {
|
||||||
|
trackEvent('scroll_depth', { depth_percent: depth })
|
||||||
|
}, [trackEvent])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Externer Link Klick
|
||||||
|
*/
|
||||||
|
const trackExternalLink = useCallback((url: string) => {
|
||||||
|
trackEvent('external_link', { url })
|
||||||
|
}, [trackEvent])
|
||||||
|
|
||||||
|
return {
|
||||||
|
trackEvent,
|
||||||
|
trackNewsletterSubscribe,
|
||||||
|
trackNewsletterConfirm,
|
||||||
|
trackContactFormSubmit,
|
||||||
|
trackCtaClick,
|
||||||
|
trackDownload,
|
||||||
|
trackFunnelStep,
|
||||||
|
trackScrollDepth,
|
||||||
|
trackExternalLink,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Server-Side Event Tracking
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/lib/analytics.server.ts
|
||||||
|
|
||||||
|
const UMAMI_HOST = process.env.UMAMI_HOST || 'https://analytics.c2sgmbh.de'
|
||||||
|
const UMAMI_WEBSITE_ID = process.env.UMAMI_WEBSITE_ID
|
||||||
|
|
||||||
|
interface ServerEventParams {
|
||||||
|
event: string
|
||||||
|
url: string
|
||||||
|
websiteId?: string
|
||||||
|
referrer?: string
|
||||||
|
data?: Record<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Server-Side Event an Umami senden
|
||||||
|
* Für Events die im Backend passieren (z.B. Newsletter-Bestätigung)
|
||||||
|
*/
|
||||||
|
export async function trackServerEvent(params: ServerEventParams) {
|
||||||
|
const websiteId = params.websiteId || UMAMI_WEBSITE_ID
|
||||||
|
|
||||||
|
if (!websiteId) {
|
||||||
|
console.warn('[Analytics] No websiteId configured for server-side tracking')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${UMAMI_HOST}/api/send`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'User-Agent': 'Payload-CMS-Server/1.0',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
type: 'event',
|
||||||
|
payload: {
|
||||||
|
website: websiteId,
|
||||||
|
url: params.url,
|
||||||
|
referrer: params.referrer || '',
|
||||||
|
name: params.event,
|
||||||
|
data: params.data,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error('[Analytics] Server event failed:', response.status)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Analytics] Server event error:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Komponenten-Beispiele
|
||||||
|
|
||||||
|
### Newsletter-Formular mit Tracking
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/components/forms/NewsletterForm.tsx
|
||||||
|
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { useAnalytics } from '@/hooks/useAnalytics'
|
||||||
|
|
||||||
|
interface NewsletterFormProps {
|
||||||
|
source?: string // z.B. 'footer', 'hero', 'popup'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NewsletterForm({ source = 'unknown' }: NewsletterFormProps) {
|
||||||
|
const [email, setEmail] = useState('')
|
||||||
|
const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle')
|
||||||
|
const { trackNewsletterSubscribe, trackFunnelStep } = useAnalytics()
|
||||||
|
|
||||||
|
async function handleSubmit(e: React.FormEvent) {
|
||||||
|
e.preventDefault()
|
||||||
|
setStatus('loading')
|
||||||
|
|
||||||
|
// Funnel-Step tracken
|
||||||
|
trackFunnelStep('newsletter', 1, 'form_submitted')
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/newsletter/subscribe', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ email, source }),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
setStatus('success')
|
||||||
|
// Erfolg tracken
|
||||||
|
trackNewsletterSubscribe(source)
|
||||||
|
trackFunnelStep('newsletter', 2, 'subscription_pending')
|
||||||
|
} else {
|
||||||
|
setStatus('error')
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setStatus('error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 'success') {
|
||||||
|
return (
|
||||||
|
<div className="p-4 bg-green-50 text-green-800 rounded">
|
||||||
|
Bitte bestätige deine E-Mail-Adresse.
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit} className="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
placeholder="E-Mail-Adresse"
|
||||||
|
required
|
||||||
|
className="flex-1 px-4 py-2 border rounded"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={status === 'loading'}
|
||||||
|
className="px-6 py-2 bg-primary text-white rounded"
|
||||||
|
>
|
||||||
|
{status === 'loading' ? 'Lädt...' : 'Anmelden'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### CTA Button mit Tracking
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/components/ui/TrackedButton.tsx
|
||||||
|
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useAnalytics } from '@/hooks/useAnalytics'
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
interface TrackedButtonProps {
|
||||||
|
href: string
|
||||||
|
ctaName: string
|
||||||
|
location?: string
|
||||||
|
children: React.ReactNode
|
||||||
|
className?: string
|
||||||
|
external?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TrackedButton({
|
||||||
|
href,
|
||||||
|
ctaName,
|
||||||
|
location = 'unknown',
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
external = false,
|
||||||
|
}: TrackedButtonProps) {
|
||||||
|
const { trackCtaClick, trackExternalLink } = useAnalytics()
|
||||||
|
|
||||||
|
function handleClick() {
|
||||||
|
trackCtaClick(ctaName, location)
|
||||||
|
if (external) {
|
||||||
|
trackExternalLink(href)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (external) {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href={href}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
onClick={handleClick}
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link href={href} onClick={handleClick} className={className}>
|
||||||
|
{children}
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Download-Link mit Tracking
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/components/ui/TrackedDownload.tsx
|
||||||
|
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useAnalytics } from '@/hooks/useAnalytics'
|
||||||
|
|
||||||
|
interface TrackedDownloadProps {
|
||||||
|
href: string
|
||||||
|
fileName: string
|
||||||
|
fileType?: string
|
||||||
|
children: React.ReactNode
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TrackedDownload({
|
||||||
|
href,
|
||||||
|
fileName,
|
||||||
|
fileType = 'document',
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
}: TrackedDownloadProps) {
|
||||||
|
const { trackDownload } = useAnalytics()
|
||||||
|
|
||||||
|
function handleClick() {
|
||||||
|
trackDownload(fileName, fileType)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href={href}
|
||||||
|
download
|
||||||
|
onClick={handleClick}
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Google Ads Integration
|
||||||
|
|
||||||
|
### Consent Mode v2 Komponente
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/components/analytics/GoogleConsentMode.tsx
|
||||||
|
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import Script from 'next/script'
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
|
interface GoogleConsentModeProps {
|
||||||
|
googleAdsId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GoogleConsentMode({ googleAdsId }: GoogleConsentModeProps) {
|
||||||
|
useEffect(() => {
|
||||||
|
// Consent-Änderungen von Orestbida abonnieren
|
||||||
|
window.addEventListener('cc:onConsent', handleConsentChange)
|
||||||
|
window.addEventListener('cc:onChange', handleConsentChange)
|
||||||
|
|
||||||
|
// Initial setzen
|
||||||
|
updateGoogleConsent()
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('cc:onConsent', handleConsentChange)
|
||||||
|
window.removeEventListener('cc:onChange', handleConsentChange)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
function handleConsentChange() {
|
||||||
|
updateGoogleConsent()
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateGoogleConsent() {
|
||||||
|
if (typeof window.gtag !== 'function') return
|
||||||
|
|
||||||
|
const cc = window.CookieConsent
|
||||||
|
if (!cc) return
|
||||||
|
|
||||||
|
const hasMarketing = cc.acceptedCategory('marketing')
|
||||||
|
|
||||||
|
window.gtag('consent', 'update', {
|
||||||
|
'ad_storage': hasMarketing ? 'granted' : 'denied',
|
||||||
|
'ad_user_data': hasMarketing ? 'granted' : 'denied',
|
||||||
|
'ad_personalization': hasMarketing ? 'granted' : 'denied',
|
||||||
|
'analytics_storage': 'denied', // Wir nutzen Umami
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Consent Default (vor gtag.js) */}
|
||||||
|
<Script
|
||||||
|
id="google-consent-default"
|
||||||
|
strategy="beforeInteractive"
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: `
|
||||||
|
window.dataLayer = window.dataLayer || [];
|
||||||
|
function gtag(){dataLayer.push(arguments);}
|
||||||
|
|
||||||
|
gtag('consent', 'default', {
|
||||||
|
'ad_storage': 'denied',
|
||||||
|
'ad_user_data': 'denied',
|
||||||
|
'ad_personalization': 'denied',
|
||||||
|
'analytics_storage': 'denied',
|
||||||
|
'wait_for_update': 500
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Google Ads Tag */}
|
||||||
|
<Script
|
||||||
|
src={`https://www.googletagmanager.com/gtag/js?id=${googleAdsId}`}
|
||||||
|
strategy="afterInteractive"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Script
|
||||||
|
id="google-ads-config"
|
||||||
|
strategy="afterInteractive"
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: `
|
||||||
|
gtag('js', new Date());
|
||||||
|
gtag('config', '${googleAdsId}', {
|
||||||
|
'allow_enhanced_conversions': true
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### GCLID Hook (für Conversion Attribution)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/hooks/useGclid.ts
|
||||||
|
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
import { useSearchParams } from 'next/navigation'
|
||||||
|
|
||||||
|
const GCLID_STORAGE_KEY = 'gclid'
|
||||||
|
const GCLID_EXPIRY_DAYS = 90
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GCLID aus URL erfassen und speichern
|
||||||
|
*/
|
||||||
|
export function useGclid() {
|
||||||
|
const searchParams = useSearchParams()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const gclid = searchParams.get('gclid')
|
||||||
|
|
||||||
|
if (gclid) {
|
||||||
|
const data = {
|
||||||
|
value: gclid,
|
||||||
|
expires: Date.now() + (GCLID_EXPIRY_DAYS * 24 * 60 * 60 * 1000)
|
||||||
|
}
|
||||||
|
localStorage.setItem(GCLID_STORAGE_KEY, JSON.stringify(data))
|
||||||
|
}
|
||||||
|
}, [searchParams])
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gespeicherte GCLID abrufen
|
||||||
|
*/
|
||||||
|
export function getStoredGclid(): string | null {
|
||||||
|
if (typeof window === 'undefined') return null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem(GCLID_STORAGE_KEY)
|
||||||
|
if (!stored) return null
|
||||||
|
|
||||||
|
const data = JSON.parse(stored)
|
||||||
|
|
||||||
|
if (Date.now() > data.expires) {
|
||||||
|
localStorage.removeItem(GCLID_STORAGE_KEY)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.value
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Client-Side Conversion Tracking
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/lib/google-ads.ts
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
gtag: (...args: unknown[]) => void
|
||||||
|
CookieConsent: {
|
||||||
|
acceptedCategory: (category: string) => boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ConversionParams {
|
||||||
|
conversionId: string
|
||||||
|
value?: number
|
||||||
|
currency?: string
|
||||||
|
transactionId?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Client-Side Conversion (nur bei Marketing-Consent)
|
||||||
|
*/
|
||||||
|
export function trackConversion(params: ConversionParams) {
|
||||||
|
if (typeof window === 'undefined') return
|
||||||
|
if (typeof window.gtag !== 'function') return
|
||||||
|
|
||||||
|
const hasConsent = window.CookieConsent?.acceptedCategory('marketing')
|
||||||
|
if (!hasConsent) return
|
||||||
|
|
||||||
|
window.gtag('event', 'conversion', {
|
||||||
|
'send_to': params.conversionId,
|
||||||
|
'value': params.value || 1.0,
|
||||||
|
'currency': params.currency || 'EUR',
|
||||||
|
'transaction_id': params.transactionId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enhanced Conversion Data setzen
|
||||||
|
*/
|
||||||
|
export function setEnhancedConversionData(data: {
|
||||||
|
email?: string
|
||||||
|
phone?: string
|
||||||
|
firstName?: string
|
||||||
|
lastName?: string
|
||||||
|
}) {
|
||||||
|
if (typeof window === 'undefined') return
|
||||||
|
if (typeof window.gtag !== 'function') return
|
||||||
|
|
||||||
|
const hasConsent = window.CookieConsent?.acceptedCategory('marketing')
|
||||||
|
if (!hasConsent) return
|
||||||
|
|
||||||
|
window.gtag('set', 'user_data', {
|
||||||
|
'email': data.email,
|
||||||
|
'phone_number': data.phone,
|
||||||
|
'address': {
|
||||||
|
'first_name': data.firstName,
|
||||||
|
'last_name': data.lastName,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Server-Side Conversion API
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/lib/google-ads.server.ts
|
||||||
|
|
||||||
|
import crypto from 'crypto'
|
||||||
|
|
||||||
|
const GOOGLE_ADS_CUSTOMER_ID = process.env.GOOGLE_ADS_CUSTOMER_ID
|
||||||
|
const GOOGLE_ADS_CONVERSION_ACTION_ID = process.env.GOOGLE_ADS_CONVERSION_ACTION_ID
|
||||||
|
const GOOGLE_ADS_API_TOKEN = process.env.GOOGLE_ADS_API_TOKEN
|
||||||
|
const GOOGLE_ADS_DEVELOPER_TOKEN = process.env.GOOGLE_ADS_DEVELOPER_TOKEN
|
||||||
|
|
||||||
|
interface ServerConversionParams {
|
||||||
|
conversionAction: string
|
||||||
|
email?: string
|
||||||
|
phone?: string
|
||||||
|
value?: number
|
||||||
|
currency?: string
|
||||||
|
gclid?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Server-Side Conversion Upload
|
||||||
|
* DSGVO-konform: Nur gehashte Daten
|
||||||
|
*/
|
||||||
|
export async function trackServerConversion(params: ServerConversionParams) {
|
||||||
|
if (!GOOGLE_ADS_CUSTOMER_ID || !GOOGLE_ADS_API_TOKEN) {
|
||||||
|
console.log('[Google Ads] Server-Side nicht konfiguriert')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const conversion: Record<string, unknown> = {
|
||||||
|
conversionAction: `customers/${GOOGLE_ADS_CUSTOMER_ID}/conversionActions/${GOOGLE_ADS_CONVERSION_ACTION_ID}`,
|
||||||
|
conversionDateTime: new Date().toISOString().replace('Z', '+00:00'),
|
||||||
|
conversionValue: params.value || 1.0,
|
||||||
|
currencyCode: params.currency || 'EUR',
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.gclid) {
|
||||||
|
conversion.gclid = params.gclid
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.email || params.phone) {
|
||||||
|
conversion.userIdentifiers = []
|
||||||
|
|
||||||
|
if (params.email) {
|
||||||
|
(conversion.userIdentifiers as Array<unknown>).push({
|
||||||
|
hashedEmail: hashForGoogle(params.email.toLowerCase().trim())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.phone) {
|
||||||
|
(conversion.userIdentifiers as Array<unknown>).push({
|
||||||
|
hashedPhoneNumber: hashForGoogle(params.phone.replace(/[^0-9+]/g, ''))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`https://googleads.googleapis.com/v15/customers/${GOOGLE_ADS_CUSTOMER_ID}:uploadConversionAdjustments`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${GOOGLE_ADS_API_TOKEN}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'developer-token': GOOGLE_ADS_DEVELOPER_TOKEN!,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
conversions: [conversion],
|
||||||
|
partialFailure: true,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error('[Google Ads] Upload failed:', await response.text())
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Google Ads] Error:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hashForGoogle(value: string): string {
|
||||||
|
return crypto.createHash('sha256').update(value).digest('hex')
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CookieInventory Erweiterung
|
||||||
|
|
||||||
|
Füge diese Cookies zur `cookie-inventory` Collection hinzu:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Über Payload Admin oder Seed-Script
|
||||||
|
|
||||||
|
const googleAdsCookies = [
|
||||||
|
{
|
||||||
|
name: '_gcl_au',
|
||||||
|
provider: 'Google Ads',
|
||||||
|
category: 'marketing',
|
||||||
|
purpose: 'Conversion-Tracking und Anzeigenmessung',
|
||||||
|
duration: '90 Tage',
|
||||||
|
type: 'first-party',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '_gcl_aw',
|
||||||
|
provider: 'Google Ads',
|
||||||
|
category: 'marketing',
|
||||||
|
purpose: 'Speichert GCLID für Conversion-Attribution',
|
||||||
|
duration: '90 Tage',
|
||||||
|
type: 'first-party',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'IDE',
|
||||||
|
provider: 'Google (doubleclick.net)',
|
||||||
|
category: 'marketing',
|
||||||
|
purpose: 'Remarketing und Anzeigenpersonalisierung',
|
||||||
|
duration: '1 Jahr',
|
||||||
|
type: 'third-party',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
### Frontend (.env.local)
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Payload CMS API (Production)
|
||||||
|
NEXT_PUBLIC_PAYLOAD_URL=https://cms.c2sgmbh.de
|
||||||
|
NEXT_PUBLIC_API_URL=https://cms.c2sgmbh.de/api
|
||||||
|
|
||||||
|
# Umami Analytics (Production)
|
||||||
|
NEXT_PUBLIC_UMAMI_HOST=https://analytics.c2sgmbh.de
|
||||||
|
|
||||||
|
# Tenant-Konfiguration (je nach Projekt)
|
||||||
|
NEXT_PUBLIC_TENANT_ID=4
|
||||||
|
NEXT_PUBLIC_TENANT_SLUG=c2s
|
||||||
|
|
||||||
|
# Google Ads (pro Tenant unterschiedlich)
|
||||||
|
NEXT_PUBLIC_GOOGLE_ADS_ID=AW-XXXXXXXXX
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backend (.env)
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Umami Server-Side Tracking
|
||||||
|
UMAMI_HOST=https://analytics.c2sgmbh.de
|
||||||
|
UMAMI_WEBSITE_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
||||||
|
|
||||||
|
# Google Ads Server-Side API
|
||||||
|
GOOGLE_ADS_CUSTOMER_ID=1234567890
|
||||||
|
GOOGLE_ADS_CONVERSION_ACTION_ID=987654321
|
||||||
|
GOOGLE_ADS_API_TOKEN=ya29.xxx
|
||||||
|
GOOGLE_ADS_DEVELOPER_TOKEN=xxx
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Event-Naming-Konvention
|
||||||
|
|
||||||
|
| Event | Name | Data |
|
||||||
|
|-------|------|------|
|
||||||
|
| Newsletter Anmeldung | `newsletter_subscribe` | `{ source: string }` |
|
||||||
|
| Newsletter Bestätigung | `newsletter_confirm` | - |
|
||||||
|
| Kontaktformular | `contact_form_submit` | `{ form_type: string }` |
|
||||||
|
| CTA Klick | `cta_click` | `{ cta_name: string, location: string }` |
|
||||||
|
| Download | `download` | `{ file_name: string, file_type: string }` |
|
||||||
|
| Funnel-Step | `funnel_step` | `{ funnel: string, step: number, step_name: string }` |
|
||||||
|
| Scroll-Tiefe | `scroll_depth` | `{ depth_percent: number }` |
|
||||||
|
| Externer Link | `external_link` | `{ url: string }` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Zusammenfassung
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ WAS IMPLEMENTIERT WERDEN MUSS │
|
||||||
|
│ │
|
||||||
|
│ 1. UMAMI (ohne Consent) │
|
||||||
|
│ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │
|
||||||
|
│ • UmamiScript Komponente in Layout einbinden │
|
||||||
|
│ • useAnalytics Hook in Formulare/CTAs integrieren │
|
||||||
|
│ • Server-Side Events für Backend-Actions │
|
||||||
|
│ │
|
||||||
|
│ 2. GOOGLE ADS (mit Consent) │
|
||||||
|
│ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │
|
||||||
|
│ • GoogleConsentMode Komponente (integriert mit Orestbida) │
|
||||||
|
│ • useGclid Hook für Attribution │
|
||||||
|
│ • Client-Side Conversions bei Consent │
|
||||||
|
│ • Server-Side Conversions immer (gehashte Daten) │
|
||||||
|
│ │
|
||||||
|
│ 3. COOKIE INVENTORY │
|
||||||
|
│ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │
|
||||||
|
│ • Google Ads Cookies dokumentieren │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dateien zu erstellen
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── components/
|
||||||
|
│ ├── analytics/
|
||||||
|
│ │ ├── UmamiScript.tsx # Umami Tracking Script
|
||||||
|
│ │ └── GoogleConsentMode.tsx # Google Consent Mode v2
|
||||||
|
│ ├── forms/
|
||||||
|
│ │ └── NewsletterForm.tsx # Mit Analytics-Integration
|
||||||
|
│ └── ui/
|
||||||
|
│ ├── TrackedButton.tsx # CTA mit Tracking
|
||||||
|
│ └── TrackedDownload.tsx # Download mit Tracking
|
||||||
|
├── config/
|
||||||
|
│ └── analytics.ts # Website-IDs, Config
|
||||||
|
├── hooks/
|
||||||
|
│ ├── useAnalytics.ts # Client-Side Event Tracking
|
||||||
|
│ └── useGclid.ts # GCLID Erfassung
|
||||||
|
└── lib/
|
||||||
|
├── analytics.server.ts # Umami Server-Side
|
||||||
|
├── google-ads.ts # Google Ads Client
|
||||||
|
└── google-ads.server.ts # Google Ads Server API
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Letzte Aktualisierung: 18. Dezember 2025*
|
||||||
|
|
@ -1,21 +1,53 @@
|
||||||
# Frontend-Entwicklung - Payload CMS Multi-Tenant
|
# Frontend-Entwicklung - Payload CMS Multi-Tenant
|
||||||
|
|
||||||
> **Server:** sv-frontend (LXC 704) - 10.10.181.104
|
> **Server:** sv-frontend (LXC 704) - 10.10.181.104
|
||||||
> **Backend API:** https://pl.c2sgmbh.de/api
|
> **Backend API:** https://cms.c2sgmbh.de/api (Production)
|
||||||
|
|
||||||
## Übersicht
|
## Übersicht
|
||||||
|
|
||||||
Das Frontend wird als separates Next.js-Projekt entwickelt und nutzt Payload CMS als Headless CMS über die REST-API.
|
Das Frontend wird als separates Next.js-Projekt entwickelt und nutzt Payload CMS als Headless CMS über die REST-API.
|
||||||
|
|
||||||
|
**Wichtig:** Die Frontend-Entwicklung verwendet die **Produktions-API und -Datenbank**, um mit echten Inhalten zu arbeiten. SEO-Einstellungen und Cookie-Consent-Konfigurationen werden ebenfalls aus der Produktionsumgebung geladen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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
|
||||||
|
```
|
||||||
|
|
||||||
|
### Warum Production-Daten?
|
||||||
|
|
||||||
|
| Aspekt | Grund |
|
||||||
|
|--------|-------|
|
||||||
|
| **Content** | Echte Inhalte für realistische Entwicklung |
|
||||||
|
| **SEO** | Produktions-Meta-Tags und Structured Data |
|
||||||
|
| **Cookie-Consent** | Live Cookie-Konfigurationen (DSGVO-relevant) |
|
||||||
|
| **Media** | Produktions-Bilder mit allen Größen |
|
||||||
|
| **Consistency** | Keine Sync-Probleme zwischen Dev/Prod |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## API-Dokumentation
|
## API-Dokumentation
|
||||||
|
|
||||||
| Ressource | URL |
|
| Ressource | URL |
|
||||||
|-----------|-----|
|
|-----------|-----|
|
||||||
| **Swagger UI** | https://pl.c2sgmbh.de/api/docs |
|
| **Swagger UI** | https://cms.c2sgmbh.de/api/docs |
|
||||||
| **OpenAPI JSON** | https://pl.c2sgmbh.de/api/openapi.json |
|
| **OpenAPI JSON** | https://cms.c2sgmbh.de/api/openapi.json |
|
||||||
| **REST API Base** | https://pl.c2sgmbh.de/api |
|
| **REST API Base** | https://cms.c2sgmbh.de/api |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -25,6 +57,7 @@ Das Frontend wird als separates Next.js-Projekt entwickelt und nutzt Payload CMS
|
||||||
|
|
||||||
- [ ] **Block-Komponenten entwickeln**
|
- [ ] **Block-Komponenten entwickeln**
|
||||||
- [ ] Hero Block
|
- [ ] Hero Block
|
||||||
|
- [ ] Hero Slider Block
|
||||||
- [ ] Text Block
|
- [ ] Text Block
|
||||||
- [ ] Image Text Block
|
- [ ] Image Text Block
|
||||||
- [ ] Card Grid Block
|
- [ ] Card Grid Block
|
||||||
|
|
@ -38,6 +71,9 @@ Das Frontend wird als separates Next.js-Projekt entwickelt und nutzt Payload CMS
|
||||||
- [ ] Testimonials Block
|
- [ ] Testimonials Block
|
||||||
- [ ] Newsletter Block
|
- [ ] Newsletter Block
|
||||||
- [ ] Process Steps Block
|
- [ ] Process Steps Block
|
||||||
|
- [ ] FAQ Block
|
||||||
|
- [ ] Team Block
|
||||||
|
- [ ] Services Block
|
||||||
|
|
||||||
- [ ] **Newsletter-Anmelde-Formular**
|
- [ ] **Newsletter-Anmelde-Formular**
|
||||||
- API: `POST /api/newsletter/subscribe`
|
- API: `POST /api/newsletter/subscribe`
|
||||||
|
|
@ -45,7 +81,7 @@ Das Frontend wird als separates Next.js-Projekt entwickelt und nutzt Payload CMS
|
||||||
- Felder: email, firstName (optional), tenantId, source
|
- Felder: email, firstName (optional), tenantId, source
|
||||||
|
|
||||||
- [ ] **Cookie-Banner implementieren**
|
- [ ] **Cookie-Banner implementieren**
|
||||||
- Cookie Configurations aus API laden
|
- Cookie Configurations aus Production-API laden
|
||||||
- Consent-Logs an Backend senden
|
- Consent-Logs an Backend senden
|
||||||
- DSGVO-konform mit Opt-In
|
- DSGVO-konform mit Opt-In
|
||||||
|
|
||||||
|
|
@ -57,9 +93,9 @@ Das Frontend wird als separates Next.js-Projekt entwickelt und nutzt Payload CMS
|
||||||
- Unterstützte Locales: `de` (default), `en`
|
- Unterstützte Locales: `de` (default), `en`
|
||||||
|
|
||||||
- [ ] **SEO-Integration**
|
- [ ] **SEO-Integration**
|
||||||
- Meta-Tags aus Pages/Posts
|
- Meta-Tags aus Pages/Posts (Production)
|
||||||
- Structured Data (JSON-LD)
|
- Structured Data (JSON-LD)
|
||||||
- Sitemap: https://pl.c2sgmbh.de/sitemap.xml
|
- Sitemap: https://cms.c2sgmbh.de/sitemap.xml
|
||||||
|
|
||||||
- [ ] **Suche implementieren**
|
- [ ] **Suche implementieren**
|
||||||
- API: `GET /api/search?q=...&locale=de`
|
- API: `GET /api/search?q=...&locale=de`
|
||||||
|
|
@ -69,26 +105,26 @@ Das Frontend wird als separates Next.js-Projekt entwickelt und nutzt Payload CMS
|
||||||
### Tenant-spezifische Features
|
### Tenant-spezifische Features
|
||||||
|
|
||||||
#### porwoll.de
|
#### porwoll.de
|
||||||
- [ ] Immobilien-Listing
|
- [ ] Portfolio-Galerie (Fotografie)
|
||||||
- [ ] Objektsuche mit Filtern
|
- [ ] Buchungsformular
|
||||||
- [ ] Kontaktformular mit Objekt-Referenz
|
- [ ] Before/After Bildvergleich
|
||||||
|
|
||||||
#### complexcaresolutions.de (C2S)
|
#### complexcaresolutions.de (C2S)
|
||||||
- [ ] Team-Übersicht
|
- [ ] Team-Übersicht
|
||||||
- [ ] Leistungs-Seiten
|
- [ ] Leistungs-Seiten
|
||||||
|
- [ ] Zertifizierungen
|
||||||
- [ ] Karriere-Seite mit Stellenangeboten
|
- [ ] Karriere-Seite mit Stellenangeboten
|
||||||
|
|
||||||
#### gunshin.de (Fotografin-Portfolio)
|
#### gunshin.de (Game Development)
|
||||||
- [ ] Portfolio-Galerie
|
- [ ] Projekt-Galerie
|
||||||
- API: `GET /api/portfolios?where[tenant][equals]=5`
|
- API: `GET /api/projects?where[tenant][equals]=5`
|
||||||
- Kategorien: `GET /api/portfolio-categories`
|
- [ ] Portfolio-Seiten
|
||||||
- [ ] Projekt-Detailseiten mit Lightbox
|
|
||||||
- [ ] Referenzen-Slider
|
- [ ] Referenzen-Slider
|
||||||
|
|
||||||
#### zweitmein.ng
|
#### zweitmein.ng
|
||||||
- [ ] Produkt-Übersicht (falls E-Commerce)
|
|
||||||
- [ ] FAQ-Sektion
|
- [ ] FAQ-Sektion
|
||||||
- [ ] Preistabellen
|
- [ ] Preistabellen
|
||||||
|
- [ ] Kontaktformular
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -107,14 +143,18 @@ Das Frontend wird als separates Next.js-Projekt entwickelt und nutzt Payload CMS
|
||||||
| FAQs | `GET /api/faqs` | FAQ-Einträge |
|
| FAQs | `GET /api/faqs` | FAQ-Einträge |
|
||||||
| Portfolios | `GET /api/portfolios` | Portfolio-Projekte |
|
| Portfolios | `GET /api/portfolios` | Portfolio-Projekte |
|
||||||
| Media | `GET /api/media` | Medien/Bilder |
|
| Media | `GET /api/media` | Medien/Bilder |
|
||||||
|
| Videos | `GET /api/videos` | Video-Bibliothek |
|
||||||
|
| Timelines | `GET /api/timelines` | Chronologische Events |
|
||||||
|
| Workflows | `GET /api/workflows` | Prozess-Darstellungen |
|
||||||
|
|
||||||
### Globals
|
### Globals (SEO & Settings aus Production)
|
||||||
|
|
||||||
| Global | Endpoint | Beschreibung |
|
| Global | Endpoint | Beschreibung |
|
||||||
|--------|----------|--------------|
|
|--------|----------|--------------|
|
||||||
| Site Settings | `GET /api/globals/site-settings` | Logo, Name, SEO |
|
| Site Settings | `GET /api/globals/site-settings` | Logo, Name, Kontakt |
|
||||||
| Navigation | `GET /api/globals/navigation` | Menü-Struktur |
|
| Navigation | `GET /api/globals/navigation` | Menü-Struktur |
|
||||||
| SEO Settings | `GET /api/globals/seo-settings` | Default SEO |
|
| SEO Settings | `GET /api/globals/seo-settings` | Default SEO (Production) |
|
||||||
|
| Privacy Policy | `GET /api/globals/privacy-policy-settings` | Datenschutz |
|
||||||
|
|
||||||
### Spezielle Endpoints
|
### Spezielle Endpoints
|
||||||
|
|
||||||
|
|
@ -123,6 +163,8 @@ Das Frontend wird als separates Next.js-Projekt entwickelt und nutzt Payload CMS
|
||||||
| `/api/search` | GET | Volltextsuche |
|
| `/api/search` | GET | Volltextsuche |
|
||||||
| `/api/search/suggestions` | GET | Auto-Complete |
|
| `/api/search/suggestions` | GET | Auto-Complete |
|
||||||
| `/api/newsletter/subscribe` | POST | Newsletter-Anmeldung |
|
| `/api/newsletter/subscribe` | POST | Newsletter-Anmeldung |
|
||||||
|
| `/api/timelines` | GET | Timeline-Daten |
|
||||||
|
| `/api/workflows` | GET | Workflow-Daten |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -132,10 +174,13 @@ Alle Collection-Anfragen sollten nach Tenant gefiltert werden:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Beispiel: Posts für Tenant "c2s" (ID: 4)
|
// Beispiel: Posts für Tenant "c2s" (ID: 4)
|
||||||
fetch('https://pl.c2sgmbh.de/api/posts?where[tenant][equals]=4&locale=de')
|
fetch('https://cms.c2sgmbh.de/api/posts?where[tenant][equals]=4&locale=de')
|
||||||
|
|
||||||
// Beispiel: Pages für Tenant "gunshin" (ID: 5)
|
// Beispiel: Pages für Tenant "gunshin" (ID: 5)
|
||||||
fetch('https://pl.c2sgmbh.de/api/pages?where[tenant][equals]=5&locale=de')
|
fetch('https://cms.c2sgmbh.de/api/pages?where[tenant][equals]=5&locale=de')
|
||||||
|
|
||||||
|
// Beispiel: SEO-Settings (Global, kein Tenant-Filter)
|
||||||
|
fetch('https://cms.c2sgmbh.de/api/globals/seo-settings')
|
||||||
```
|
```
|
||||||
|
|
||||||
### Tenant-IDs
|
### Tenant-IDs
|
||||||
|
|
@ -181,10 +226,10 @@ Unterstützte Locales: `de` (default), `en`
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Deutsch (default)
|
// Deutsch (default)
|
||||||
fetch('https://pl.c2sgmbh.de/api/posts?locale=de')
|
fetch('https://cms.c2sgmbh.de/api/posts?locale=de')
|
||||||
|
|
||||||
// Englisch
|
// Englisch
|
||||||
fetch('https://pl.c2sgmbh.de/api/posts?locale=en')
|
fetch('https://cms.c2sgmbh.de/api/posts?locale=en')
|
||||||
|
|
||||||
// Fallback: Wenn EN nicht vorhanden, wird DE zurückgegeben
|
// Fallback: Wenn EN nicht vorhanden, wird DE zurückgegeben
|
||||||
```
|
```
|
||||||
|
|
@ -196,7 +241,7 @@ fetch('https://pl.c2sgmbh.de/api/posts?locale=en')
|
||||||
### Anmeldung
|
### Anmeldung
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const response = await fetch('https://pl.c2sgmbh.de/api/newsletter/subscribe', {
|
const response = await fetch('https://cms.c2sgmbh.de/api/newsletter/subscribe', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
|
@ -219,18 +264,25 @@ const response = await fetch('https://pl.c2sgmbh.de/api/newsletter/subscribe', {
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Cookie-Consent
|
## Cookie-Consent (Production-Daten)
|
||||||
|
|
||||||
### Konfiguration laden
|
### Konfiguration laden
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const config = await fetch('https://pl.c2sgmbh.de/api/cookie-configurations?where[tenant][equals]=4')
|
// Cookie-Konfiguration aus Production laden
|
||||||
|
const config = await fetch('https://cms.c2sgmbh.de/api/cookie-configurations?where[tenant][equals]=4')
|
||||||
|
.then(r => r.json())
|
||||||
|
|
||||||
|
// config.docs enthält:
|
||||||
|
// - Kategorien (necessary, analytics, marketing, etc.)
|
||||||
|
// - Cookie-Details pro Kategorie
|
||||||
|
// - Texte für Banner (lokalisiert)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Consent loggen
|
### Consent loggen
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
await fetch('https://pl.c2sgmbh.de/api/consent-logs', {
|
await fetch('https://cms.c2sgmbh.de/api/consent-logs', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
|
@ -245,12 +297,49 @@ await fetch('https://pl.c2sgmbh.de/api/consent-logs', {
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## SEO-Integration (Production-Daten)
|
||||||
|
|
||||||
|
### Global SEO-Settings
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// SEO-Defaults aus Production laden
|
||||||
|
const seoSettings = await fetch('https://cms.c2sgmbh.de/api/globals/seo-settings')
|
||||||
|
.then(r => r.json())
|
||||||
|
|
||||||
|
// Enthält:
|
||||||
|
// - defaultTitle, titleTemplate
|
||||||
|
// - defaultDescription
|
||||||
|
// - defaultImage (OG-Image)
|
||||||
|
// - robotsDefault
|
||||||
|
```
|
||||||
|
|
||||||
|
### Page-spezifische SEO
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// SEO-Daten aus Page laden
|
||||||
|
const page = await fetch('https://cms.c2sgmbh.de/api/pages?where[slug][equals]=about&where[tenant][equals]=4')
|
||||||
|
.then(r => r.json())
|
||||||
|
|
||||||
|
// page.docs[0].meta enthält:
|
||||||
|
// - title, description
|
||||||
|
// - image (OG-Image Override)
|
||||||
|
// - noIndex, noFollow
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sitemap
|
||||||
|
|
||||||
|
Die Sitemap wird automatisch von Payload generiert:
|
||||||
|
- **URL:** https://cms.c2sgmbh.de/sitemap.xml
|
||||||
|
- Enthält alle publizierten Pages und Posts
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Kontaktformular
|
## Kontaktformular
|
||||||
|
|
||||||
Formular-Submissions werden über die Forms-Collection verarbeitet:
|
Formular-Submissions werden über die Forms-Collection verarbeitet:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
await fetch('https://pl.c2sgmbh.de/api/form-submissions', {
|
await fetch('https://cms.c2sgmbh.de/api/form-submissions', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
|
@ -272,14 +361,14 @@ Falls User-Authentifizierung benötigt wird:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Login
|
// Login
|
||||||
const { token, user } = await fetch('https://pl.c2sgmbh.de/api/users/login', {
|
const { token, user } = await fetch('https://cms.c2sgmbh.de/api/users/login', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ email, password })
|
body: JSON.stringify({ email, password })
|
||||||
}).then(r => r.json())
|
}).then(r => r.json())
|
||||||
|
|
||||||
// Authentifizierte Requests
|
// Authentifizierte Requests
|
||||||
fetch('https://pl.c2sgmbh.de/api/...', {
|
fetch('https://cms.c2sgmbh.de/api/...', {
|
||||||
headers: { 'Authorization': `JWT ${token}` }
|
headers: { 'Authorization': `JWT ${token}` }
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
@ -293,7 +382,9 @@ fetch('https://pl.c2sgmbh.de/api/...', {
|
||||||
Die Payload-Typen können aus dem Backend exportiert werden:
|
Die Payload-Typen können aus dem Backend exportiert werden:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Auf dem Payload-Server
|
# Auf dem Payload-Server (Production)
|
||||||
|
ssh payload@162.55.85.18
|
||||||
|
cd ~/payload-cms
|
||||||
pnpm payload generate:types
|
pnpm payload generate:types
|
||||||
|
|
||||||
# Datei: src/payload-types.ts
|
# Datei: src/payload-types.ts
|
||||||
|
|
@ -316,14 +407,60 @@ Diese Datei kann ins Frontend-Projekt kopiert werden für typsichere API-Calls.
|
||||||
- TTL: 60 Sekunden für Suche
|
- TTL: 60 Sekunden für Suche
|
||||||
- Cache wird bei Content-Änderungen invalidiert
|
- Cache wird bei Content-Änderungen invalidiert
|
||||||
|
|
||||||
|
### CORS
|
||||||
|
|
||||||
|
Die Production-API erlaubt Requests von:
|
||||||
|
- `*.porwoll.tech` (Development)
|
||||||
|
- `porwoll.de`, `complexcaresolutions.de`, `gunshin.de` (Production)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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
|
||||||
|
codex # Codex CLI
|
||||||
|
gemini # Gemini CLI
|
||||||
|
```
|
||||||
|
|
||||||
|
### Service-Management
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Systemd Service starten
|
||||||
|
systemctl start frontend-porwoll
|
||||||
|
|
||||||
|
# Service stoppen
|
||||||
|
systemctl stop frontend-porwoll
|
||||||
|
|
||||||
|
# Logs anzeigen
|
||||||
|
journalctl -u frontend-porwoll -f
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Ressourcen
|
## Ressourcen
|
||||||
|
|
||||||
- **Payload CMS Docs:** https://payloadcms.com/docs
|
- **Payload CMS Docs:** https://payloadcms.com/docs
|
||||||
- **API-Dokumentation:** https://pl.c2sgmbh.de/api/docs
|
- **API-Dokumentation:** https://cms.c2sgmbh.de/api/docs
|
||||||
- **Backend-Repository:** https://github.com/c2s-admin/cms.c2sgmbh.git
|
- **Backend-Repository:** https://github.com/complexcaresolutions/cms.c2sgmbh.git
|
||||||
|
- **Analytics:** https://analytics.c2sgmbh.de
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Erstellt: 11.12.2025*
|
*Letzte Aktualisierung: 18.12.2025*
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,18 @@
|
||||||
# Security-Richtlinien - Payload CMS Multi-Tenant
|
# Security-Richtlinien - Payload CMS Multi-Tenant
|
||||||
|
|
||||||
> Letzte Aktualisierung: 17.12.2025
|
> Letzte Aktualisierung: 18.12.2025
|
||||||
|
|
||||||
## Übersicht
|
## Übersicht
|
||||||
|
|
||||||
Dieses Dokument beschreibt die implementierten Sicherheitsmaßnahmen für das Payload CMS Multi-Tenant-Projekt.
|
Dieses Dokument beschreibt die implementierten Sicherheitsmaßnahmen für das Payload CMS Multi-Tenant-Projekt.
|
||||||
|
|
||||||
|
**Umgebungen:**
|
||||||
|
|
||||||
|
| Umgebung | URL | TRUST_PROXY |
|
||||||
|
|----------|-----|-------------|
|
||||||
|
| Production | https://cms.c2sgmbh.de | `true` (Nginx) |
|
||||||
|
| Staging | https://pl.porwoll.tech | `true` (Caddy) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Security-Module
|
## Security-Module
|
||||||
|
|
@ -289,16 +296,48 @@ Das Admin Panel verwendet eine Custom Login Route (`src/app/(payload)/api/users/
|
||||||
|
|
||||||
- **Audit-Logging:** Jeder Login-Versuch wird in AuditLogs protokolliert
|
- **Audit-Logging:** Jeder Login-Versuch wird in AuditLogs protokolliert
|
||||||
- **Rate-Limiting:** 5 Versuche pro 15 Minuten (authLimiter)
|
- **Rate-Limiting:** 5 Versuche pro 15 Minuten (authLimiter)
|
||||||
|
- **Browser-Redirect:** Sichere Weiterleitung nach erfolgreichem Login
|
||||||
- **Content-Type Support:**
|
- **Content-Type Support:**
|
||||||
- JSON (`application/json`)
|
- JSON (`application/json`)
|
||||||
- FormData mit `_payload` JSON-Feld (Payload Admin Panel Format)
|
- FormData mit `_payload` JSON-Feld (Payload Admin Panel Format)
|
||||||
- Standard FormData (`multipart/form-data`)
|
- Standard FormData (`multipart/form-data`)
|
||||||
- URL-encoded (`application/x-www-form-urlencoded`)
|
- URL-encoded (`application/x-www-form-urlencoded`)
|
||||||
|
|
||||||
|
**Browser Form Redirect:**
|
||||||
|
```
|
||||||
|
POST /api/users/login?redirect=/admin/collections/posts
|
||||||
|
Content-Type: application/x-www-form-urlencoded
|
||||||
|
|
||||||
|
email=admin@example.com&password=secret
|
||||||
|
```
|
||||||
|
|
||||||
|
**Redirect-Validierung:**
|
||||||
|
- Nur relative Pfade erlaubt (`/admin/...`)
|
||||||
|
- Externe URLs werden blockiert
|
||||||
|
- Protocol-Handler (`javascript:`, `data:`) abgelehnt
|
||||||
|
- Default: `/admin` bei fehlendem/ungültigem Redirect
|
||||||
|
|
||||||
**Sicherheitsaspekte:**
|
**Sicherheitsaspekte:**
|
||||||
- Passwort wird nie in Logs/Responses exponiert
|
- Passwort wird nie in Logs/Responses exponiert
|
||||||
- Fehlgeschlagene Login-Versuche werden mit IP und User-Agent geloggt
|
- Fehlgeschlagene Login-Versuche werden mit IP und User-Agent geloggt
|
||||||
- Rate-Limiting verhindert Brute-Force-Angriffe
|
- Rate-Limiting verhindert Brute-Force-Angriffe
|
||||||
|
- Open Redirect Prevention durch URL-Validierung
|
||||||
|
|
||||||
|
### Custom Admin Login Page
|
||||||
|
|
||||||
|
Eine optionale Custom Login-Seite ist verfügbar unter `src/app/(payload)/admin/login/`:
|
||||||
|
|
||||||
|
```
|
||||||
|
src/app/(payload)/admin/login/
|
||||||
|
├── page.tsx # Login-Formular mit Styling
|
||||||
|
└── page.module.scss # Custom Styles
|
||||||
|
```
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- Styled Login-Form passend zum Admin-Theme
|
||||||
|
- Redirect-Parameter Support (`?redirect=/admin/...`)
|
||||||
|
- Fehlerbehandlung mit User-Feedback
|
||||||
|
- Kompatibel mit Payload's Session-Management
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -306,8 +345,26 @@ Das Admin Panel verwendet eine Custom Login Route (`src/app/(payload)/api/users/
|
||||||
|
|
||||||
| Datum | Änderung |
|
| Datum | Änderung |
|
||||||
|-------|----------|
|
|-------|----------|
|
||||||
|
| 18.12.2025 | **Custom Admin Login Page:** Styled Login-Formular, Browser-Redirect mit Safe-URL-Validierung, Open Redirect Prevention |
|
||||||
| 17.12.2025 | **Security-Audit Fixes:** TRUST_PROXY für IP-Header-Spoofing, CSRF_SECRET Pflicht in Production, IP-Allowlist Startup-Warnungen, Tests auf 177 erweitert |
|
| 17.12.2025 | **Security-Audit Fixes:** TRUST_PROXY für IP-Header-Spoofing, CSRF_SECRET Pflicht in Production, IP-Allowlist Startup-Warnungen, Tests auf 177 erweitert |
|
||||||
| 09.12.2025 | Custom Login Route Dokumentation, multipart/form-data _payload Support |
|
| 09.12.2025 | Custom Login Route Dokumentation, multipart/form-data _payload Support |
|
||||||
| 08.12.2025 | Security Test Suite (143 Tests) |
|
| 08.12.2025 | Security Test Suite (143 Tests) |
|
||||||
| 07.12.2025 | Rate Limiter, CSRF, IP Allowlist, Data Masking |
|
| 07.12.2025 | Rate Limiter, CSRF, IP Allowlist, Data Masking |
|
||||||
| 07.12.2025 | Pre-Commit Hook, GitHub Actions Workflow |
|
| 07.12.2025 | Pre-Commit Hook, GitHub Actions Workflow |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dateien
|
||||||
|
|
||||||
|
| Pfad | Beschreibung |
|
||||||
|
|------|--------------|
|
||||||
|
| `src/lib/security/rate-limiter.ts` | Rate Limiting mit Redis/Memory |
|
||||||
|
| `src/lib/security/ip-allowlist.ts` | IP-basierte Zugriffskontrolle |
|
||||||
|
| `src/lib/security/csrf.ts` | CSRF Token Generation & Validation |
|
||||||
|
| `src/lib/security/data-masking.ts` | Sensitive Data Masking |
|
||||||
|
| `src/app/(payload)/api/users/login/route.ts` | Custom Login API |
|
||||||
|
| `src/app/(payload)/admin/login/page.tsx` | Custom Login Page |
|
||||||
|
| `scripts/detect-secrets.sh` | Pre-Commit Secret Detection |
|
||||||
|
| `.github/workflows/security.yml` | CI Security Scanning |
|
||||||
|
| `tests/unit/security/` | Security Unit Tests |
|
||||||
|
| `tests/int/security-api.int.spec.ts` | Security Integration Tests |
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,60 @@
|
||||||
# SEO-Erweiterung
|
# SEO-Erweiterung
|
||||||
|
|
||||||
|
*Letzte Aktualisierung: 18. Dezember 2025*
|
||||||
|
|
||||||
## Übersicht
|
## Übersicht
|
||||||
|
|
||||||
Diese Dokumentation beschreibt die implementierten SEO-Features für das Payload CMS Multi-Tenant System.
|
Diese Dokumentation beschreibt die implementierten SEO-Features für das Payload CMS Multi-Tenant System.
|
||||||
|
|
||||||
|
**Wichtig:** Frontends verwenden die **Production-API** (cms.c2sgmbh.de) für SEO-Daten, um konsistente Meta-Tags und Structured Data zu gewährleisten.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API-Endpoints für Frontend
|
||||||
|
|
||||||
|
### SEO-Daten abrufen
|
||||||
|
|
||||||
|
| Endpoint | Beschreibung |
|
||||||
|
|----------|--------------|
|
||||||
|
| `GET /api/globals/seo-settings` | Globale SEO-Konfiguration |
|
||||||
|
| `GET /api/pages?where[slug][equals]=...` | Page-spezifische SEO (meta-Feld) |
|
||||||
|
| `GET /api/posts?where[slug][equals]=...` | Post-spezifische SEO |
|
||||||
|
|
||||||
|
### Beispiel: SEO-Settings laden
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Frontend: SEO-Defaults aus Production laden
|
||||||
|
const seoSettings = await fetch('https://cms.c2sgmbh.de/api/globals/seo-settings')
|
||||||
|
.then(r => r.json())
|
||||||
|
|
||||||
|
// Enthält:
|
||||||
|
// - metaDefaults (titleSuffix, defaultDescription, defaultImage)
|
||||||
|
// - organization (name, legalName, logo, foundingDate)
|
||||||
|
// - contact (email, phone, fax)
|
||||||
|
// - address (street, city, country, etc.)
|
||||||
|
// - socialProfiles (Array)
|
||||||
|
// - localBusiness (type, priceRange, openingHours)
|
||||||
|
// - robots (indexing, additionalDisallow)
|
||||||
|
// - verification (google, bing, yandex)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Beispiel: Page-SEO laden
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Page mit SEO-Daten laden
|
||||||
|
const page = await fetch(
|
||||||
|
'https://cms.c2sgmbh.de/api/pages?where[slug][equals]=about&where[tenant][equals]=4&locale=de'
|
||||||
|
).then(r => r.json())
|
||||||
|
|
||||||
|
// page.docs[0].meta enthält:
|
||||||
|
// - title (Page-spezifischer Titel)
|
||||||
|
// - description (Meta-Description)
|
||||||
|
// - image (OG-Image Override)
|
||||||
|
// - noIndex, noFollow (Indexierungssteuerung)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Implementierte Features
|
## Implementierte Features
|
||||||
|
|
||||||
### 1. Dynamische Sitemap (`/sitemap.xml`)
|
### 1. Dynamische Sitemap (`/sitemap.xml`)
|
||||||
|
|
@ -16,6 +67,7 @@ Die Sitemap wird dynamisch aus der Datenbank generiert und enthält:
|
||||||
- Alle veröffentlichten Posts mit typ-basierter URL (Priorität: 0.6, Änderungshäufigkeit: monatlich)
|
- Alle veröffentlichten Posts mit typ-basierter URL (Priorität: 0.6, Änderungshäufigkeit: monatlich)
|
||||||
|
|
||||||
**URL-Schema für Posts:**
|
**URL-Schema für Posts:**
|
||||||
|
|
||||||
| Post-Typ | URL-Prefix |
|
| Post-Typ | URL-Prefix |
|
||||||
|----------|------------|
|
|----------|------------|
|
||||||
| blog | `/blog/{slug}` |
|
| blog | `/blog/{slug}` |
|
||||||
|
|
@ -42,8 +94,8 @@ Allow: /
|
||||||
Disallow: /admin
|
Disallow: /admin
|
||||||
Disallow: /api
|
Disallow: /api
|
||||||
|
|
||||||
Host: https://pl.c2sgmbh.de
|
Host: https://cms.c2sgmbh.de
|
||||||
Sitemap: https://pl.c2sgmbh.de/sitemap.xml
|
Sitemap: https://cms.c2sgmbh.de/sitemap.xml
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Structured Data (JSON-LD)
|
### 3. Structured Data (JSON-LD)
|
||||||
|
|
@ -69,12 +121,26 @@ Bietet Helper-Funktionen für Schema.org-konforme JSON-LD Daten:
|
||||||
| `combineSchemas()` | Kombiniert mehrere Schemas |
|
| `combineSchemas()` | Kombiniert mehrere Schemas |
|
||||||
| `renderJsonLd()` | Sicheres Rendering von JSON-LD |
|
| `renderJsonLd()` | Sicheres Rendering von JSON-LD |
|
||||||
|
|
||||||
#### Verwendungsbeispiel
|
#### Verwendungsbeispiel (Frontend)
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
|
// src/components/seo/JsonLd.tsx
|
||||||
import { generateArticleSchema, renderJsonLd } from '@/lib/structuredData'
|
import { generateArticleSchema, renderJsonLd } from '@/lib/structuredData'
|
||||||
|
|
||||||
export default function BlogPost({ post }) {
|
interface BlogPostProps {
|
||||||
|
post: {
|
||||||
|
title: string
|
||||||
|
excerpt: string
|
||||||
|
slug: string
|
||||||
|
publishedAt: string
|
||||||
|
updatedAt: string
|
||||||
|
author?: { name: string }
|
||||||
|
featuredImage?: { url: string }
|
||||||
|
categories?: Array<{ title: string }>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function BlogPost({ post }: BlogPostProps) {
|
||||||
const schema = generateArticleSchema({
|
const schema = generateArticleSchema({
|
||||||
title: post.title,
|
title: post.title,
|
||||||
description: post.excerpt,
|
description: post.excerpt,
|
||||||
|
|
@ -84,7 +150,7 @@ export default function BlogPost({ post }) {
|
||||||
author: post.author,
|
author: post.author,
|
||||||
featuredImage: post.featuredImage,
|
featuredImage: post.featuredImage,
|
||||||
categories: post.categories,
|
categories: post.categories,
|
||||||
}, 'https://example.com')
|
}, 'https://complexcaresolutions.de') // Tenant-Domain
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -149,6 +215,117 @@ Globale SEO-Konfiguration im Admin-Panel unter "Einstellungen > SEO Einstellunge
|
||||||
- Bing Webmaster Tools
|
- Bing Webmaster Tools
|
||||||
- Yandex Webmaster
|
- Yandex Webmaster
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Frontend-Integration
|
||||||
|
|
||||||
|
### Next.js Metadata API
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/app/[locale]/page.tsx
|
||||||
|
import type { Metadata } from 'next'
|
||||||
|
|
||||||
|
async function getSeoSettings() {
|
||||||
|
const res = await fetch('https://cms.c2sgmbh.de/api/globals/seo-settings', {
|
||||||
|
next: { revalidate: 3600 } // 1 Stunde Cache
|
||||||
|
})
|
||||||
|
return res.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getPage(slug: string, tenantId: number, locale: string) {
|
||||||
|
const res = await fetch(
|
||||||
|
`https://cms.c2sgmbh.de/api/pages?where[slug][equals]=${slug}&where[tenant][equals]=${tenantId}&locale=${locale}`,
|
||||||
|
{ next: { revalidate: 60 } }
|
||||||
|
)
|
||||||
|
return res.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateMetadata({ params }): Promise<Metadata> {
|
||||||
|
const seoSettings = await getSeoSettings()
|
||||||
|
const pageData = await getPage(params.slug || 'home', 4, params.locale)
|
||||||
|
const page = pageData.docs[0]
|
||||||
|
|
||||||
|
const title = page?.meta?.title
|
||||||
|
? `${page.meta.title} ${seoSettings.metaDefaults?.titleSuffix || ''}`
|
||||||
|
: seoSettings.metaDefaults?.titleSuffix
|
||||||
|
|
||||||
|
const description = page?.meta?.description
|
||||||
|
|| seoSettings.metaDefaults?.defaultDescription
|
||||||
|
|
||||||
|
const image = page?.meta?.image?.url
|
||||||
|
|| seoSettings.metaDefaults?.defaultImage?.url
|
||||||
|
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
openGraph: {
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
images: image ? [{ url: image }] : [],
|
||||||
|
type: 'website',
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: 'summary_large_image',
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
images: image ? [image] : [],
|
||||||
|
},
|
||||||
|
robots: {
|
||||||
|
index: !page?.meta?.noIndex,
|
||||||
|
follow: !page?.meta?.noFollow,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verification Meta Tags
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/app/layout.tsx
|
||||||
|
export async function generateMetadata(): Promise<Metadata> {
|
||||||
|
const seoSettings = await getSeoSettings()
|
||||||
|
|
||||||
|
return {
|
||||||
|
verification: {
|
||||||
|
google: seoSettings.verification?.google,
|
||||||
|
// Bing und Yandex als other
|
||||||
|
other: {
|
||||||
|
'msvalidate.01': seoSettings.verification?.bing,
|
||||||
|
'yandex-verification': seoSettings.verification?.yandex,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Multi-Tenant SEO
|
||||||
|
|
||||||
|
Jeder Tenant hat eigene SEO-Konfigurationen. Die SEO Settings Global gilt pro Installation, aber Page/Post-SEO ist tenant-spezifisch.
|
||||||
|
|
||||||
|
### Tenant-spezifische Domains
|
||||||
|
|
||||||
|
| Tenant ID | Domain | Sitemap |
|
||||||
|
|-----------|--------|---------|
|
||||||
|
| 1 | porwoll.de | https://porwoll.de/sitemap.xml |
|
||||||
|
| 4 | complexcaresolutions.de | https://complexcaresolutions.de/sitemap.xml |
|
||||||
|
| 5 | gunshin.de | https://gunshin.de/sitemap.xml |
|
||||||
|
|
||||||
|
### Lokalisierung
|
||||||
|
|
||||||
|
SEO-Felder sind lokalisiert (de/en):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Deutscher Content
|
||||||
|
fetch('https://cms.c2sgmbh.de/api/pages?slug=about&tenant=4&locale=de')
|
||||||
|
|
||||||
|
// Englischer Content
|
||||||
|
fetch('https://cms.c2sgmbh.de/api/pages?slug=about&tenant=4&locale=en')
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Datenbank-Tabellen
|
## Datenbank-Tabellen
|
||||||
|
|
||||||
Die Migration `20251130_150000_blocks_tables.ts` erstellt:
|
Die Migration `20251130_150000_blocks_tables.ts` erstellt:
|
||||||
|
|
@ -159,15 +336,43 @@ Die Migration `20251130_150000_blocks_tables.ts` erstellt:
|
||||||
- `seo_settings_local_business_opening_hours` - Öffnungszeiten
|
- `seo_settings_local_business_opening_hours` - Öffnungszeiten
|
||||||
- `seo_settings_robots_additional_disallow` - Ausgeschlossene Pfade
|
- `seo_settings_robots_additional_disallow` - Ausgeschlossene Pfade
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## URLs
|
## URLs
|
||||||
|
|
||||||
- **Sitemap:** https://pl.c2sgmbh.de/sitemap.xml
|
### Production (für Frontends)
|
||||||
- **Robots:** https://pl.c2sgmbh.de/robots.txt
|
|
||||||
- **SEO Settings:** https://pl.c2sgmbh.de/admin/globals/seo-settings
|
|
||||||
|
|
||||||
## Nächste Schritte
|
| Resource | URL |
|
||||||
|
|----------|-----|
|
||||||
|
| **API Base** | https://cms.c2sgmbh.de/api |
|
||||||
|
| **SEO Settings** | https://cms.c2sgmbh.de/api/globals/seo-settings |
|
||||||
|
| **Sitemap** | https://cms.c2sgmbh.de/sitemap.xml |
|
||||||
|
| **Robots** | https://cms.c2sgmbh.de/robots.txt |
|
||||||
|
| **Admin Panel** | https://cms.c2sgmbh.de/admin/globals/seo-settings |
|
||||||
|
|
||||||
1. SEO Settings im Admin-Panel konfigurieren
|
### Development
|
||||||
2. JSON-LD in Frontend-Templates einbinden
|
|
||||||
3. Meta-Tags in Layout integrieren
|
| Resource | URL |
|
||||||
4. Google Search Console einrichten
|
|----------|-----|
|
||||||
|
| **API Base** | https://pl.porwoll.tech/api |
|
||||||
|
| **SEO Settings** | https://pl.porwoll.tech/api/globals/seo-settings |
|
||||||
|
| **Admin Panel** | https://pl.porwoll.tech/admin/globals/seo-settings |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Checkliste: SEO-Setup pro Tenant
|
||||||
|
|
||||||
|
- [ ] SEO Settings im Admin-Panel konfigurieren
|
||||||
|
- [ ] Organisation (Name, Logo, Beschreibung)
|
||||||
|
- [ ] Kontaktdaten und Adresse
|
||||||
|
- [ ] Social Media Profile hinzufügen
|
||||||
|
- [ ] Local Business aktivieren (falls relevant)
|
||||||
|
- [ ] Google Search Console Code eintragen
|
||||||
|
- [ ] JSON-LD in Frontend-Templates einbinden
|
||||||
|
- [ ] Meta-Tags in Layout integrieren
|
||||||
|
- [ ] Sitemap bei Google Search Console einreichen
|
||||||
|
- [ ] robots.txt prüfen
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Letzte Aktualisierung: 18. Dezember 2025*
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@
|
||||||
### Dokumentation
|
### Dokumentation
|
||||||
| Status | Task |
|
| Status | Task |
|
||||||
|--------|------|
|
|--------|------|
|
||||||
| [ ] | DEPLOYMENT.md erstellen |
|
| [x] | DEPLOYMENT.md erstellen |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -173,7 +173,7 @@
|
||||||
|
|
||||||
## Dokumentation
|
## Dokumentation
|
||||||
|
|
||||||
- [ ] DEPLOYMENT.md (Deployment-Prozess)
|
- [x] DEPLOYMENT.md (Deployment-Prozess) *(erledigt: 18.12.2025)*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -222,12 +222,21 @@
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Letzte Aktualisierung: 17.12.2025*
|
*Letzte Aktualisierung: 18.12.2025*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
|
### 18.12.2025
|
||||||
|
- **Dokumentation konsolidiert:**
|
||||||
|
- CLAUDE.md: Tech-Stack auf aktuelle Versionen aktualisiert (Payload 3.68.4, Next.js 15.5.9, React 19.2.3)
|
||||||
|
- CLAUDE.md: Architektur-Diagramm erweitert (sv-caddy, sv-frontend hinzugefügt)
|
||||||
|
- CLAUDE.md: Videos/VideoCategories Collections hinzugefügt
|
||||||
|
- docs/INFRASTRUCTURE.md: Komplett aktualisiert mit neuer Infrastruktur
|
||||||
|
- docs/PROJECT_STATUS.md: Neues Statusdokument behalten
|
||||||
|
- Obsolete Dateien entfernt: INFRASTRUCTURE_COMPLETE_DECEMBER_2025.md, TECHSTACK_COMPLETE_DECEMBER_2025.md
|
||||||
|
|
||||||
### 17.12.2025
|
### 17.12.2025
|
||||||
- **Security Code-Review abgeschlossen:**
|
- **Security Code-Review abgeschlossen:**
|
||||||
- **IP Header Spoofing behoben:** `X-Forwarded-For`/`X-Real-IP` werden nur bei `TRUST_PROXY=true` vertraut
|
- **IP Header Spoofing behoben:** `X-Forwarded-For`/`X-Real-IP` werden nur bei `TRUST_PROXY=true` vertraut
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,59 @@
|
||||||
# Universal Features - Dokumentation
|
# Universal Features - Dokumentation
|
||||||
|
|
||||||
|
*Letzte Aktualisierung: 18. Dezember 2025*
|
||||||
|
|
||||||
## Übersicht
|
## Übersicht
|
||||||
|
|
||||||
Die Universal Features erweitern das Payload CMS um wiederverwendbare Collections und Blocks für Blog, News, Testimonials, Newsletter und Prozess-Darstellungen. Alle Features sind Multi-Tenant-fähig.
|
Die Universal Features erweitern das Payload CMS um wiederverwendbare Collections und Blocks für Blog, News, Testimonials, Newsletter, FAQ, Team und Prozess-Darstellungen. Alle Features sind Multi-Tenant-fähig.
|
||||||
|
|
||||||
|
**Wichtig:** Frontends verwenden die **Production-API** (cms.c2sgmbh.de).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API-Endpoints für Frontend
|
||||||
|
|
||||||
|
### Collections
|
||||||
|
|
||||||
|
| Collection | Endpoint | Beschreibung |
|
||||||
|
|------------|----------|--------------|
|
||||||
|
| Posts | `GET /api/posts` | Blog, News, Presse |
|
||||||
|
| Testimonials | `GET /api/testimonials` | Kundenbewertungen |
|
||||||
|
| FAQs | `GET /api/faqs` | FAQ-Einträge |
|
||||||
|
| Team | `GET /api/team` | Team-Mitglieder |
|
||||||
|
| Services | `GET /api/services` | Leistungen |
|
||||||
|
| Timelines | `GET /api/timelines` | Chronologische Events |
|
||||||
|
| Workflows | `GET /api/workflows` | Prozess-Darstellungen |
|
||||||
|
|
||||||
|
### Beispiel: Posts laden
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Featured Blog-Posts für Tenant C2S laden
|
||||||
|
const posts = await fetch(
|
||||||
|
'https://cms.c2sgmbh.de/api/posts?where[tenant][equals]=4&where[type][equals]=blog&where[isFeatured][equals]=true&locale=de&limit=6'
|
||||||
|
).then(r => r.json())
|
||||||
|
```
|
||||||
|
|
||||||
|
### Newsletter API
|
||||||
|
|
||||||
|
| Endpoint | Methode | Beschreibung |
|
||||||
|
|----------|---------|--------------|
|
||||||
|
| `/api/newsletter/subscribe` | POST | Newsletter-Anmeldung |
|
||||||
|
| `/api/newsletter/confirm` | GET/POST | Double Opt-In Bestätigung |
|
||||||
|
| `/api/newsletter/unsubscribe` | GET/POST | Abmeldung |
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Newsletter-Anmeldung
|
||||||
|
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',
|
||||||
|
tenantId: 4,
|
||||||
|
source: 'footer'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -31,7 +82,7 @@ Die Posts Collection dient zur Verwaltung von Blog-Artikeln, News, Pressemitteil
|
||||||
| `author` | Text | Autorname | Nein |
|
| `author` | Text | Autorname | Nein |
|
||||||
| `status` | Select | draft/published/archived | Nein |
|
| `status` | Select | draft/published/archived | Nein |
|
||||||
| `publishedAt` | Date | Veröffentlichungsdatum | Nein |
|
| `publishedAt` | Date | Veröffentlichungsdatum | Nein |
|
||||||
| `seo` | Group | SEO-Einstellungen | Nein |
|
| `meta` | Group | SEO-Einstellungen | Nein |
|
||||||
|
|
||||||
#### Post-Typen
|
#### Post-Typen
|
||||||
|
|
||||||
|
|
@ -40,11 +91,6 @@ Die Posts Collection dient zur Verwaltung von Blog-Artikeln, News, Pressemitteil
|
||||||
- `press` - Pressemitteilung
|
- `press` - Pressemitteilung
|
||||||
- `announcement` - Ankündigung
|
- `announcement` - Ankündigung
|
||||||
|
|
||||||
#### Access Control
|
|
||||||
|
|
||||||
- **Read:** Öffentlich für veröffentlichte Beiträge des eigenen Tenants
|
|
||||||
- **Create/Update/Delete:** Nur authentifizierte Benutzer
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 2. Testimonials Collection
|
### 2. Testimonials Collection
|
||||||
|
|
@ -71,11 +117,6 @@ Kundenstimmen und Bewertungen für Referenz-Seiten.
|
||||||
| `isActive` | Checkbox | Sichtbarkeit | Nein |
|
| `isActive` | Checkbox | Sichtbarkeit | Nein |
|
||||||
| `order` | Number | Sortierung | Nein |
|
| `order` | Number | Sortierung | Nein |
|
||||||
|
|
||||||
#### Access Control
|
|
||||||
|
|
||||||
- **Read:** Öffentlich für aktive Testimonials des eigenen Tenants
|
|
||||||
- **Create/Update/Delete:** Nur authentifizierte Benutzer
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 3. Newsletter Subscribers Collection
|
### 3. Newsletter Subscribers Collection
|
||||||
|
|
@ -84,7 +125,7 @@ Kundenstimmen und Bewertungen für Referenz-Seiten.
|
||||||
**Slug:** `newsletter-subscribers`
|
**Slug:** `newsletter-subscribers`
|
||||||
**Admin-Gruppe:** Marketing
|
**Admin-Gruppe:** Marketing
|
||||||
|
|
||||||
DSGVO-konforme Speicherung von Newsletter-Anmeldungen mit Double Opt-In Support.
|
DSGVO-konforme Speicherung von Newsletter-Anmeldungen mit Double Opt-In.
|
||||||
|
|
||||||
#### Felder
|
#### Felder
|
||||||
|
|
||||||
|
|
@ -103,6 +144,14 @@ DSGVO-konforme Speicherung von Newsletter-Anmeldungen mit Double Opt-In Support.
|
||||||
| `ipAddress` | Text | IP-Adresse (DSGVO) | Nein |
|
| `ipAddress` | Text | IP-Adresse (DSGVO) | Nein |
|
||||||
| `userAgent` | Text | Browser-Info | Nein |
|
| `userAgent` | Text | Browser-Info | Nein |
|
||||||
|
|
||||||
|
#### Double Opt-In Flow
|
||||||
|
|
||||||
|
1. User meldet sich an → Status: `pending`, Token generiert
|
||||||
|
2. E-Mail mit Bestätigungs-Link wird automatisch gesendet
|
||||||
|
3. User klickt Link → Status: `confirmed`
|
||||||
|
4. Willkommens-E-Mail wird gesendet
|
||||||
|
5. Abmeldung jederzeit über Link möglich
|
||||||
|
|
||||||
#### Status-Werte
|
#### Status-Werte
|
||||||
|
|
||||||
- `pending` - Ausstehend (Double Opt-In)
|
- `pending` - Ausstehend (Double Opt-In)
|
||||||
|
|
@ -110,29 +159,124 @@ DSGVO-konforme Speicherung von Newsletter-Anmeldungen mit Double Opt-In Support.
|
||||||
- `unsubscribed` - Abgemeldet
|
- `unsubscribed` - Abgemeldet
|
||||||
- `bounced` - E-Mail nicht zustellbar
|
- `bounced` - E-Mail nicht zustellbar
|
||||||
|
|
||||||
#### Interessen-Optionen
|
---
|
||||||
|
|
||||||
- `general` - Allgemeine Updates
|
### 4. FAQs Collection
|
||||||
- `blog` - Blog-Artikel
|
|
||||||
- `products` - Produkt-News
|
|
||||||
- `offers` - Angebote & Aktionen
|
|
||||||
- `events` - Events
|
|
||||||
|
|
||||||
#### Access Control
|
**Pfad:** `src/collections/FAQs.ts`
|
||||||
|
**Slug:** `faqs`
|
||||||
|
**Admin-Gruppe:** Content
|
||||||
|
|
||||||
- **Read:** Nur authentifizierte Benutzer (Datenschutz!)
|
Häufig gestellte Fragen mit Kategorisierung.
|
||||||
- **Create:** Öffentlich (für Anmeldungen)
|
|
||||||
- **Update/Delete:** Nur authentifizierte Benutzer
|
|
||||||
|
|
||||||
#### Automatische Hooks
|
#### Felder
|
||||||
|
|
||||||
Bei der Erstellung wird automatisch:
|
| Feld | Typ | Beschreibung | Pflicht |
|
||||||
- `subscribedAt` auf aktuelles Datum gesetzt
|
|------|-----|--------------|---------|
|
||||||
- `confirmationToken` generiert (UUID)
|
| `question` | Text | Frage | Ja |
|
||||||
|
| `answer` | RichText | Antwort | Ja |
|
||||||
|
| `category` | Text | Kategorie | Nein |
|
||||||
|
| `order` | Number | Sortierung | Nein |
|
||||||
|
| `isActive` | Checkbox | Sichtbarkeit | Nein |
|
||||||
|
|
||||||
Bei Status-Änderungen:
|
---
|
||||||
- `confirmedAt` wird gesetzt bei Wechsel zu "confirmed"
|
|
||||||
- `unsubscribedAt` wird gesetzt bei Wechsel zu "unsubscribed"
|
### 5. Team Collection
|
||||||
|
|
||||||
|
**Pfad:** `src/collections/Team.ts`
|
||||||
|
**Slug:** `team`
|
||||||
|
**Admin-Gruppe:** Content
|
||||||
|
|
||||||
|
Team-Mitglieder und Mitarbeiter.
|
||||||
|
|
||||||
|
#### Felder
|
||||||
|
|
||||||
|
| Feld | Typ | Beschreibung | Pflicht |
|
||||||
|
|------|-----|--------------|---------|
|
||||||
|
| `name` | Text | Name | Ja |
|
||||||
|
| `role` | Text | Position/Rolle | Nein |
|
||||||
|
| `image` | Upload | Portrait-Foto | Nein |
|
||||||
|
| `bio` | Textarea | Kurzbiografie | Nein |
|
||||||
|
| `email` | Email | E-Mail-Adresse | Nein |
|
||||||
|
| `phone` | Text | Telefon | Nein |
|
||||||
|
| `socialLinks` | Array | Social Media Links | Nein |
|
||||||
|
| `order` | Number | Sortierung | Nein |
|
||||||
|
| `isActive` | Checkbox | Sichtbarkeit | Nein |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. Services Collection
|
||||||
|
|
||||||
|
**Pfad:** `src/collections/Services.ts`
|
||||||
|
**Slug:** `services`
|
||||||
|
**Admin-Gruppe:** Content
|
||||||
|
|
||||||
|
Leistungen und Dienstleistungen.
|
||||||
|
|
||||||
|
#### Felder
|
||||||
|
|
||||||
|
| Feld | Typ | Beschreibung | Pflicht |
|
||||||
|
|------|-----|--------------|---------|
|
||||||
|
| `title` | Text | Leistungsname | Ja |
|
||||||
|
| `slug` | Text | URL-Pfad | Ja |
|
||||||
|
| `excerpt` | Textarea | Kurzbeschreibung | Nein |
|
||||||
|
| `content` | RichText | Detailbeschreibung | Nein |
|
||||||
|
| `icon` | Text | Icon-Name | Nein |
|
||||||
|
| `image` | Upload | Bild | Nein |
|
||||||
|
| `category` | Relationship | Service-Kategorie | Nein |
|
||||||
|
| `order` | Number | Sortierung | Nein |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7. Timelines Collection
|
||||||
|
|
||||||
|
**Pfad:** `src/collections/Timelines.ts`
|
||||||
|
**Slug:** `timelines`
|
||||||
|
**Admin-Gruppe:** Content
|
||||||
|
|
||||||
|
Chronologische Events für Geschichte, Meilensteine, Releases.
|
||||||
|
|
||||||
|
#### API
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Timeline laden
|
||||||
|
fetch('https://cms.c2sgmbh.de/api/timelines?tenant=4&slug=company-history&locale=de')
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Timeline-Typen
|
||||||
|
|
||||||
|
- `history` - Unternehmensgeschichte
|
||||||
|
- `milestones` - Projektmeilensteine
|
||||||
|
- `releases` - Produkt-Releases
|
||||||
|
- `career` - Karriere/Lebenslauf
|
||||||
|
- `events` - Ereignisse
|
||||||
|
- `process` - Prozess/Ablauf
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8. Workflows Collection
|
||||||
|
|
||||||
|
**Pfad:** `src/collections/Workflows.ts`
|
||||||
|
**Slug:** `workflows`
|
||||||
|
**Admin-Gruppe:** Content
|
||||||
|
|
||||||
|
Komplexe Prozesse mit Phasen, Abhängigkeiten und Status-Tracking.
|
||||||
|
|
||||||
|
#### API
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Workflow laden
|
||||||
|
fetch('https://cms.c2sgmbh.de/api/workflows?tenant=4&type=project&locale=de')
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Workflow-Typen
|
||||||
|
|
||||||
|
- `project` - Projektabläufe
|
||||||
|
- `business` - Geschäftsprozesse
|
||||||
|
- `approval` - Genehmigungs-Workflows
|
||||||
|
- `onboarding` - Mitarbeiter-/Kundeneinführung
|
||||||
|
- `support` - Support/Service-Prozesse
|
||||||
|
- `development` - Entwicklungsprozesse
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -140,6 +284,19 @@ Bei Status-Änderungen:
|
||||||
|
|
||||||
Alle Blocks können in der Pages Collection verwendet werden.
|
Alle Blocks können in der Pages Collection verwendet werden.
|
||||||
|
|
||||||
|
### Block-Übersicht
|
||||||
|
|
||||||
|
| Block | Slug | Beschreibung |
|
||||||
|
|-------|------|--------------|
|
||||||
|
| Posts List | `posts-list-block` | Blog/News-Liste |
|
||||||
|
| Testimonials | `testimonials-block` | Kundenstimmen |
|
||||||
|
| Newsletter | `newsletter-block` | Anmeldeformular |
|
||||||
|
| Process Steps | `process-steps-block` | Prozess-Schritte |
|
||||||
|
| Timeline | `timeline-block` | Chronologie |
|
||||||
|
| FAQ | `faq-block` | FAQ-Akkordeon |
|
||||||
|
| Team | `team-block` | Team-Mitglieder |
|
||||||
|
| Services | `services-block` | Leistungen |
|
||||||
|
|
||||||
### 1. Posts List Block
|
### 1. Posts List Block
|
||||||
|
|
||||||
**Slug:** `posts-list-block`
|
**Slug:** `posts-list-block`
|
||||||
|
|
@ -164,8 +321,6 @@ Zeigt eine Liste von Blog-Artikeln, News oder anderen Post-Typen an.
|
||||||
| `showCategory` | Checkbox | true | Kategorie anzeigen |
|
| `showCategory` | Checkbox | true | Kategorie anzeigen |
|
||||||
| `showPagination` | Checkbox | false | Pagination |
|
| `showPagination` | Checkbox | false | Pagination |
|
||||||
| `showReadMore` | Checkbox | true | "Alle anzeigen" Link |
|
| `showReadMore` | Checkbox | true | "Alle anzeigen" Link |
|
||||||
| `readMoreLabel` | Text | "Alle Beiträge anzeigen" | Link-Text |
|
|
||||||
| `readMoreLink` | Text | /blog | Ziel-URL |
|
|
||||||
| `backgroundColor` | Select | white | Hintergrundfarbe |
|
| `backgroundColor` | Select | white | Hintergrundfarbe |
|
||||||
|
|
||||||
#### Layout-Optionen
|
#### Layout-Optionen
|
||||||
|
|
@ -184,25 +339,6 @@ Zeigt eine Liste von Blog-Artikeln, News oder anderen Post-Typen an.
|
||||||
|
|
||||||
Zeigt Kundenstimmen aus der Testimonials Collection.
|
Zeigt Kundenstimmen aus der Testimonials Collection.
|
||||||
|
|
||||||
#### Konfigurationsoptionen
|
|
||||||
|
|
||||||
| Option | Typ | Standard | Beschreibung |
|
|
||||||
|--------|-----|----------|--------------|
|
|
||||||
| `title` | Text | "Das sagen unsere Kunden" | Überschrift |
|
|
||||||
| `subtitle` | Text | - | Untertitel |
|
|
||||||
| `layout` | Select | slider | Darstellung |
|
|
||||||
| `columns` | Select | 3 | Spalten (bei Grid) |
|
|
||||||
| `displayMode` | Select | all | Auswahl-Modus |
|
|
||||||
| `selectedTestimonials` | Relationship | - | Handverlesene Auswahl |
|
|
||||||
| `limit` | Number | 6 | Max. Anzahl |
|
|
||||||
| `showRating` | Checkbox | true | Sterne anzeigen |
|
|
||||||
| `showImage` | Checkbox | true | Foto anzeigen |
|
|
||||||
| `showCompany` | Checkbox | true | Unternehmen anzeigen |
|
|
||||||
| `showSource` | Checkbox | false | Quelle anzeigen |
|
|
||||||
| `autoplay` | Checkbox | true | Auto-Wechsel (Slider) |
|
|
||||||
| `autoplaySpeed` | Number | 5000 | Wechselintervall (ms) |
|
|
||||||
| `backgroundColor` | Select | light | Hintergrundfarbe |
|
|
||||||
|
|
||||||
#### Layout-Optionen
|
#### Layout-Optionen
|
||||||
|
|
||||||
- `slider` - Karussell
|
- `slider` - Karussell
|
||||||
|
|
@ -219,27 +355,6 @@ Zeigt Kundenstimmen aus der Testimonials Collection.
|
||||||
|
|
||||||
Anmeldeformular für Newsletter mit DSGVO-Hinweis.
|
Anmeldeformular für Newsletter mit DSGVO-Hinweis.
|
||||||
|
|
||||||
#### Konfigurationsoptionen
|
|
||||||
|
|
||||||
| Option | Typ | Standard | Beschreibung |
|
|
||||||
|--------|-----|----------|--------------|
|
|
||||||
| `title` | Text | "Newsletter abonnieren" | Überschrift |
|
|
||||||
| `subtitle` | Textarea | Standard-Text | Beschreibung |
|
|
||||||
| `layout` | Select | inline | Formular-Layout |
|
|
||||||
| `image` | Upload | - | Bild (bei with-image) |
|
|
||||||
| `imagePosition` | Select | left | Bildposition |
|
|
||||||
| `collectName` | Checkbox | false | Name abfragen |
|
|
||||||
| `showInterests` | Checkbox | false | Interessen anzeigen |
|
|
||||||
| `availableInterests` | Select (hasMany) | - | Verfügbare Interessen |
|
|
||||||
| `buttonText` | Text | "Anmelden" | Button-Text |
|
|
||||||
| `placeholderEmail` | Text | "Ihre E-Mail-Adresse" | Placeholder |
|
|
||||||
| `successMessage` | Textarea | Standard-Text | Erfolgsmeldung |
|
|
||||||
| `errorMessage` | Text | Standard-Text | Fehlermeldung |
|
|
||||||
| `privacyText` | Textarea | Standard-Text | Datenschutz-Hinweis |
|
|
||||||
| `privacyLink` | Text | /datenschutz | Link zur DSE |
|
|
||||||
| `source` | Text | website | Tracking-Quelle |
|
|
||||||
| `backgroundColor` | Select | accent | Hintergrundfarbe |
|
|
||||||
|
|
||||||
#### Layout-Optionen
|
#### Layout-Optionen
|
||||||
|
|
||||||
- `inline` - Eingabe + Button nebeneinander
|
- `inline` - Eingabe + Button nebeneinander
|
||||||
|
|
@ -250,85 +365,54 @@ Anmeldeformular für Newsletter mit DSGVO-Hinweis.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 4. Process Steps Block
|
### 4. FAQ Block
|
||||||
|
|
||||||
**Slug:** `process-steps-block`
|
**Slug:** `faq-block`
|
||||||
|
|
||||||
Zeigt Prozess-Schritte / "So funktioniert es" Darstellungen.
|
FAQ-Akkordeon mit Schema.org Markup.
|
||||||
|
|
||||||
#### Konfigurationsoptionen
|
#### Konfigurationsoptionen
|
||||||
|
|
||||||
| Option | Typ | Standard | Beschreibung |
|
| Option | Typ | Beschreibung |
|
||||||
|--------|-----|----------|--------------|
|
|--------|-----|--------------|
|
||||||
| `title` | Text | "So funktioniert es" | Überschrift |
|
| `title` | Text | Überschrift |
|
||||||
| `subtitle` | Text | - | Untertitel |
|
| `subtitle` | Text | Untertitel |
|
||||||
| `layout` | Select | horizontal | Darstellung |
|
| `displayMode` | Select | all / selected / byCategory |
|
||||||
| `showNumbers` | Checkbox | true | Schritt-Nummern |
|
| `selectedFaqs` | Relationship | Handverlesene FAQs |
|
||||||
| `showIcons` | Checkbox | true | Icons anzeigen |
|
| `filterCategory` | Text | Kategorie-Filter |
|
||||||
| `steps` | Array | - | Schritte (2-10) |
|
| `layout` | Select | accordion / list / grid |
|
||||||
| `cta.show` | Checkbox | false | CTA anzeigen |
|
| `expandFirst` | Checkbox | Erste FAQ offen |
|
||||||
| `cta.label` | Text | "Jetzt starten" | Button-Text |
|
| `showSchema` | Checkbox | JSON-LD generieren |
|
||||||
| `cta.href` | Text | - | Button-Link |
|
|
||||||
| `cta.variant` | Select | default | Button-Stil |
|
|
||||||
| `backgroundColor` | Select | white | Hintergrundfarbe |
|
|
||||||
|
|
||||||
#### Schritte-Felder
|
|
||||||
|
|
||||||
- `title` - Schritt-Titel (Pflicht)
|
|
||||||
- `description` - Beschreibung
|
|
||||||
- `icon` - Emoji oder Icon-Name
|
|
||||||
- `image` - Optionales Bild
|
|
||||||
|
|
||||||
#### Layout-Optionen
|
|
||||||
|
|
||||||
- `horizontal` - Nebeneinander
|
|
||||||
- `vertical` - Untereinander
|
|
||||||
- `alternating` - Zickzack
|
|
||||||
- `connected` - Mit Verbindungslinien
|
|
||||||
- `timeline` - Timeline-Stil
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 5. Timeline Block
|
### 5. Team Block
|
||||||
|
|
||||||
**Slug:** `timeline-block`
|
**Slug:** `team-block`
|
||||||
|
|
||||||
Chronologische Darstellung von Ereignissen (z.B. Firmengeschichte).
|
Team-Mitglieder aus der Team Collection.
|
||||||
|
|
||||||
#### Konfigurationsoptionen
|
|
||||||
|
|
||||||
| Option | Typ | Standard | Beschreibung |
|
|
||||||
|--------|-----|----------|--------------|
|
|
||||||
| `title` | Text | - | Überschrift |
|
|
||||||
| `subtitle` | Text | - | Untertitel |
|
|
||||||
| `layout` | Select | vertical | Darstellung |
|
|
||||||
| `showConnector` | Checkbox | true | Verbindungslinie |
|
|
||||||
| `markerStyle` | Select | dot | Marker-Stil |
|
|
||||||
| `items` | Array | - | Einträge |
|
|
||||||
| `backgroundColor` | Select | white | Hintergrundfarbe |
|
|
||||||
|
|
||||||
#### Einträge-Felder
|
|
||||||
|
|
||||||
- `year` - Jahr/Datum
|
|
||||||
- `title` - Titel (Pflicht)
|
|
||||||
- `description` - Beschreibung
|
|
||||||
- `icon` - Emoji oder Icon
|
|
||||||
- `image` - Optionales Bild
|
|
||||||
- `link.label` - Link-Text
|
|
||||||
- `link.href` - Link-URL
|
|
||||||
|
|
||||||
#### Layout-Optionen
|
#### Layout-Optionen
|
||||||
|
|
||||||
- `vertical` - Standard vertikal
|
- `grid` - Karten im Grid
|
||||||
- `alternating` - Links/Rechts wechselnd
|
- `list` - Listenansicht
|
||||||
- `horizontal` - Horizontale Zeitleiste
|
- `carousel` - Karussell
|
||||||
|
- `compact` - Kompakt
|
||||||
|
|
||||||
#### Marker-Stile
|
---
|
||||||
|
|
||||||
- `dot` - Punkt
|
### 6. Services Block
|
||||||
- `number` - Nummer
|
|
||||||
- `icon` - Icon
|
**Slug:** `services-block`
|
||||||
- `date` - Jahr/Datum
|
|
||||||
|
Leistungen aus der Services Collection.
|
||||||
|
|
||||||
|
#### Layout-Optionen
|
||||||
|
|
||||||
|
- `grid` - Karten im Grid
|
||||||
|
- `list` - Listenansicht mit Details
|
||||||
|
- `icons` - Icon-Grid
|
||||||
|
- `tabs` - Tab-Navigation
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -344,6 +428,11 @@ multiTenantPlugin({
|
||||||
posts: {},
|
posts: {},
|
||||||
testimonials: {},
|
testimonials: {},
|
||||||
'newsletter-subscribers': {},
|
'newsletter-subscribers': {},
|
||||||
|
faqs: {},
|
||||||
|
team: {},
|
||||||
|
services: {},
|
||||||
|
timelines: {},
|
||||||
|
workflows: {},
|
||||||
// ... weitere Collections
|
// ... weitere Collections
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
@ -351,10 +440,18 @@ multiTenantPlugin({
|
||||||
|
|
||||||
### Tenant-Zuordnung
|
### Tenant-Zuordnung
|
||||||
|
|
||||||
Jedes Dokument enthält ein `tenant`-Feld, das auf die Tenants-Collection verweist. Die Access-Control-Funktionen in `src/lib/tenantAccess.ts` sorgen für die Isolation:
|
Jedes Dokument enthält ein `tenant`-Feld. Die Access-Control sorgt für Isolation:
|
||||||
|
|
||||||
- **Authentifizierte Admins:** Sehen alle Dokumente
|
- **Authentifizierte Admins:** Sehen alle Dokumente
|
||||||
- **Anonyme Requests:** Nur Dokumente des Tenants, der zur Domain passt
|
- **Anonyme Requests:** Nur Dokumente des passenden Tenants
|
||||||
|
|
||||||
|
### Tenant-IDs
|
||||||
|
|
||||||
|
| ID | Name | Slug |
|
||||||
|
|----|------|------|
|
||||||
|
| 1 | porwoll.de | porwoll |
|
||||||
|
| 4 | Complex Care Solutions GmbH | c2s |
|
||||||
|
| 5 | Gunshin | gunshin |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -363,38 +460,83 @@ Jedes Dokument enthält ein `tenant`-Feld, das auf die Tenants-Collection verwei
|
||||||
```
|
```
|
||||||
src/
|
src/
|
||||||
├── collections/
|
├── collections/
|
||||||
│ ├── Posts.ts # Blog/News Collection
|
│ ├── Posts.ts
|
||||||
│ ├── Testimonials.ts # Kundenstimmen
|
│ ├── Categories.ts
|
||||||
│ └── NewsletterSubscribers.ts # Newsletter-Anmeldungen
|
│ ├── Testimonials.ts
|
||||||
|
│ ├── NewsletterSubscribers.ts
|
||||||
|
│ ├── FAQs.ts
|
||||||
|
│ ├── Team.ts
|
||||||
|
│ ├── Services.ts
|
||||||
|
│ ├── ServiceCategories.ts
|
||||||
|
│ ├── Timelines.ts
|
||||||
|
│ └── Workflows.ts
|
||||||
├── blocks/
|
├── blocks/
|
||||||
│ ├── PostsListBlock.ts # Blog/News-Liste
|
│ ├── PostsListBlock.ts
|
||||||
│ ├── TestimonialsBlock.ts # Testimonials-Anzeige
|
│ ├── TestimonialsBlock.ts
|
||||||
│ ├── NewsletterBlock.ts # Newsletter-Formular
|
│ ├── NewsletterBlock.ts
|
||||||
│ ├── ProcessStepsBlock.ts # Prozess-Schritte
|
│ ├── ProcessStepsBlock.ts
|
||||||
│ ├── TimelineBlock.ts # Timeline
|
│ ├── TimelineBlock.ts
|
||||||
│ └── index.ts # Block-Exports
|
│ ├── FAQBlock.ts
|
||||||
|
│ ├── TeamBlock.ts
|
||||||
|
│ ├── ServicesBlock.ts
|
||||||
|
│ └── index.ts
|
||||||
├── lib/
|
├── lib/
|
||||||
│ └── tenantAccess.ts # Access-Control-Funktionen
|
│ ├── tenantAccess.ts
|
||||||
└── payload.config.ts # Haupt-Konfiguration
|
│ └── email/
|
||||||
|
│ ├── newsletter-service.ts
|
||||||
|
│ └── newsletter-templates.ts
|
||||||
|
├── hooks/
|
||||||
|
│ └── sendNewsletterConfirmation.ts
|
||||||
|
└── app/(payload)/api/newsletter/
|
||||||
|
├── subscribe/route.ts
|
||||||
|
├── confirm/route.ts
|
||||||
|
└── unsubscribe/route.ts
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Datenbank-Tabellen
|
## URLs
|
||||||
|
|
||||||
| Tabelle | Beschreibung |
|
### Production (für Frontends)
|
||||||
|---------|--------------|
|
|
||||||
| `posts` | Blog/News-Beiträge |
|
| Resource | URL |
|
||||||
| `posts_rels` | Kategorien-Beziehungen |
|
|----------|-----|
|
||||||
| `testimonials` | Kundenstimmen |
|
| Posts API | https://cms.c2sgmbh.de/api/posts |
|
||||||
| `newsletter_subscribers` | Newsletter-Anmeldungen |
|
| Testimonials API | https://cms.c2sgmbh.de/api/testimonials |
|
||||||
| `newsletter_subscribers_interests` | Interessen (hasMany) |
|
| FAQs API | https://cms.c2sgmbh.de/api/faqs |
|
||||||
| `pages_rels` | Block-Relationships |
|
| Team API | https://cms.c2sgmbh.de/api/team |
|
||||||
|
| Services API | https://cms.c2sgmbh.de/api/services |
|
||||||
|
| Timelines API | https://cms.c2sgmbh.de/api/timelines |
|
||||||
|
| Workflows API | https://cms.c2sgmbh.de/api/workflows |
|
||||||
|
| Newsletter Subscribe | https://cms.c2sgmbh.de/api/newsletter/subscribe |
|
||||||
|
|
||||||
|
### Development
|
||||||
|
|
||||||
|
| Resource | URL |
|
||||||
|
|----------|-----|
|
||||||
|
| API Base | https://pl.porwoll.tech/api |
|
||||||
|
| Admin Panel | https://pl.porwoll.tech/admin |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
|
### Version 1.2 (18.12.2025)
|
||||||
|
|
||||||
|
- Dokumentation auf Production-URLs aktualisiert
|
||||||
|
- API-Endpoints für Frontend hinzugefügt
|
||||||
|
- Newsletter Double Opt-In Flow dokumentiert
|
||||||
|
- Timelines und Workflows Collections hinzugefügt
|
||||||
|
- FAQ, Team, Services Blocks dokumentiert
|
||||||
|
|
||||||
|
### Version 1.1 (14.12.2025)
|
||||||
|
|
||||||
|
- Timelines Collection für chronologische Darstellungen
|
||||||
|
- Workflows Collection für Prozesse
|
||||||
|
- FAQs Collection mit Kategorisierung
|
||||||
|
- Team Collection mit Social Links
|
||||||
|
- Services Collection mit Kategorien
|
||||||
|
|
||||||
### Version 1.0 (30.11.2025)
|
### Version 1.0 (30.11.2025)
|
||||||
|
|
||||||
- Posts Collection um `type`, `isFeatured`, `excerpt` erweitert
|
- Posts Collection um `type`, `isFeatured`, `excerpt` erweitert
|
||||||
|
|
@ -402,4 +544,7 @@ src/
|
||||||
- NewsletterSubscribers Collection erstellt (DSGVO-konform)
|
- NewsletterSubscribers Collection erstellt (DSGVO-konform)
|
||||||
- 5 neue Blocks für Pages implementiert
|
- 5 neue Blocks für Pages implementiert
|
||||||
- Multi-Tenant Integration für alle Collections
|
- Multi-Tenant Integration für alle Collections
|
||||||
- Migration `20251130_135459` erstellt und angewendet
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Letzte Aktualisierung: 18. Dezember 2025*
|
||||||
|
|
|
||||||
|
|
@ -100,20 +100,15 @@ export default buildConfig({
|
||||||
components: {
|
components: {
|
||||||
// Tenant-Kontext in der Admin-Header-Leiste anzeigen
|
// Tenant-Kontext in der Admin-Header-Leiste anzeigen
|
||||||
afterNavLinks: ['@/components/admin/TenantBreadcrumb#TenantBreadcrumb'],
|
afterNavLinks: ['@/components/admin/TenantBreadcrumb#TenantBreadcrumb'],
|
||||||
// Custom Views
|
// Custom Views disabled due to bug - see https://github.com/payloadcms/payload/issues/XXXX
|
||||||
views: {
|
// TypeError: Missing parameter name at 5 (path-to-regexp error)
|
||||||
// Tenant Self-Service Dashboard
|
// views: {
|
||||||
tenantDashboard: {
|
// TenantDashboard: {
|
||||||
Component: '@/components/admin/TenantDashboardView#TenantDashboardView',
|
// Component: '@/components/admin/TenantDashboardView#TenantDashboardView',
|
||||||
path: '/tenant-dashboard',
|
// path: '/tenant-dashboard',
|
||||||
meta: {
|
// },
|
||||||
title: 'Tenant Dashboard',
|
// },
|
||||||
description: 'E-Mail-Statistiken und Übersicht für Ihren Tenant',
|
// beforeNavLinks: ['@/components/admin/DashboardNavLink#DashboardNavLink'],
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// Navigation um Dashboard-Link zu ergänzen
|
|
||||||
beforeNavLinks: ['@/components/admin/DashboardNavLink#DashboardNavLink'],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// Multi-Tenant Email Adapter
|
// Multi-Tenant Email Adapter
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue