feat(BlogWoman): add Favorites, Series collections and content blocks

Add new collections and blocks for BlogWoman affiliate and video content:

Collections:
- Favorites: Affiliate products with categories, badges, and price ranges
- Series: YouTube series with custom branding (logo, colors)

Blocks:
- FavoritesBlock: Grid/list/carousel display for affiliate products
- SeriesBlock: Series overview with filtering
- SeriesDetailBlock: Single series page with hero
- VideoEmbedBlock: YouTube/Vimeo embed with privacy mode
- FeaturedContentBlock: Curated mixed-content collections

Also includes documentation updates for deployment and API guides.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Martin Porwoll 2026-01-08 14:57:58 +00:00
parent 68032a4bf8
commit 3ccb8bd585
19 changed files with 1816 additions and 150 deletions

147
CLAUDE.md
View file

@ -704,12 +704,14 @@ SELECT * FROM audit_logs ORDER BY created_at DESC LIMIT 10;
\dt -- Alle Tabellen
```
## Blocks Übersicht
## Blocks Übersicht (42 Blocks)
### Core Blocks
| Block | Slug | Beschreibung |
|-------|------|--------------|
| HeroBlock | hero-block | Einzelner Hero mit Bild, Headline, CTA |
| HeroSliderBlock | hero-slider-block | Hero-Slider mit mehreren Slides |
| ImageSliderBlock | image-slider-block | Bildergalerie/Karussell |
| TextBlock | text-block | Textinhalt mit Rich-Text |
| ImageTextBlock | image-text-block | Bild + Text nebeneinander |
| CardGridBlock | card-grid-block | Karten-Raster |
@ -719,6 +721,10 @@ SELECT * FROM audit_logs ORDER BY created_at DESC LIMIT 10;
| TimelineBlock | timeline-block | Timeline-Darstellung |
| DividerBlock | divider-block | Trennlinie |
| VideoBlock | video-block | Video einbetten |
### Content Blocks
| Block | Slug | Beschreibung |
|-------|------|--------------|
| PostsListBlock | posts-list-block | Beitrags-Liste |
| TestimonialsBlock | testimonials-block | Kundenbewertungen |
| NewsletterBlock | newsletter-block | Newsletter-Anmeldung |
@ -726,8 +732,54 @@ SELECT * FROM audit_logs ORDER BY created_at DESC LIMIT 10;
| FAQBlock | faq-block | FAQ-Akkordeon |
| TeamBlock | team-block | Team-Mitglieder |
| ServicesBlock | services-block | Leistungen |
### Blogging Blocks
| Block | Slug | Beschreibung |
|-------|------|--------------|
| AuthorBioBlock | author-bio-block | Autoren-Biografie |
| RelatedPostsBlock | related-posts-block | Verwandte Beiträge |
| ShareButtonsBlock | share-buttons-block | Social Share Buttons |
| TableOfContentsBlock | table-of-contents-block | Inhaltsverzeichnis |
### Team Blocks
| Block | Slug | Beschreibung |
|-------|------|--------------|
| TeamFilterBlock | team-filter-block | Team mit Filter-Funktion |
| OrgChartBlock | org-chart-block | Organigramm |
### Feature Blocks
| Block | Slug | Beschreibung |
|-------|------|--------------|
| LocationsBlock | locations-block | Standorte/Filialen |
| LogoGridBlock | logo-grid-block | Partner/Kunden-Logos |
| StatsBlock | stats-block | Statistiken/Zahlen |
| JobsBlock | jobs-block | Stellenangebote |
| DownloadsBlock | downloads-block | Download-Bereich |
| MapBlock | map-block | Karten-Einbindung |
### Interactive Blocks
| Block | Slug | Beschreibung |
|-------|------|--------------|
| EventsBlock | events-block | Veranstaltungen |
| PricingBlock | pricing-block | Preistabellen |
| TabsBlock | tabs-block | Tab-Navigation |
| AccordionBlock | accordion-block | Akkordeon/Aufklappbar |
| ComparisonBlock | comparison-block | Vergleichstabelle |
### Tenant-specific Blocks
| Block | Slug | Beschreibung |
|-------|------|--------------|
| BeforeAfterBlock | before-after-block | Vorher/Nachher Bildvergleich (porwoll.de) |
### BlogWoman Blocks
| Block | Slug | Beschreibung |
|-------|------|--------------|
| FavoritesBlock | favorites-block | Affiliate-Produkte Grid/Liste/Karussell |
| SeriesBlock | series-block | YouTube-Serien Übersicht |
| SeriesDetailBlock | series-detail-block | Serien-Einzelseite mit Hero |
| VideoEmbedBlock | video-embed-block | YouTube/Vimeo Embed mit Privacy Mode |
| FeaturedContentBlock | featured-content-block | Kuratierte Mixed-Content Sammlung |
### HeroSliderBlock Features
Vollwertiger Hero-Slider mit:
@ -761,39 +813,95 @@ Vollwertiger Hero-Slider mit:
- Separate Mobile-Höhe
- Content-Breite
## Collections Übersicht
## Collections Übersicht (40+ Collections)
### Core Collections
| Collection | Slug | Beschreibung |
|------------|------|--------------|
| Users | users | Benutzer mit isSuperAdmin Flag |
| Tenants | tenants | Mandanten mit E-Mail-Konfiguration |
| Media | media | Medien mit 11 responsive Image Sizes |
| Pages | pages | Seiten mit Blocks |
### Content Collections
| Collection | Slug | Beschreibung |
|------------|------|--------------|
| Posts | posts | Blog/News/Presse mit Kategorien |
| Categories | categories | Kategorien für Posts |
| Portfolios | portfolios | Portfolio-Galerien (Fotografie) |
| PortfolioCategories | portfolio-categories | Kategorien für Portfolios |
| Tags | tags | Tags für Posts (Blogging) |
| Authors | authors | Autoren für Posts |
| Testimonials | testimonials | Kundenbewertungen |
| FAQs | faqs | Häufig gestellte Fragen (FAQ) |
| SocialLinks | social-links | Social Media Links |
### Team & Services
| Collection | Slug | Beschreibung |
|------------|------|--------------|
| Team | team | Team-Mitglieder und Mitarbeiter |
| ServiceCategories | service-categories | Kategorien für Leistungen |
| Services | services | Leistungen und Dienstleistungen |
| NewsletterSubscribers | newsletter-subscribers | Newsletter mit Double Opt-In |
| SocialLinks | social-links | Social Media Links |
| Forms | forms | Formular-Builder |
| FormSubmissions | form-submissions | Formular-Einsendungen mit Status-Workflow |
| EmailLogs | email-logs | E-Mail-Protokollierung |
| AuditLogs | audit-logs | Security Audit Trail |
| CookieConfigurations | cookie-configurations | Cookie-Banner Konfiguration |
| CookieInventory | cookie-inventory | Cookie-Inventar |
| ConsentLogs | consent-logs | Consent-Protokollierung |
| Jobs | jobs | Stellenangebote |
### Portfolio & Media
| Collection | Slug | Beschreibung |
|------------|------|--------------|
| Portfolios | portfolios | Portfolio-Galerien (Fotografie) |
| PortfolioCategories | portfolio-categories | Kategorien für Portfolios |
| Videos | videos | Video-Bibliothek mit YouTube/Vimeo/Uploads |
| VideoCategories | video-categories | Kategorien für Videos |
### Products (E-Commerce)
| Collection | Slug | Beschreibung |
|------------|------|--------------|
| Products | products | Produkte |
| ProductCategories | product-categories | Produkt-Kategorien |
### Feature Collections
| Collection | Slug | Beschreibung |
|------------|------|--------------|
| Locations | locations | Standorte/Filialen |
| Partners | partners | Partner/Kunden |
| Downloads | downloads | Download-Dateien |
| Events | events | Veranstaltungen |
| Timelines | timelines | Chronologische Events (Geschichte, Meilensteine) |
| Workflows | workflows | Komplexe Prozesse mit Phasen und Schritten |
### Formulare & Newsletter
| Collection | Slug | Beschreibung |
|------------|------|--------------|
| Forms | forms | Formular-Builder (Plugin) |
| FormSubmissions | form-submissions | Formular-Einsendungen mit CRM-Workflow |
| NewsletterSubscribers | newsletter-subscribers | Newsletter mit Double Opt-In |
### Consent & Privacy (DSGVO)
| Collection | Slug | Beschreibung |
|------------|------|--------------|
| CookieConfigurations | cookie-configurations | Cookie-Banner Konfiguration |
| CookieInventory | cookie-inventory | Cookie-Inventar |
| ConsentLogs | consent-logs | Consent-Protokollierung (WORM) |
| PrivacyPolicySettings | privacy-policy-settings | Datenschutz-Einstellungen |
### Tenant-specific Collections
| Collection | Slug | Beschreibung |
|------------|------|--------------|
| Bookings | bookings | Fotografie-Buchungen (porwoll.de) |
| Certifications | certifications | Zertifizierungen (C2S) |
| Projects | projects | Game-Development-Projekte (gunshin.de) |
| Videos | videos | Video-Bibliothek mit YouTube/Vimeo/Uploads |
| VideoCategories | video-categories | Kategorien für Videos |
### BlogWoman Collections
| Collection | Slug | Beschreibung |
|------------|------|--------------|
| Favorites | favorites | Affiliate-Produkte mit Kategorien und Badges |
| Series | series | YouTube-Serien mit Branding (Logo, Farben) |
### System Collections
| Collection | Slug | Beschreibung |
|------------|------|--------------|
| EmailLogs | email-logs | E-Mail-Protokollierung |
| AuditLogs | audit-logs | Security Audit Trail |
| SiteSettings | site-settings | Website-Einstellungen (pro Tenant) |
| Navigations | navigations | Navigationsmenüs (pro Tenant) |
| Redirects | redirects | URL-Weiterleitungen (Plugin) |
## Timeline Collection
@ -930,12 +1038,11 @@ Die FormSubmissions Collection wurde zu einem leichtgewichtigen CRM erweitert:
## Globals
> **Hinweis:** SiteSettings, Navigations und PrivacyPolicySettings wurden zu tenant-spezifischen Collections umgewandelt (siehe Collections Übersicht).
| Global | Slug | Beschreibung |
|--------|------|--------------|
| SiteSettings | site-settings | Allgemeine Website-Einstellungen |
| Navigation | navigation | Navigationsmenü |
| SEOSettings | seo-settings | SEO-Einstellungen |
| PrivacyPolicySettings | privacy-policy-settings | Datenschutz-Einstellungen |
| SEOSettings | seo-settings | Globale SEO-Einstellungen (systemweit) |
## Test Suite
@ -1070,4 +1177,4 @@ ssh payload@162.55.85.18
### Scripts & Backup
- `scripts/backup/README.md` - Backup-System Dokumentation
*Letzte Aktualisierung: 27.12.2025*
*Letzte Aktualisierung: 29.12.2025*

View file

@ -1,6 +1,8 @@
# Deployment Guide - Payload CMS Multi-Tenant
*Letzte Aktualisierung: 18. Dezember 2025*
*Letzte Aktualisierung: 27. Dezember 2025*
> **Wichtig:** Für die vollständige Deployment-Strategie siehe [DEPLOYMENT_STRATEGY.md](./DEPLOYMENT_STRATEGY.md)
## Übersicht
@ -94,61 +96,59 @@ pm2 logs payload --lines 20
## Production Deployment (main → cms.c2sgmbh.de)
### Schritt 1: Merge zu main
### Option A: Via GitHub Actions (Empfohlen)
1. **develop in main mergen und pushen**
```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
2. **GitHub Actions Workflow starten**
- Gehe zu: Actions → "Deploy to Production" → "Run workflow"
- Oder via CLI:
```bash
gh workflow run deploy-production.yml
```
3. **Der Workflow führt automatisch aus:**
- Pre-flight Checks
- Tests (optional)
- Datenbank-Backup
- Deployment
- Health Check
- Bei Fehler: Automatischer Rollback
### Option B: Via Deploy-Script auf Server
```bash
# SSH zum Production-Server
ssh payload@162.55.85.18
# Deploy-Script ausführen
~/deploy.sh
# Deploy-Script ausführen (mit Backup, Health Check, Rollback-Fähigkeit)
cd ~/payload-cms
./scripts/deploy-production.sh
# Optionen:
./scripts/deploy-production.sh -y # Ohne Bestätigung
./scripts/deploy-production.sh --skip-backup # Ohne Backup (nicht empfohlen)
./scripts/deploy-production.sh --skip-build # Nur Service-Neustart
./scripts/deploy-production.sh --rollback # Rollback zur vorherigen Version
./scripts/deploy-production.sh --dry-run # Zeigt was passieren würde
```
### Deploy-Script (~/deploy.sh)
### Rollback bei Problemen
```bash
#!/bin/bash
set -e
# Automatischer Rollback zur vorherigen Version
./scripts/deploy-production.sh --rollback
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
# Oder manuell zu spezifischem Commit
git log --oneline -10
git reset --hard <commit-sha>
pnpm install --frozen-lockfile
pnpm build
pm2 restart payload
```
@ -396,15 +396,17 @@ TRUST_PROXY=true
| Workflow | Trigger | Aktion |
|----------|---------|--------|
| `ci.yml` | Push/PR auf main, develop | Lint, Test, Build |
| `ci.yml` | Push/PR auf main, develop | Lint, Test, Build, E2E |
| `security.yml` | Push/PR, Schedule | Security Scanning |
| `deploy-staging.yml` | Push auf develop | Auto-Deploy zu Staging |
| `deploy-production.yml` | Manuell (workflow_dispatch) | Production Deployment |
### Secrets (GitHub)
| Secret | Beschreibung |
|--------|--------------|
| `STAGING_SSH_KEY` | SSH Private Key für sv-payload |
| `PRODUCTION_SSH_KEY` | SSH Private Key für Hetzner 3 |
### Manuelles Deployment triggern
@ -522,4 +524,4 @@ pnpm payload migrate --name 20251216_073000_add_video_collections
---
*Dokumentation: Complex Care Solutions GmbH | 18.12.2025*
*Dokumentation: Complex Care Solutions GmbH | 29.12.2025*

View file

@ -1,6 +1,8 @@
# Deployment-Strategie: Dev → Production
*Erstellt: 27. Dezember 2025*
*Erstellt: 27. Dezember 2025 | Aktualisiert: 29. Dezember 2025*
> **Siehe auch:** [STAGING-DEPLOYMENT.md](./STAGING-DEPLOYMENT.md) für detaillierte Staging-Workflow-Dokumentation
## Zusammenfassung

View file

@ -1,6 +1,6 @@
# Infrastruktur Dokumentation
*Letzte Aktualisierung: 18. Dezember 2025*
*Letzte Aktualisierung: 29. Dezember 2025*
## Gesamtübersicht
@ -84,10 +84,10 @@
### 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
- Next.js 15.5.9
- Claude Code (aktuell)
- Codex CLI (aktuell)
- Gemini CLI (aktuell)
### Projekte & Ports
@ -239,4 +239,4 @@ systemctl start frontend-porwoll
---
*Dokumentation: Martin Porwoll | Complex Care Solutions GmbH | 18.12.2025*
*Dokumentation: Martin Porwoll | Complex Care Solutions GmbH | 29.12.2025*

View file

@ -1,6 +1,6 @@
# Projekt Status - Dezember 2025
**Stand:** 27. Dezember 2025
**Stand:** 29. Dezember 2025
## Zusammenfassung
@ -177,6 +177,15 @@ pm2 logs payload
## 📝 Änderungsprotokoll
### 29.12.2025
- **Dokumentation konsolidiert und aktualisiert:**
- CLAUDE.md: Collections (40+) und Blocks (37) vollständig dokumentiert
- CLAUDE.md: Neue Collections (Products, Tags, Authors, Locations, Partners, Jobs, Downloads, Events)
- CLAUDE.md: Neue Blocks (ImageSlider, Blogging, Team-Filter, Feature-Blocks, Interactive-Blocks)
- CLAUDE.md: Globals-Abschnitt korrigiert (SiteSettings/Navigations sind jetzt Collections)
- docs/INFRASTRUCTURE.md, docs/DEPLOYMENT.md aktualisiert
- docs/anleitungen/*.md auf aktuelle URLs korrigiert
### 27.12.2025
- Payload CMS Update 3.68.4 → 3.69.0
- Bug-Fixes Admin Panel:

View file

@ -1,9 +1,11 @@
# Staging Deployment
> **Staging URL:** https://pl.c2sgmbh.de
> **Staging URL:** https://pl.porwoll.tech
> **Server:** sv-payload (37.24.237.181)
> **Branch:** `develop`
> **Siehe auch:** [DEPLOYMENT_STRATEGY.md](./DEPLOYMENT_STRATEGY.md) für die vollständige Deployment-Strategie (Dev → Prod)
---
## Übersicht
@ -14,7 +16,7 @@
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────────────┐│
│ │ Developer │ │ GitHub │ │ Staging Server ││
│ │ │ │ Actions │ │ pl.c2sgmbh.de ││
│ │ │ │ Actions │ │ pl.porwoll.tech ││
│ └──────┬───────┘ └──────┬───────┘ └──────────────┬───────────────┘│
│ │ │ │ │
│ │ git push │ │ │
@ -80,7 +82,7 @@ Jobs: deploy
### 3. Verify Deployment
- HTTP-Status von https://pl.c2sgmbh.de/admin prüfen
- HTTP-Status von https://pl.porwoll.tech/admin prüfen
- Bei Fehler: Benachrichtigung im Workflow-Summary
---
@ -233,7 +235,7 @@ main (Produktion)
| Branch | Deployment | URL |
|--------|------------|-----|
| `main` | Produktion (manuell) | cms.c2sgmbh.de |
| `develop` | Staging (automatisch) | pl.c2sgmbh.de |
| `develop` | Staging (automatisch) | pl.porwoll.tech |
---
@ -249,4 +251,4 @@ gh run list --workflow=deploy-staging.yml --limit=5
---
*Letzte Aktualisierung: 14.12.2025*
*Letzte Aktualisierung: 29.12.2025*

View file

@ -4,7 +4,7 @@
Das Payload CMS stellt eine REST-API und eine GraphQL-API bereit. Diese Anleitung beschreibt die Nutzung der REST-API für alle Collections.
**Base URL:** `https://pl.c2sgmbh.de/api`
**Base URL:** `https://pl.porwoll.tech/api`
---
@ -13,7 +13,7 @@ Das Payload CMS stellt eine REST-API und eine GraphQL-API bereit. Diese Anleitun
### Login
```bash
curl -X POST "https://pl.c2sgmbh.de/api/users/login" \
curl -X POST "https://pl.porwoll.tech/api/users/login" \
-H "Content-Type: application/json" \
-d '{
"email": "admin@example.com",
@ -38,7 +38,7 @@ curl -X POST "https://pl.c2sgmbh.de/api/users/login" \
### Token verwenden
```bash
curl "https://pl.c2sgmbh.de/api/posts" \
curl "https://pl.porwoll.tech/api/posts" \
-H "Authorization: JWT eyJhbGciOiJIUzI1NiIs..."
```
@ -50,13 +50,13 @@ Das CMS unterstützt Deutsch (de) und Englisch (en). Lokalisierte Felder können
```bash
# Deutsche Inhalte (Standard)
curl "https://pl.c2sgmbh.de/api/posts?locale=de"
curl "https://pl.porwoll.tech/api/posts?locale=de"
# Englische Inhalte
curl "https://pl.c2sgmbh.de/api/posts?locale=en"
curl "https://pl.porwoll.tech/api/posts?locale=en"
# Alle Sprachen gleichzeitig
curl "https://pl.c2sgmbh.de/api/posts?locale=all"
curl "https://pl.porwoll.tech/api/posts?locale=all"
```
---
@ -66,7 +66,7 @@ curl "https://pl.c2sgmbh.de/api/posts?locale=all"
### Aktuellen User abrufen
```bash
curl "https://pl.c2sgmbh.de/api/users/me" \
curl "https://pl.porwoll.tech/api/users/me" \
-H "Authorization: JWT your-token"
```
@ -85,14 +85,14 @@ curl "https://pl.c2sgmbh.de/api/users/me" \
### Alle Tenants abrufen (Auth + SuperAdmin erforderlich)
```bash
curl "https://pl.c2sgmbh.de/api/tenants" \
curl "https://pl.porwoll.tech/api/tenants" \
-H "Authorization: JWT your-token"
```
### Tenant erstellen (Auth + SuperAdmin erforderlich)
```bash
curl -X POST "https://pl.c2sgmbh.de/api/tenants" \
curl -X POST "https://pl.porwoll.tech/api/tenants" \
-H "Authorization: JWT your-token" \
-H "Content-Type: application/json" \
-d '{
@ -121,47 +121,47 @@ curl -X POST "https://pl.c2sgmbh.de/api/tenants" \
```bash
# Alle Posts
curl "https://pl.c2sgmbh.de/api/posts"
curl "https://pl.porwoll.tech/api/posts"
# Nur Blog-Artikel
curl "https://pl.c2sgmbh.de/api/posts?where[type][equals]=blog"
curl "https://pl.porwoll.tech/api/posts?where[type][equals]=blog"
# Nur News
curl "https://pl.c2sgmbh.de/api/posts?where[type][equals]=news"
curl "https://pl.porwoll.tech/api/posts?where[type][equals]=news"
# Nur veröffentlichte Posts
curl "https://pl.c2sgmbh.de/api/posts?where[status][equals]=published"
curl "https://pl.porwoll.tech/api/posts?where[status][equals]=published"
# Nur hervorgehobene Posts
curl "https://pl.c2sgmbh.de/api/posts?where[isFeatured][equals]=true"
curl "https://pl.porwoll.tech/api/posts?where[isFeatured][equals]=true"
# Mit Sortierung (neueste zuerst)
curl "https://pl.c2sgmbh.de/api/posts?sort=-publishedAt"
curl "https://pl.porwoll.tech/api/posts?sort=-publishedAt"
# Limitiert auf 10 Einträge
curl "https://pl.c2sgmbh.de/api/posts?limit=10"
curl "https://pl.porwoll.tech/api/posts?limit=10"
# Pagination (Seite 2)
curl "https://pl.c2sgmbh.de/api/posts?limit=10&page=2"
curl "https://pl.porwoll.tech/api/posts?limit=10&page=2"
# Mit Locale
curl "https://pl.c2sgmbh.de/api/posts?locale=de"
curl "https://pl.porwoll.tech/api/posts?locale=de"
```
### Einzelnen Post abrufen
```bash
# Nach ID
curl "https://pl.c2sgmbh.de/api/posts/1"
curl "https://pl.porwoll.tech/api/posts/1"
# Nach Slug (über Query)
curl "https://pl.c2sgmbh.de/api/posts?where[slug][equals]=mein-erster-artikel"
curl "https://pl.porwoll.tech/api/posts?where[slug][equals]=mein-erster-artikel"
```
### Post erstellen (Auth erforderlich)
```bash
curl -X POST "https://pl.c2sgmbh.de/api/posts" \
curl -X POST "https://pl.porwoll.tech/api/posts" \
-H "Authorization: JWT your-token" \
-H "Content-Type: application/json" \
-d '{
@ -189,7 +189,7 @@ curl -X POST "https://pl.c2sgmbh.de/api/posts" \
### Post aktualisieren (Auth erforderlich)
```bash
curl -X PATCH "https://pl.c2sgmbh.de/api/posts/1" \
curl -X PATCH "https://pl.porwoll.tech/api/posts/1" \
-H "Authorization: JWT your-token" \
-H "Content-Type: application/json" \
-d '{
@ -201,7 +201,7 @@ curl -X PATCH "https://pl.c2sgmbh.de/api/posts/1" \
### Post löschen (Auth erforderlich)
```bash
curl -X DELETE "https://pl.c2sgmbh.de/api/posts/1" \
curl -X DELETE "https://pl.porwoll.tech/api/posts/1" \
-H "Authorization: JWT your-token"
```
@ -213,22 +213,22 @@ curl -X DELETE "https://pl.c2sgmbh.de/api/posts/1" \
```bash
# Alle Testimonials
curl "https://pl.c2sgmbh.de/api/testimonials"
curl "https://pl.porwoll.tech/api/testimonials"
# Nur aktive Testimonials
curl "https://pl.c2sgmbh.de/api/testimonials?where[isActive][equals]=true"
curl "https://pl.porwoll.tech/api/testimonials?where[isActive][equals]=true"
# Sortiert nach Bewertung (beste zuerst)
curl "https://pl.c2sgmbh.de/api/testimonials?sort=-rating"
curl "https://pl.porwoll.tech/api/testimonials?sort=-rating"
# Sortiert nach eigener Reihenfolge
curl "https://pl.c2sgmbh.de/api/testimonials?sort=order"
curl "https://pl.porwoll.tech/api/testimonials?sort=order"
```
### Testimonial erstellen (Auth erforderlich)
```bash
curl -X POST "https://pl.c2sgmbh.de/api/testimonials" \
curl -X POST "https://pl.porwoll.tech/api/testimonials" \
-H "Authorization: JWT your-token" \
-H "Content-Type: application/json" \
-d '{
@ -252,7 +252,7 @@ curl -X POST "https://pl.c2sgmbh.de/api/testimonials" \
```bash
# Einfache Anmeldung
curl -X POST "https://pl.c2sgmbh.de/api/newsletter-subscribers" \
curl -X POST "https://pl.porwoll.tech/api/newsletter-subscribers" \
-H "Content-Type: application/json" \
-d '{
"tenant": 1,
@ -261,7 +261,7 @@ curl -X POST "https://pl.c2sgmbh.de/api/newsletter-subscribers" \
}'
# Mit Namen und Interessen
curl -X POST "https://pl.c2sgmbh.de/api/newsletter-subscribers" \
curl -X POST "https://pl.porwoll.tech/api/newsletter-subscribers" \
-H "Content-Type: application/json" \
-d '{
"tenant": 1,
@ -292,15 +292,15 @@ curl -X POST "https://pl.c2sgmbh.de/api/newsletter-subscribers" \
```bash
# Alle Subscribers
curl "https://pl.c2sgmbh.de/api/newsletter-subscribers" \
curl "https://pl.porwoll.tech/api/newsletter-subscribers" \
-H "Authorization: JWT your-token"
# Nur bestätigte Subscribers
curl "https://pl.c2sgmbh.de/api/newsletter-subscribers?where[status][equals]=confirmed" \
curl "https://pl.porwoll.tech/api/newsletter-subscribers?where[status][equals]=confirmed" \
-H "Authorization: JWT your-token"
# Nach E-Mail suchen
curl "https://pl.c2sgmbh.de/api/newsletter-subscribers?where[email][equals]=kunde@example.com" \
curl "https://pl.porwoll.tech/api/newsletter-subscribers?where[email][equals]=kunde@example.com" \
-H "Authorization: JWT your-token"
```
@ -308,7 +308,7 @@ curl "https://pl.c2sgmbh.de/api/newsletter-subscribers?where[email][equals]=kund
```bash
# Über Token bestätigen
curl -X PATCH "https://pl.c2sgmbh.de/api/newsletter-subscribers/1" \
curl -X PATCH "https://pl.porwoll.tech/api/newsletter-subscribers/1" \
-H "Authorization: JWT your-token" \
-H "Content-Type: application/json" \
-d '{
@ -319,7 +319,7 @@ curl -X PATCH "https://pl.c2sgmbh.de/api/newsletter-subscribers/1" \
### Subscriber abmelden
```bash
curl -X PATCH "https://pl.c2sgmbh.de/api/newsletter-subscribers/1" \
curl -X PATCH "https://pl.porwoll.tech/api/newsletter-subscribers/1" \
-H "Authorization: JWT your-token" \
-H "Content-Type: application/json" \
-d '{
@ -335,16 +335,16 @@ curl -X PATCH "https://pl.c2sgmbh.de/api/newsletter-subscribers/1" \
```bash
# Alle Seiten
curl "https://pl.c2sgmbh.de/api/pages"
curl "https://pl.porwoll.tech/api/pages"
# Seite nach Slug
curl "https://pl.c2sgmbh.de/api/pages?where[slug][equals]=startseite"
curl "https://pl.porwoll.tech/api/pages?where[slug][equals]=startseite"
# Nur veröffentlichte Seiten
curl "https://pl.c2sgmbh.de/api/pages?where[status][equals]=published"
curl "https://pl.porwoll.tech/api/pages?where[status][equals]=published"
# Mit Locale
curl "https://pl.c2sgmbh.de/api/pages?locale=de&depth=2"
curl "https://pl.porwoll.tech/api/pages?locale=de&depth=2"
```
### Seite mit Blocks
@ -389,7 +389,7 @@ Die Cookie-Konfiguration wird automatisch nach Domain gefiltert.
```bash
# Für Frontend Cookie-Banner
curl "https://pl.c2sgmbh.de/api/cookie-configurations?where[tenant][equals]=1"
curl "https://pl.porwoll.tech/api/cookie-configurations?where[tenant][equals]=1"
```
**Response:**
@ -432,13 +432,13 @@ Dokumentation aller verwendeten Cookies für die Datenschutzerklärung.
```bash
# Alle Cookies eines Tenants
curl "https://pl.c2sgmbh.de/api/cookie-inventory?where[tenant][equals]=1"
curl "https://pl.porwoll.tech/api/cookie-inventory?where[tenant][equals]=1"
# Nur aktive Cookies
curl "https://pl.c2sgmbh.de/api/cookie-inventory?where[tenant][equals]=1&where[isActive][equals]=true"
curl "https://pl.porwoll.tech/api/cookie-inventory?where[tenant][equals]=1&where[isActive][equals]=true"
# Nach Kategorie filtern
curl "https://pl.c2sgmbh.de/api/cookie-inventory?where[tenant][equals]=1&where[category][equals]=analytics"
curl "https://pl.porwoll.tech/api/cookie-inventory?where[tenant][equals]=1&where[category][equals]=analytics"
```
**Response:**
@ -473,7 +473,7 @@ curl "https://pl.c2sgmbh.de/api/cookie-inventory?where[tenant][equals]=1&where[c
Consent-Logs sind ein WORM (Write-Once-Read-Many) Audit-Trail für DSGVO-Nachweise.
```bash
curl -X POST "https://pl.c2sgmbh.de/api/consent-logs" \
curl -X POST "https://pl.porwoll.tech/api/consent-logs" \
-H "Content-Type: application/json" \
-H "X-API-Key: your-consent-api-key" \
-d '{
@ -531,7 +531,7 @@ curl -X POST "https://pl.c2sgmbh.de/api/consent-logs" \
Konfiguration für die Datenschutzerklärungs-Seite (z.B. Alfright Integration).
```bash
curl "https://pl.c2sgmbh.de/api/privacy-policy-settings?where[tenant][equals]=1"
curl "https://pl.porwoll.tech/api/privacy-policy-settings?where[tenant][equals]=1"
```
**Response:**
@ -693,7 +693,7 @@ curl "https://pl.c2sgmbh.de/api/privacy-policy-settings?where[tenant][equals]=1"
1. **Admin Panel:** Unter "Settings → Tenants" die ID ablesen
2. **API:**
```bash
curl "https://pl.c2sgmbh.de/api/tenants" \
curl "https://pl.porwoll.tech/api/tenants" \
-H "Authorization: JWT your-token"
```
@ -705,7 +705,7 @@ Wenn ein Frontend über eine Tenant-Domain (z.B. `porwoll.de`) zugreift, wird de
Für direkte API-Zugriffe:
```bash
curl "https://pl.c2sgmbh.de/api/posts?where[tenant][equals]=1"
curl "https://pl.porwoll.tech/api/posts?where[tenant][equals]=1"
```
### Super Admin
@ -720,7 +720,7 @@ User mit `isSuperAdmin: true` haben Zugriff auf alle Tenants und können neue Te
```typescript
// lib/api.ts
const API_BASE = 'https://pl.c2sgmbh.de/api'
const API_BASE = 'https://pl.porwoll.tech/api'
const TENANT_ID = 1
export async function getPosts(type?: string, limit = 10, locale = 'de') {
@ -889,6 +889,6 @@ Aktuell gibt es kein Rate Limiting. Für Production-Umgebungen sollte ein Revers
## Weitere Ressourcen
- **Admin Panel:** https://pl.c2sgmbh.de/admin
- **Admin Panel:** https://pl.porwoll.tech/admin
- **Payload CMS Docs:** https://payloadcms.com/docs
- **GraphQL Playground:** https://pl.c2sgmbh.de/api/graphql (wenn aktiviert)
- **GraphQL Playground:** https://pl.porwoll.tech/api/graphql (wenn aktiviert)

View file

@ -1,6 +1,6 @@
# Security-Richtlinien - Payload CMS Multi-Tenant
> Letzte Aktualisierung: 18.12.2025
> Letzte Aktualisierung: 29.12.2025
## Übersicht
@ -323,29 +323,13 @@ email=admin@example.com&password=secret
- 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
---
## Änderungshistorie
| Datum | Änderung |
|-------|----------|
| 18.12.2025 | **Custom Admin Login Page:** Styled Login-Formular, Browser-Redirect mit Safe-URL-Validierung, Open Redirect Prevention |
| 29.12.2025 | **Dokumentation aktualisiert:** Custom Login Page Abschnitt entfernt (wurde am 27.12.2025 entfernt) |
| 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 |
| 08.12.2025 | Security Test Suite (143 Tests) |
@ -362,8 +346,7 @@ src/app/(payload)/admin/login/
| `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 |
| `src/app/(payload)/api/users/login/route.ts` | Custom Login API mit Audit |
| `scripts/detect-secrets.sh` | Pre-Commit Secret Detection |
| `.github/workflows/security.yml` | CI Security Scanning |
| `tests/unit/security/` | Security Unit Tests |

View file

@ -222,13 +222,31 @@
---
*Letzte Aktualisierung: 27.12.2025*
*Letzte Aktualisierung: 29.12.2025*
---
## Changelog
### 29.12.2025
- **Dokumentation konsolidiert und aktualisiert:**
- CLAUDE.md: Collections (40+) und Blocks (37) vollständig dokumentiert
- CLAUDE.md: Neue Collections hinzugefügt (Products, Tags, Authors, Locations, Partners, Jobs, Downloads, Events)
- CLAUDE.md: Neue Blocks hinzugefügt (ImageSlider, Blogging, Team-Filter, Feature, Interactive Blocks)
- CLAUDE.md: Globals-Abschnitt korrigiert (SiteSettings/Navigations sind jetzt Collections)
- docs/INFRASTRUCTURE.md aktualisiert
- docs/DEPLOYMENT.md aktualisiert
- docs/STAGING-DEPLOYMENT.md: URLs korrigiert (pl.porwoll.tech)
- docs/anleitungen/API_ANLEITUNG.md: URLs korrigiert (pl.porwoll.tech)
- docs/anleitungen/SECURITY.md: Custom Login Page Abschnitt entfernt
### 27.12.2025
- **Production Deployment-Strategie implementiert:**
- GitHub Actions Workflow `deploy-production.yml` für manuelles Production-Deployment
- Deploy-Script `scripts/deploy-production.sh` mit Backup, Health Check, Rollback
- Umfassende Dokumentation in `docs/DEPLOYMENT_STRATEGY.md`
- Features: Pre-flight Checks, DB-Backup, automatischer Rollback bei Fehler
- Neues GitHub Secret erforderlich: `PRODUCTION_SSH_KEY`
- **Payload CMS Update 3.68.4 → 3.69.0:**
- Login Redirect Loop behoben (formatAdminURL generiert keine absoluten URLs mehr)
- Update aller @payloadcms/* Pakete

View file

@ -0,0 +1,182 @@
import type { Block } from 'payload'
/**
* Background color options (shared with other blocks)
*/
const backgroundColorOptions = [
{ label: 'Weiß', value: 'white' },
{ label: 'Ivory', value: 'ivory' },
{ label: 'Sand', value: 'sand' },
{ label: 'Hell (Grau)', value: 'light' },
{ label: 'Dunkel', value: 'dark' },
]
/**
* Category options matching the Favorites collection
*/
const categoryFilterOptions = [
{ label: 'Alle Kategorien', value: 'all' },
{ label: 'Fashion', value: 'fashion' },
{ label: 'Beauty', value: 'beauty' },
{ label: 'Travel', value: 'travel' },
{ label: 'Tech', value: 'tech' },
{ label: 'Home', value: 'home' },
]
/**
* FavoritesBlock
*
* Zeigt Favoriten/Affiliate-Produkte aus der Favorites Collection.
* Unterstützt verschiedene Layouts und Filteroptionen.
*/
export const FavoritesBlock: Block = {
slug: 'favorites-block',
labels: {
singular: 'Favoriten',
plural: 'Favoriten',
},
imageURL: '/assets/blocks/favorites.png',
fields: [
// Header
{
name: 'title',
type: 'text',
label: 'Überschrift',
localized: true,
},
{
name: 'subtitle',
type: 'text',
label: 'Untertitel',
localized: true,
},
// Filtering
{
name: 'category',
type: 'select',
defaultValue: 'all',
options: categoryFilterOptions,
label: 'Kategorie-Filter',
admin: {
description: 'Nur Favoriten dieser Kategorie anzeigen',
},
},
{
name: 'showFeaturedOnly',
type: 'checkbox',
defaultValue: false,
label: 'Nur Featured anzeigen',
admin: {
description: 'Nur als "Featured" markierte Favoriten anzeigen',
},
},
{
name: 'limit',
type: 'number',
defaultValue: 8,
min: 1,
max: 50,
label: 'Anzahl',
admin: {
description: 'Maximale Anzahl der angezeigten Favoriten',
},
},
// Layout
{
name: 'layout',
type: 'select',
defaultValue: 'grid',
label: 'Layout',
options: [
{ label: 'Grid', value: 'grid' },
{ label: 'Liste', value: 'list' },
{ label: 'Karussell', value: 'carousel' },
],
},
{
name: 'columns',
type: 'select',
defaultValue: '4',
label: 'Spalten',
options: [
{ label: '2 Spalten', value: '2' },
{ label: '3 Spalten', value: '3' },
{ label: '4 Spalten', value: '4' },
],
admin: {
condition: (_, siblingData) =>
siblingData?.layout === 'grid' || siblingData?.layout === 'carousel',
},
},
// Display Options
{
name: 'showPrice',
type: 'checkbox',
defaultValue: true,
label: 'Preis anzeigen',
},
{
name: 'showBadge',
type: 'checkbox',
defaultValue: true,
label: 'Badge anzeigen',
},
{
name: 'showDescription',
type: 'checkbox',
defaultValue: false,
label: 'Beschreibung anzeigen',
},
{
name: 'showCategory',
type: 'checkbox',
defaultValue: false,
label: 'Kategorie anzeigen',
},
// Styling
{
name: 'backgroundColor',
type: 'select',
defaultValue: 'white',
options: backgroundColorOptions,
label: 'Hintergrundfarbe',
},
// CTA
{
name: 'cta',
type: 'group',
label: 'Call-to-Action',
fields: [
{
name: 'showCta',
type: 'checkbox',
defaultValue: false,
label: 'CTA-Button anzeigen',
},
{
name: 'ctaText',
type: 'text',
label: 'Button-Text',
defaultValue: 'Alle Favoriten ansehen',
localized: true,
admin: {
condition: (_, siblingData) => siblingData?.showCta,
},
},
{
name: 'ctaUrl',
type: 'text',
label: 'Button-Link',
admin: {
condition: (_, siblingData) => siblingData?.showCta,
},
},
],
},
],
}

View file

@ -0,0 +1,342 @@
import type { Block } from 'payload'
/**
* Background color options
*/
const backgroundColorOptions = [
{ label: 'Weiß', value: 'white' },
{ label: 'Ivory', value: 'ivory' },
{ label: 'Sand', value: 'sand' },
{ label: 'Hell (Grau)', value: 'light' },
{ label: 'Dunkel', value: 'dark' },
]
/**
* Item Type options
*/
const itemTypeOptions = [
{ label: 'Blog-Beitrag', value: 'post' },
{ label: 'Video', value: 'video' },
{ label: 'Serie', value: 'series' },
{ label: 'Externer Link', value: 'external' },
]
/**
* FeaturedContentBlock
*
* Kuratierte Sammlung von verschiedenen Content-Typen.
* Ermöglicht das Mischen von Posts, Videos, Serien und externen Links.
*/
export const FeaturedContentBlock: Block = {
slug: 'featured-content-block',
labels: {
singular: 'Featured Content',
plural: 'Featured Contents',
},
imageURL: '/assets/blocks/featured-content.png',
fields: [
// Header
{
name: 'title',
type: 'text',
label: 'Überschrift',
localized: true,
},
{
name: 'subtitle',
type: 'text',
label: 'Untertitel',
localized: true,
},
// Items Array
{
name: 'items',
type: 'array',
required: true,
minRows: 1,
maxRows: 12,
label: 'Inhalte',
admin: {
description: 'Wähle verschiedene Content-Typen aus',
},
fields: [
// Item Type Selection
{
name: 'itemType',
type: 'select',
required: true,
options: itemTypeOptions,
label: 'Typ',
},
// Post Selection (condition: itemType === 'post')
{
name: 'post',
type: 'relationship',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
relationTo: 'posts' as any,
label: 'Beitrag auswählen',
admin: {
condition: (_, siblingData) => siblingData?.itemType === 'post',
},
},
// Video Selection (condition: itemType === 'video')
{
name: 'video',
type: 'relationship',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
relationTo: 'videos' as any,
label: 'Video auswählen',
admin: {
condition: (_, siblingData) => siblingData?.itemType === 'video',
},
},
// Series Selection (condition: itemType === 'series')
{
name: 'series',
type: 'relationship',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
relationTo: 'series' as any,
label: 'Serie auswählen',
admin: {
condition: (_, siblingData) => siblingData?.itemType === 'series',
},
},
// External Content (condition: itemType === 'external')
{
name: 'externalTitle',
type: 'text',
label: 'Titel',
localized: true,
admin: {
condition: (_, siblingData) => siblingData?.itemType === 'external',
},
},
{
name: 'externalUrl',
type: 'text',
label: 'URL',
admin: {
condition: (_, siblingData) => siblingData?.itemType === 'external',
},
validate: (
value: string | undefined | null,
{ siblingData }: { siblingData?: Record<string, unknown> }
) => {
if (siblingData?.itemType !== 'external') return true
if (!value) return 'URL ist erforderlich'
try {
new URL(value)
return true
} catch {
return 'Bitte eine gültige URL eingeben'
}
},
},
{
name: 'externalImage',
type: 'upload',
relationTo: 'media',
label: 'Bild',
admin: {
condition: (_, siblingData) => siblingData?.itemType === 'external',
},
},
{
name: 'externalDescription',
type: 'textarea',
label: 'Beschreibung',
maxLength: 200,
localized: true,
admin: {
condition: (_, siblingData) => siblingData?.itemType === 'external',
},
},
// Custom Label (for all types)
{
name: 'customLabel',
type: 'text',
label: 'Custom-Label',
localized: true,
admin: {
description: 'Optionales Label (z.B. "NEU", "TRENDING", "MUSS-LESEN")',
},
},
// Custom Order/Featured
{
name: 'featured',
type: 'checkbox',
defaultValue: false,
label: 'Hervorgehoben',
admin: {
description: 'In featured-grid Layout größer darstellen',
},
},
],
},
// Layout
{
name: 'layout',
type: 'select',
defaultValue: 'grid',
label: 'Layout',
options: [
{ label: 'Grid', value: 'grid' },
{ label: 'Karussell', value: 'carousel' },
{ label: 'Liste', value: 'list' },
{ label: 'Featured Grid (erstes Element größer)', value: 'featured-grid' },
],
},
{
name: 'columns',
type: 'select',
defaultValue: '3',
label: 'Spalten',
options: [
{ label: '2 Spalten', value: '2' },
{ label: '3 Spalten', value: '3' },
{ label: '4 Spalten', value: '4' },
],
admin: {
condition: (_, siblingData) =>
siblingData?.layout === 'grid' ||
siblingData?.layout === 'carousel' ||
siblingData?.layout === 'featured-grid',
},
},
// Display Options
{
name: 'showDates',
type: 'checkbox',
defaultValue: true,
label: 'Datum anzeigen',
},
{
name: 'showType',
type: 'checkbox',
defaultValue: true,
label: 'Content-Typ anzeigen',
admin: {
description: 'Icon oder Label für Post/Video/Serie anzeigen',
},
},
{
name: 'showDescription',
type: 'checkbox',
defaultValue: true,
label: 'Beschreibung anzeigen',
},
{
name: 'showCustomLabels',
type: 'checkbox',
defaultValue: true,
label: 'Custom-Labels anzeigen',
},
// Styling
{
name: 'backgroundColor',
type: 'select',
defaultValue: 'white',
options: backgroundColorOptions,
label: 'Hintergrundfarbe',
},
// Card Style
{
name: 'card',
type: 'group',
label: 'Karten-Stil',
fields: [
{
name: 'bg',
type: 'select',
defaultValue: 'white',
label: 'Karten-Hintergrund',
options: [
{ label: 'Weiß', value: 'white' },
{ label: 'Transparent', value: 'transparent' },
{ label: 'Hell', value: 'light' },
],
},
{
name: 'shadow',
type: 'checkbox',
defaultValue: true,
label: 'Schatten',
},
{
name: 'border',
type: 'checkbox',
defaultValue: false,
label: 'Rahmen',
},
{
name: 'imgRatio',
type: 'select',
defaultValue: '16:9',
label: 'Bild-Verhältnis',
options: [
{ label: '16:9', value: '16:9' },
{ label: '4:3', value: '4:3' },
{ label: '1:1 (Quadrat)', value: '1:1' },
{ label: '3:2', value: '3:2' },
],
},
{
name: 'hover',
type: 'select',
defaultValue: 'lift',
label: 'Hover-Effekt',
options: [
{ label: 'Keiner', value: 'none' },
{ label: 'Anheben', value: 'lift' },
{ label: 'Zoom (Bild)', value: 'zoom' },
{ label: 'Schatten verstärken', value: 'shadow' },
],
},
],
},
// CTA
{
name: 'cta',
type: 'group',
label: 'Call-to-Action',
fields: [
{
name: 'showCta',
type: 'checkbox',
defaultValue: false,
label: 'CTA-Button anzeigen',
},
{
name: 'ctaText',
type: 'text',
label: 'Button-Text',
defaultValue: 'Alle ansehen',
localized: true,
admin: {
condition: (_, siblingData) => siblingData?.showCta,
},
},
{
name: 'ctaUrl',
type: 'text',
label: 'Button-Link',
admin: {
condition: (_, siblingData) => siblingData?.showCta,
},
},
],
},
],
}

154
src/blocks/SeriesBlock.ts Normal file
View file

@ -0,0 +1,154 @@
import type { Block } from 'payload'
/**
* Background color options
*/
const backgroundColorOptions = [
{ label: 'Weiß', value: 'white' },
{ label: 'Ivory', value: 'ivory' },
{ label: 'Sand', value: 'sand' },
{ label: 'Hell (Grau)', value: 'light' },
{ label: 'Dunkel', value: 'dark' },
]
/**
* SeriesBlock
*
* Zeigt Serien aus der Series Collection.
* Verschiedene Layouts für Übersichtsseiten.
*/
export const SeriesBlock: Block = {
slug: 'series-block',
labels: {
singular: 'Serien-Übersicht',
plural: 'Serien-Übersichten',
},
imageURL: '/assets/blocks/series.png',
fields: [
// Header
{
name: 'title',
type: 'text',
label: 'Überschrift',
localized: true,
},
{
name: 'subtitle',
type: 'text',
label: 'Untertitel',
localized: true,
},
// Layout
{
name: 'layout',
type: 'select',
defaultValue: 'grid',
label: 'Layout',
options: [
{ label: 'Grid', value: 'grid' },
{ label: 'Liste', value: 'list' },
{ label: 'Featured (Hero + Grid)', value: 'featured' },
],
},
{
name: 'columns',
type: 'select',
defaultValue: '3',
label: 'Spalten',
options: [
{ label: '2 Spalten', value: '2' },
{ label: '3 Spalten', value: '3' },
{ label: '4 Spalten', value: '4' },
],
admin: {
condition: (_, siblingData) =>
siblingData?.layout === 'grid' || siblingData?.layout === 'featured',
},
},
// Display Options
{
name: 'showDescription',
type: 'checkbox',
defaultValue: true,
label: 'Beschreibung anzeigen',
},
{
name: 'showLogo',
type: 'checkbox',
defaultValue: true,
label: 'Logo anzeigen',
},
{
name: 'showTagline',
type: 'checkbox',
defaultValue: true,
label: 'Tagline anzeigen',
},
{
name: 'useBrandColors',
type: 'checkbox',
defaultValue: true,
label: 'Markenfarben verwenden',
admin: {
description: 'Serien-spezifische Farben für Akzente nutzen',
},
},
// Filtering
{
name: 'limit',
type: 'number',
defaultValue: 6,
min: 1,
max: 20,
label: 'Anzahl',
admin: {
description: 'Maximale Anzahl der angezeigten Serien',
},
},
// Styling
{
name: 'backgroundColor',
type: 'select',
defaultValue: 'white',
options: backgroundColorOptions,
label: 'Hintergrundfarbe',
},
// CTA
{
name: 'cta',
type: 'group',
label: 'Call-to-Action',
fields: [
{
name: 'showCta',
type: 'checkbox',
defaultValue: false,
label: 'CTA-Button anzeigen',
},
{
name: 'ctaText',
type: 'text',
label: 'Button-Text',
defaultValue: 'Alle Serien ansehen',
localized: true,
admin: {
condition: (_, siblingData) => siblingData?.showCta,
},
},
{
name: 'ctaUrl',
type: 'text',
label: 'Button-Link',
admin: {
condition: (_, siblingData) => siblingData?.showCta,
},
},
],
},
],
}

View file

@ -0,0 +1,155 @@
import type { Block } from 'payload'
/**
* SeriesDetailBlock
*
* Für Serien-Einzelseiten mit Hero und Content.
* Zeigt die ausgewählte Serie mit Hero, Beschreibung und verwandten Posts.
*/
export const SeriesDetailBlock: Block = {
slug: 'series-detail-block',
labels: {
singular: 'Serien-Detail',
plural: 'Serien-Details',
},
imageURL: '/assets/blocks/series-detail.png',
fields: [
// Series Selection
{
name: 'series',
type: 'relationship',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
relationTo: 'series' as any,
required: true,
label: 'Serie auswählen',
admin: {
description: 'Die anzuzeigende Serie',
},
},
// Display Options
{
name: 'showHero',
type: 'checkbox',
defaultValue: true,
label: 'Hero-Bereich anzeigen',
admin: {
description: 'Cover-Bild mit Logo und Tagline',
},
},
{
name: 'showDescription',
type: 'checkbox',
defaultValue: true,
label: 'Beschreibung anzeigen',
},
{
name: 'showBrandColors',
type: 'checkbox',
defaultValue: true,
label: 'Markenfarben verwenden',
admin: {
description: 'Serien-spezifische Farben für Akzente nutzen',
},
},
// Related Posts
{
name: 'showRelatedPosts',
type: 'checkbox',
defaultValue: true,
label: 'Verwandte Beiträge anzeigen',
admin: {
description: 'Posts die mit dieser Serie verknüpft sind',
},
},
{
name: 'relatedPostsLimit',
type: 'number',
defaultValue: 6,
min: 1,
max: 20,
label: 'Anzahl verwandter Beiträge',
admin: {
condition: (_, siblingData) => siblingData?.showRelatedPosts,
},
},
{
name: 'relatedPostsTitle',
type: 'text',
label: 'Titel für verwandte Beiträge',
defaultValue: 'Beiträge aus dieser Serie',
localized: true,
admin: {
condition: (_, siblingData) => siblingData?.showRelatedPosts,
},
},
// YouTube Integration
{
name: 'showYoutubePlaylist',
type: 'checkbox',
defaultValue: true,
label: 'YouTube-Playlist anzeigen',
admin: {
description: 'Embed der YouTube-Playlist falls konfiguriert',
},
},
// Layout
{
name: 'layout',
type: 'select',
defaultValue: 'full',
label: 'Layout',
options: [
{ label: 'Vollständig', value: 'full' },
{ label: 'Kompakt', value: 'compact' },
],
},
// Hero Options (only when showHero is true)
{
name: 'hero',
type: 'group',
label: 'Hero-Optionen',
admin: {
condition: (_, siblingData) => siblingData?.showHero,
},
fields: [
{
name: 'height',
type: 'select',
defaultValue: 'medium',
label: 'Hero-Höhe',
options: [
{ label: 'Klein (300px)', value: 'small' },
{ label: 'Mittel (400px)', value: 'medium' },
{ label: 'Groß (500px)', value: 'large' },
{ label: 'Vollbild', value: 'fullscreen' },
],
},
{
name: 'overlay',
type: 'checkbox',
defaultValue: true,
label: 'Overlay anzeigen',
admin: {
description: 'Dunkles Overlay für bessere Lesbarkeit',
},
},
{
name: 'textAlign',
type: 'select',
defaultValue: 'center',
label: 'Text-Ausrichtung',
options: [
{ label: 'Links', value: 'left' },
{ label: 'Mitte', value: 'center' },
{ label: 'Rechts', value: 'right' },
],
},
],
},
],
}

View file

@ -0,0 +1,294 @@
import type { Block } from 'payload'
/**
* Aspect ratio options
*/
const aspectRatioOptions = [
{ label: '16:9 (Standard)', value: '16:9' },
{ label: '4:3', value: '4:3' },
{ label: '1:1 (Quadrat)', value: '1:1' },
{ label: '9:16 (Vertikal)', value: '9:16' },
]
/**
* Max width options
*/
const maxWidthOptions = [
{ label: 'Volle Breite', value: 'full' },
{ label: 'Groß (1024px)', value: 'large' },
{ label: 'Mittel (768px)', value: 'medium' },
{ label: 'Klein (512px)', value: 'small' },
]
/**
* VideoEmbedBlock
*
* Einbettung von YouTube, Vimeo oder benutzerdefinierten Videos.
* Unterstützt Privacy Mode, Lazy Loading und verschiedene Aspect Ratios.
*
* Verwendet die Video-Utils aus src/lib/video/video-utils.ts für URL-Parsing
*/
export const VideoEmbedBlock: Block = {
slug: 'video-embed-block',
labels: {
singular: 'Video-Embed',
plural: 'Video-Embeds',
},
imageURL: '/assets/blocks/video-embed.png',
fields: [
// Title (optional)
{
name: 'title',
type: 'text',
label: 'Titel',
localized: true,
admin: {
description: 'Optionaler Titel über dem Video',
},
},
// Video Source Selection
{
name: 'videoSource',
type: 'select',
required: true,
defaultValue: 'youtube',
label: 'Video-Quelle',
options: [
{ label: 'YouTube', value: 'youtube' },
{ label: 'Vimeo', value: 'vimeo' },
{ label: 'Eigene URL', value: 'custom' },
],
},
// YouTube URL
{
name: 'youtubeUrl',
type: 'text',
label: 'YouTube-URL',
admin: {
description: 'z.B. https://www.youtube.com/watch?v=VIDEO_ID oder https://youtu.be/VIDEO_ID',
condition: (_, siblingData) => siblingData?.videoSource === 'youtube',
},
validate: (
value: string | undefined | null,
{ siblingData }: { siblingData?: Record<string, unknown> }
) => {
if (siblingData?.videoSource !== 'youtube') return true
if (!value) return 'YouTube-URL ist erforderlich'
// Check for valid YouTube URL patterns
const youtubePatterns = [
/youtube\.com\/watch\?v=/,
/youtu\.be\//,
/youtube\.com\/embed\//,
/youtube\.com\/shorts\//,
/youtube-nocookie\.com\/embed\//,
]
const isValidYouTube = youtubePatterns.some((pattern) => pattern.test(value))
if (!isValidYouTube) {
return 'Bitte eine gültige YouTube-URL eingeben'
}
return true
},
},
// Vimeo URL
{
name: 'vimeoUrl',
type: 'text',
label: 'Vimeo-URL',
admin: {
description: 'z.B. https://vimeo.com/VIDEO_ID',
condition: (_, siblingData) => siblingData?.videoSource === 'vimeo',
},
validate: (
value: string | undefined | null,
{ siblingData }: { siblingData?: Record<string, unknown> }
) => {
if (siblingData?.videoSource !== 'vimeo') return true
if (!value) return 'Vimeo-URL ist erforderlich'
// Check for valid Vimeo URL patterns
const vimeoPatterns = [/vimeo\.com\/\d+/, /player\.vimeo\.com\/video\/\d+/]
const isValidVimeo = vimeoPatterns.some((pattern) => pattern.test(value))
if (!isValidVimeo) {
return 'Bitte eine gültige Vimeo-URL eingeben'
}
return true
},
},
// Custom URL
{
name: 'customUrl',
type: 'text',
label: 'Video-URL',
admin: {
description: 'Direkte URL zu einer MP4, WebM oder anderen Video-Datei',
condition: (_, siblingData) => siblingData?.videoSource === 'custom',
},
validate: (
value: string | undefined | null,
{ siblingData }: { siblingData?: Record<string, unknown> }
) => {
if (siblingData?.videoSource !== 'custom') return true
if (!value) return 'Video-URL ist erforderlich'
try {
new URL(value)
return true
} catch {
return 'Bitte eine gültige URL eingeben'
}
},
},
// Custom Thumbnail
{
name: 'thumbnail',
type: 'upload',
relationTo: 'media',
label: 'Vorschaubild',
admin: {
description:
'Optional: Eigenes Vorschaubild. Bei YouTube wird automatisch das Thumbnail verwendet.',
},
},
// Caption
{
name: 'caption',
type: 'text',
localized: true,
label: 'Bildunterschrift',
admin: {
description: 'Optionaler Text unter dem Video',
},
},
// Privacy & Performance
{
name: 'privacyMode',
type: 'checkbox',
defaultValue: true,
label: 'Privacy Mode',
admin: {
description: 'YouTube: Verwendet youtube-nocookie.com für besseren Datenschutz',
},
},
{
name: 'lazyLoad',
type: 'checkbox',
defaultValue: true,
label: 'Lazy Loading',
admin: {
description: 'Video erst laden, wenn es im Viewport sichtbar ist',
},
},
// Display Options
{
name: 'aspectRatio',
type: 'select',
defaultValue: '16:9',
options: aspectRatioOptions,
label: 'Seitenverhältnis',
},
{
name: 'maxWidth',
type: 'select',
defaultValue: 'large',
options: maxWidthOptions,
label: 'Maximale Breite',
},
// Playback Options
{
name: 'playbackOptions',
type: 'group',
label: 'Wiedergabe-Optionen',
fields: [
{
name: 'autoplay',
type: 'checkbox',
defaultValue: false,
label: 'Autoplay',
admin: {
description: 'Automatisch abspielen (erfordert Mute für die meisten Browser)',
},
},
{
name: 'muted',
type: 'checkbox',
defaultValue: false,
label: 'Stumm',
admin: {
description: 'Video stumm abspielen',
},
},
{
name: 'loop',
type: 'checkbox',
defaultValue: false,
label: 'Endlosschleife',
},
{
name: 'showControls',
type: 'checkbox',
defaultValue: true,
label: 'Steuerung anzeigen',
},
{
name: 'startTime',
type: 'number',
min: 0,
label: 'Startzeit (Sekunden)',
admin: {
description: 'Video ab dieser Sekunde starten',
},
},
],
},
// Styling
{
name: 'style',
type: 'group',
label: 'Darstellung',
fields: [
{
name: 'alignment',
type: 'select',
defaultValue: 'center',
label: 'Ausrichtung',
options: [
{ label: 'Links', value: 'left' },
{ label: 'Mitte', value: 'center' },
{ label: 'Rechts', value: 'right' },
],
},
{
name: 'borderRadius',
type: 'select',
defaultValue: 'md',
label: 'Eckenradius',
options: [
{ label: 'Keine', value: 'none' },
{ label: 'Klein', value: 'sm' },
{ label: 'Mittel', value: 'md' },
{ label: 'Groß', value: 'lg' },
],
},
{
name: 'shadow',
type: 'checkbox',
defaultValue: true,
label: 'Schatten',
},
],
},
],
}

View file

@ -47,3 +47,10 @@ export { ComparisonBlock } from './ComparisonBlock'
// Tenant-specific Blocks
export { BeforeAfterBlock } from './BeforeAfterBlock'
// BlogWoman Blocks
export { FavoritesBlock } from './FavoritesBlock'
export { SeriesBlock } from './SeriesBlock'
export { SeriesDetailBlock } from './SeriesDetailBlock'
export { VideoEmbedBlock } from './VideoEmbedBlock'
export { FeaturedContentBlock } from './FeaturedContentBlock'

View file

@ -0,0 +1,214 @@
// src/collections/Favorites.ts
import type { CollectionConfig } from 'payload'
import { authenticatedOnly, tenantScopedPublicRead } from '../lib/tenantAccess'
/**
* Category Options for Favorites
*/
const categoryOptions = [
{ label: 'Fashion', value: 'fashion' },
{ label: 'Beauty', value: 'beauty' },
{ label: 'Travel', value: 'travel' },
{ label: 'Tech', value: 'tech' },
{ label: 'Home', value: 'home' },
]
/**
* Badge Options for Favorites
*/
const badgeOptions = [
{ label: 'Investment Piece', value: 'investment-piece' },
{ label: 'Daily Driver', value: 'daily-driver' },
{ label: 'GRFI Approved', value: 'grfi-approved' },
{ label: 'Neu', value: 'new' },
{ label: 'Bestseller', value: 'bestseller' },
]
/**
* Price Range Options for Favorites
*/
const priceRangeOptions = [
{ label: 'Budget (< €50)', value: 'budget' },
{ label: 'Mid (€50-150)', value: 'mid' },
{ label: 'Premium (€150-500)', value: 'premium' },
{ label: 'Luxury (> €500)', value: 'luxury' },
]
/**
* Affiliate Network Options
*/
const affiliateNetworkOptions = [
{ label: 'Amazon', value: 'amazon' },
{ label: 'Awin', value: 'awin' },
{ label: 'LTK (RewardStyle)', value: 'ltk' },
{ label: 'Direct', value: 'direct' },
{ label: 'Other', value: 'other' },
]
/**
* Favorites Collection
*
* Affiliate-Produkte und Favoriten für BlogWoman.de
* Tenant-scoped für Multi-Tenant-Betrieb.
*/
export const Favorites: CollectionConfig = {
slug: 'favorites',
admin: {
group: 'Content',
useAsTitle: 'title',
defaultColumns: ['title', 'category', 'featured', 'isActive'],
description: 'Affiliate-Produkte und Favoriten',
},
access: {
read: tenantScopedPublicRead,
create: authenticatedOnly,
update: authenticatedOnly,
delete: authenticatedOnly,
},
fields: [
// Main Content
{
name: 'title',
type: 'text',
required: true,
maxLength: 200,
label: 'Titel',
},
{
name: 'slug',
type: 'text',
required: true,
unique: true,
label: 'Slug',
admin: {
position: 'sidebar',
description: 'URL-freundlicher Name',
},
},
{
name: 'description',
type: 'textarea',
maxLength: 300,
label: 'Beschreibung',
admin: {
description: 'Kurze Produktbeschreibung (max. 300 Zeichen)',
},
},
// Categorization
{
name: 'category',
type: 'select',
required: true,
options: categoryOptions,
label: 'Kategorie',
},
{
name: 'subcategory',
type: 'text',
maxLength: 100,
label: 'Unterkategorie',
admin: {
description: 'z.B. "Taschen", "Hautpflege", "Laptops"',
},
},
// Pricing
{
name: 'price',
type: 'number',
min: 0,
label: 'Preis (€)',
admin: {
description: 'Aktueller Preis in Euro',
},
},
{
name: 'priceRange',
type: 'select',
options: priceRangeOptions,
label: 'Preiskategorie',
},
// Affiliate
{
name: 'affiliateUrl',
type: 'text',
required: true,
label: 'Affiliate-URL',
admin: {
description: 'Vollständiger Affiliate-Link mit Tracking',
},
validate: (value: string | undefined | null) => {
if (!value) return 'Affiliate-URL ist erforderlich'
try {
new URL(value)
return true
} catch {
return 'Bitte eine gültige URL eingeben'
}
},
},
{
name: 'affiliateNetwork',
type: 'select',
options: affiliateNetworkOptions,
label: 'Affiliate-Netzwerk',
},
// Media
{
name: 'image',
type: 'upload',
relationTo: 'media',
required: true,
label: 'Produktbild',
admin: {
description: 'Hauptbild des Produkts',
},
},
// Display Options
{
name: 'badge',
type: 'select',
options: badgeOptions,
label: 'Badge',
admin: {
description: 'Optionales Label/Abzeichen',
},
},
{
name: 'featured',
type: 'checkbox',
defaultValue: false,
label: 'Featured',
admin: {
position: 'sidebar',
description: 'In der Featured-Sektion anzeigen',
},
},
{
name: 'isActive',
type: 'checkbox',
required: true,
defaultValue: true,
label: 'Aktiv',
admin: {
position: 'sidebar',
description: 'Inaktive Favoriten werden nicht angezeigt',
},
},
{
name: 'order',
type: 'number',
defaultValue: 0,
label: 'Sortierung',
admin: {
position: 'sidebar',
description: 'Niedrigere Zahlen werden zuerst angezeigt',
},
},
],
}

View file

@ -43,6 +43,12 @@ import {
ComparisonBlock,
// Tenant-specific Blocks
BeforeAfterBlock,
// BlogWoman Blocks
FavoritesBlock,
SeriesBlock,
SeriesDetailBlock,
VideoEmbedBlock,
FeaturedContentBlock,
} from '../blocks'
import { pagesAccess } from '../lib/access'
@ -140,6 +146,12 @@ export const Pages: CollectionConfig = {
ComparisonBlock,
// Tenant-specific Blocks
BeforeAfterBlock,
// BlogWoman Blocks
FavoritesBlock,
SeriesBlock,
SeriesDetailBlock,
VideoEmbedBlock,
FeaturedContentBlock,
],
},
{

173
src/collections/Series.ts Normal file
View file

@ -0,0 +1,173 @@
// src/collections/Series.ts
import type { CollectionConfig } from 'payload'
import { authenticatedOnly, tenantScopedPublicRead } from '../lib/tenantAccess'
/**
* Series Collection
*
* YouTube-Serien mit eigenem Branding für BlogWoman.de
* Unterstützt lokalisierte Inhalte und Custom Brand Colors.
* Tenant-scoped für Multi-Tenant-Betrieb.
*/
export const Series: CollectionConfig = {
slug: 'series',
admin: {
group: 'Content',
useAsTitle: 'title',
defaultColumns: ['title', 'slug', 'brandColor', 'isActive'],
description: 'YouTube-Serien und Content-Reihen',
},
access: {
read: tenantScopedPublicRead,
create: authenticatedOnly,
update: authenticatedOnly,
delete: authenticatedOnly,
},
fields: [
// Main Content
{
name: 'title',
type: 'text',
required: true,
localized: true,
label: 'Titel',
},
{
name: 'slug',
type: 'text',
required: true,
unique: true,
label: 'Slug',
admin: {
description: 'URL-freundlicher Name (z.B. "grfi-series")',
},
},
{
name: 'tagline',
type: 'text',
maxLength: 150,
localized: true,
label: 'Tagline',
admin: {
description: 'Kurzer Slogan oder Untertitel (max. 150 Zeichen)',
},
},
{
name: 'description',
type: 'richText',
localized: true,
label: 'Beschreibung',
admin: {
description: 'Ausführliche Beschreibung der Serie',
},
},
// Branding
{
name: 'logo',
type: 'upload',
relationTo: 'media',
label: 'Logo',
admin: {
description: 'Serien-Logo (empfohlen: transparent PNG)',
},
},
{
name: 'coverImage',
type: 'upload',
relationTo: 'media',
label: 'Cover-Bild',
admin: {
description: 'Hauptbild für die Serie (16:9 empfohlen)',
},
},
{
name: 'brandColor',
type: 'text',
label: 'Markenfarbe',
admin: {
description: 'Hex-Farbcode (z.B. #B08D57)',
placeholder: '#B08D57',
},
validate: (value: string | undefined | null) => {
if (!value) return true // Optional field
const hexRegex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/
if (!hexRegex.test(value)) {
return 'Bitte einen gültigen Hex-Farbcode eingeben (z.B. #B08D57)'
}
return true
},
},
{
name: 'accentColor',
type: 'text',
label: 'Akzentfarbe',
admin: {
description: 'Sekundäre Farbe (z.B. #FFFFFF)',
placeholder: '#FFFFFF',
},
validate: (value: string | undefined | null) => {
if (!value) return true // Optional field
const hexRegex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/
if (!hexRegex.test(value)) {
return 'Bitte einen gültigen Hex-Farbcode eingeben (z.B. #FFFFFF)'
}
return true
},
},
// YouTube Integration
{
name: 'youtubePlaylistId',
type: 'text',
label: 'YouTube Playlist ID',
admin: {
description: 'Die Playlist-ID aus YouTube (z.B. PLxxxxxx)',
},
},
{
name: 'youtubePlaylistUrl',
type: 'text',
label: 'YouTube Playlist URL',
admin: {
description: 'Vollständiger Link zur YouTube-Playlist',
},
validate: (value: string | undefined | null) => {
if (!value) return true // Optional field
try {
const url = new URL(value)
if (!url.hostname.includes('youtube.com') && !url.hostname.includes('youtu.be')) {
return 'Bitte eine gültige YouTube-URL eingeben'
}
return true
} catch {
return 'Bitte eine gültige URL eingeben'
}
},
},
// Display Settings
{
name: 'order',
type: 'number',
defaultValue: 0,
label: 'Sortierung',
admin: {
position: 'sidebar',
description: 'Niedrigere Zahlen werden zuerst angezeigt',
},
},
{
name: 'isActive',
type: 'checkbox',
required: true,
defaultValue: true,
label: 'Aktiv',
admin: {
position: 'sidebar',
description: 'Inaktive Serien werden nicht angezeigt',
},
},
],
}

View file

@ -64,6 +64,10 @@ import { Bookings } from './collections/Bookings'
import { Certifications } from './collections/Certifications'
import { Projects } from './collections/Projects'
// BlogWoman Collections
import { Favorites } from './collections/Favorites'
import { Series } from './collections/Series'
// Consent Management Collections
import { CookieConfigurations } from './collections/CookieConfigurations'
import { CookieInventory } from './collections/CookieInventory'
@ -197,6 +201,9 @@ export default buildConfig({
Bookings,
Certifications,
Projects,
// BlogWoman Collections
Favorites,
Series,
// Consent Management
CookieConfigurations,
CookieInventory,
@ -266,6 +273,9 @@ export default buildConfig({
bookings: {},
certifications: {},
projects: {},
// BlogWoman Collections
favorites: {},
series: {},
// Consent Management Collections - customTenantField: true weil sie bereits ein tenant-Feld haben
'cookie-configurations': { customTenantField: true },
'cookie-inventory': { customTenantField: true },