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 \dt -- Alle Tabellen
``` ```
## Blocks Übersicht ## Blocks Übersicht (42 Blocks)
### Core Blocks
| Block | Slug | Beschreibung | | Block | Slug | Beschreibung |
|-------|------|--------------| |-------|------|--------------|
| HeroBlock | hero-block | Einzelner Hero mit Bild, Headline, CTA | | HeroBlock | hero-block | Einzelner Hero mit Bild, Headline, CTA |
| HeroSliderBlock | hero-slider-block | Hero-Slider mit mehreren Slides | | HeroSliderBlock | hero-slider-block | Hero-Slider mit mehreren Slides |
| ImageSliderBlock | image-slider-block | Bildergalerie/Karussell |
| TextBlock | text-block | Textinhalt mit Rich-Text | | TextBlock | text-block | Textinhalt mit Rich-Text |
| ImageTextBlock | image-text-block | Bild + Text nebeneinander | | ImageTextBlock | image-text-block | Bild + Text nebeneinander |
| CardGridBlock | card-grid-block | Karten-Raster | | 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 | | TimelineBlock | timeline-block | Timeline-Darstellung |
| DividerBlock | divider-block | Trennlinie | | DividerBlock | divider-block | Trennlinie |
| VideoBlock | video-block | Video einbetten | | VideoBlock | video-block | Video einbetten |
### Content Blocks
| Block | Slug | Beschreibung |
|-------|------|--------------|
| PostsListBlock | posts-list-block | Beitrags-Liste | | PostsListBlock | posts-list-block | Beitrags-Liste |
| TestimonialsBlock | testimonials-block | Kundenbewertungen | | TestimonialsBlock | testimonials-block | Kundenbewertungen |
| NewsletterBlock | newsletter-block | Newsletter-Anmeldung | | 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 | | FAQBlock | faq-block | FAQ-Akkordeon |
| TeamBlock | team-block | Team-Mitglieder | | TeamBlock | team-block | Team-Mitglieder |
| ServicesBlock | services-block | Leistungen | | 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) | | 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 ### HeroSliderBlock Features
Vollwertiger Hero-Slider mit: Vollwertiger Hero-Slider mit:
@ -761,39 +813,95 @@ Vollwertiger Hero-Slider mit:
- Separate Mobile-Höhe - Separate Mobile-Höhe
- Content-Breite - Content-Breite
## Collections Übersicht ## Collections Übersicht (40+ Collections)
### Core Collections
| Collection | Slug | Beschreibung | | Collection | Slug | Beschreibung |
|------------|------|--------------| |------------|------|--------------|
| Users | users | Benutzer mit isSuperAdmin Flag | | Users | users | Benutzer mit isSuperAdmin Flag |
| Tenants | tenants | Mandanten mit E-Mail-Konfiguration | | Tenants | tenants | Mandanten mit E-Mail-Konfiguration |
| Media | media | Medien mit 11 responsive Image Sizes | | Media | media | Medien mit 11 responsive Image Sizes |
| Pages | pages | Seiten mit Blocks | | Pages | pages | Seiten mit Blocks |
### Content Collections
| Collection | Slug | Beschreibung |
|------------|------|--------------|
| Posts | posts | Blog/News/Presse mit Kategorien | | Posts | posts | Blog/News/Presse mit Kategorien |
| Categories | categories | Kategorien für Posts | | Categories | categories | Kategorien für Posts |
| Portfolios | portfolios | Portfolio-Galerien (Fotografie) | | Tags | tags | Tags für Posts (Blogging) |
| PortfolioCategories | portfolio-categories | Kategorien für Portfolios | | Authors | authors | Autoren für Posts |
| Testimonials | testimonials | Kundenbewertungen | | Testimonials | testimonials | Kundenbewertungen |
| FAQs | faqs | Häufig gestellte Fragen (FAQ) | | FAQs | faqs | Häufig gestellte Fragen (FAQ) |
| SocialLinks | social-links | Social Media Links |
### Team & Services
| Collection | Slug | Beschreibung |
|------------|------|--------------|
| Team | team | Team-Mitglieder und Mitarbeiter | | Team | team | Team-Mitglieder und Mitarbeiter |
| ServiceCategories | service-categories | Kategorien für Leistungen | | ServiceCategories | service-categories | Kategorien für Leistungen |
| Services | services | Leistungen und Dienstleistungen | | Services | services | Leistungen und Dienstleistungen |
| NewsletterSubscribers | newsletter-subscribers | Newsletter mit Double Opt-In | | Jobs | jobs | Stellenangebote |
| SocialLinks | social-links | Social Media Links |
| Forms | forms | Formular-Builder | ### Portfolio & Media
| FormSubmissions | form-submissions | Formular-Einsendungen mit Status-Workflow | | Collection | Slug | Beschreibung |
| EmailLogs | email-logs | E-Mail-Protokollierung | |------------|------|--------------|
| AuditLogs | audit-logs | Security Audit Trail | | Portfolios | portfolios | Portfolio-Galerien (Fotografie) |
| CookieConfigurations | cookie-configurations | Cookie-Banner Konfiguration | | PortfolioCategories | portfolio-categories | Kategorien für Portfolios |
| CookieInventory | cookie-inventory | Cookie-Inventar | | Videos | videos | Video-Bibliothek mit YouTube/Vimeo/Uploads |
| ConsentLogs | consent-logs | Consent-Protokollierung | | 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) | | Timelines | timelines | Chronologische Events (Geschichte, Meilensteine) |
| Workflows | workflows | Komplexe Prozesse mit Phasen und Schritten | | 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) | | 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 | ### 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 ## Timeline Collection
@ -930,12 +1038,11 @@ Die FormSubmissions Collection wurde zu einem leichtgewichtigen CRM erweitert:
## Globals ## Globals
> **Hinweis:** SiteSettings, Navigations und PrivacyPolicySettings wurden zu tenant-spezifischen Collections umgewandelt (siehe Collections Übersicht).
| Global | Slug | Beschreibung | | Global | Slug | Beschreibung |
|--------|------|--------------| |--------|------|--------------|
| SiteSettings | site-settings | Allgemeine Website-Einstellungen | | SEOSettings | seo-settings | Globale SEO-Einstellungen (systemweit) |
| Navigation | navigation | Navigationsmenü |
| SEOSettings | seo-settings | SEO-Einstellungen |
| PrivacyPolicySettings | privacy-policy-settings | Datenschutz-Einstellungen |
## Test Suite ## Test Suite
@ -1070,4 +1177,4 @@ ssh payload@162.55.85.18
### Scripts & Backup ### Scripts & Backup
- `scripts/backup/README.md` - Backup-System Dokumentation - `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 # 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 ## Übersicht
@ -94,61 +96,59 @@ pm2 logs payload --lines 20
## Production Deployment (main → cms.c2sgmbh.de) ## 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 ```bash
# Auf dem Development-Server oder lokal
git checkout main git checkout main
git pull origin main git pull origin main
git merge develop git merge develop
git push origin main 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 ```bash
# SSH zum Production-Server # SSH zum Production-Server
ssh payload@162.55.85.18 ssh payload@162.55.85.18
# Deploy-Script ausführen # Deploy-Script ausführen (mit Backup, Health Check, Rollback-Fähigkeit)
~/deploy.sh 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 ```bash
#!/bin/bash # Automatischer Rollback zur vorherigen Version
set -e ./scripts/deploy-production.sh --rollback
cd ~/payload-cms # Oder manuell zu spezifischem Commit
git log --oneline -10
echo "📥 Pulling latest changes..." git reset --hard <commit-sha>
git pull origin main pnpm install --frozen-lockfile
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 pnpm build
pm2 restart payload pm2 restart payload
``` ```
@ -396,15 +396,17 @@ TRUST_PROXY=true
| Workflow | Trigger | Aktion | | 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 | | `security.yml` | Push/PR, Schedule | Security Scanning |
| `deploy-staging.yml` | Push auf develop | Auto-Deploy zu Staging | | `deploy-staging.yml` | Push auf develop | Auto-Deploy zu Staging |
| `deploy-production.yml` | Manuell (workflow_dispatch) | Production Deployment |
### Secrets (GitHub) ### Secrets (GitHub)
| Secret | Beschreibung | | Secret | Beschreibung |
|--------|--------------| |--------|--------------|
| `STAGING_SSH_KEY` | SSH Private Key für sv-payload | | `STAGING_SSH_KEY` | SSH Private Key für sv-payload |
| `PRODUCTION_SSH_KEY` | SSH Private Key für Hetzner 3 |
### Manuelles Deployment triggern ### 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 # 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 ## Zusammenfassung

View file

@ -1,6 +1,6 @@
# Infrastruktur Dokumentation # Infrastruktur Dokumentation
*Letzte Aktualisierung: 18. Dezember 2025* *Letzte Aktualisierung: 29. Dezember 2025*
## Gesamtübersicht ## Gesamtübersicht
@ -84,10 +84,10 @@
### Software Stack ### Software Stack
- Node.js 22.x - Node.js 22.x
- pnpm - pnpm
- Next.js 16.0.10 - Next.js 15.5.9
- Claude Code 2.0.72 - Claude Code (aktuell)
- Codex CLI 0.73.0 - Codex CLI (aktuell)
- Gemini CLI 0.21.2 - Gemini CLI (aktuell)
### Projekte & Ports ### 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 # Projekt Status - Dezember 2025
**Stand:** 27. Dezember 2025 **Stand:** 29. Dezember 2025
## Zusammenfassung ## Zusammenfassung
@ -177,6 +177,15 @@ pm2 logs payload
## 📝 Änderungsprotokoll ## 📝 Ä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 ### 27.12.2025
- Payload CMS Update 3.68.4 → 3.69.0 - Payload CMS Update 3.68.4 → 3.69.0
- Bug-Fixes Admin Panel: - Bug-Fixes Admin Panel:

View file

@ -1,9 +1,11 @@
# Staging Deployment # Staging Deployment
> **Staging URL:** https://pl.c2sgmbh.de > **Staging URL:** https://pl.porwoll.tech
> **Server:** sv-payload (37.24.237.181) > **Server:** sv-payload (37.24.237.181)
> **Branch:** `develop` > **Branch:** `develop`
> **Siehe auch:** [DEPLOYMENT_STRATEGY.md](./DEPLOYMENT_STRATEGY.md) für die vollständige Deployment-Strategie (Dev → Prod)
--- ---
## Übersicht ## Übersicht
@ -14,7 +16,7 @@
│ │ │ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────────────┐│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────────────┐│
│ │ Developer │ │ GitHub │ │ Staging Server ││ │ │ Developer │ │ GitHub │ │ Staging Server ││
│ │ │ │ Actions │ │ pl.c2sgmbh.de ││ │ │ │ │ Actions │ │ pl.porwoll.tech ││
│ └──────┬───────┘ └──────┬───────┘ └──────────────┬───────────────┘│ │ └──────┬───────┘ └──────┬───────┘ └──────────────┬───────────────┘│
│ │ │ │ │ │ │ │ │ │
│ │ git push │ │ │ │ │ git push │ │ │
@ -80,7 +82,7 @@ Jobs: deploy
### 3. Verify Deployment ### 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 - Bei Fehler: Benachrichtigung im Workflow-Summary
--- ---
@ -233,7 +235,7 @@ main (Produktion)
| Branch | Deployment | URL | | Branch | Deployment | URL |
|--------|------------|-----| |--------|------------|-----|
| `main` | Produktion (manuell) | cms.c2sgmbh.de | | `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. 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 ### Login
```bash ```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" \ -H "Content-Type: application/json" \
-d '{ -d '{
"email": "admin@example.com", "email": "admin@example.com",
@ -38,7 +38,7 @@ curl -X POST "https://pl.c2sgmbh.de/api/users/login" \
### Token verwenden ### Token verwenden
```bash ```bash
curl "https://pl.c2sgmbh.de/api/posts" \ curl "https://pl.porwoll.tech/api/posts" \
-H "Authorization: JWT eyJhbGciOiJIUzI1NiIs..." -H "Authorization: JWT eyJhbGciOiJIUzI1NiIs..."
``` ```
@ -50,13 +50,13 @@ Das CMS unterstützt Deutsch (de) und Englisch (en). Lokalisierte Felder können
```bash ```bash
# Deutsche Inhalte (Standard) # Deutsche Inhalte (Standard)
curl "https://pl.c2sgmbh.de/api/posts?locale=de" curl "https://pl.porwoll.tech/api/posts?locale=de"
# Englische Inhalte # Englische Inhalte
curl "https://pl.c2sgmbh.de/api/posts?locale=en" curl "https://pl.porwoll.tech/api/posts?locale=en"
# Alle Sprachen gleichzeitig # 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 ### Aktuellen User abrufen
```bash ```bash
curl "https://pl.c2sgmbh.de/api/users/me" \ curl "https://pl.porwoll.tech/api/users/me" \
-H "Authorization: JWT your-token" -H "Authorization: JWT your-token"
``` ```
@ -85,14 +85,14 @@ curl "https://pl.c2sgmbh.de/api/users/me" \
### Alle Tenants abrufen (Auth + SuperAdmin erforderlich) ### Alle Tenants abrufen (Auth + SuperAdmin erforderlich)
```bash ```bash
curl "https://pl.c2sgmbh.de/api/tenants" \ curl "https://pl.porwoll.tech/api/tenants" \
-H "Authorization: JWT your-token" -H "Authorization: JWT your-token"
``` ```
### Tenant erstellen (Auth + SuperAdmin erforderlich) ### Tenant erstellen (Auth + SuperAdmin erforderlich)
```bash ```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 "Authorization: JWT your-token" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{ -d '{
@ -121,47 +121,47 @@ curl -X POST "https://pl.c2sgmbh.de/api/tenants" \
```bash ```bash
# Alle Posts # Alle Posts
curl "https://pl.c2sgmbh.de/api/posts" curl "https://pl.porwoll.tech/api/posts"
# Nur Blog-Artikel # 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 # 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 # 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 # 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) # 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 # 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) # 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 # Mit Locale
curl "https://pl.c2sgmbh.de/api/posts?locale=de" curl "https://pl.porwoll.tech/api/posts?locale=de"
``` ```
### Einzelnen Post abrufen ### Einzelnen Post abrufen
```bash ```bash
# Nach ID # Nach ID
curl "https://pl.c2sgmbh.de/api/posts/1" curl "https://pl.porwoll.tech/api/posts/1"
# Nach Slug (über Query) # 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) ### Post erstellen (Auth erforderlich)
```bash ```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 "Authorization: JWT your-token" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{ -d '{
@ -189,7 +189,7 @@ curl -X POST "https://pl.c2sgmbh.de/api/posts" \
### Post aktualisieren (Auth erforderlich) ### Post aktualisieren (Auth erforderlich)
```bash ```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 "Authorization: JWT your-token" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{ -d '{
@ -201,7 +201,7 @@ curl -X PATCH "https://pl.c2sgmbh.de/api/posts/1" \
### Post löschen (Auth erforderlich) ### Post löschen (Auth erforderlich)
```bash ```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" -H "Authorization: JWT your-token"
``` ```
@ -213,22 +213,22 @@ curl -X DELETE "https://pl.c2sgmbh.de/api/posts/1" \
```bash ```bash
# Alle Testimonials # Alle Testimonials
curl "https://pl.c2sgmbh.de/api/testimonials" curl "https://pl.porwoll.tech/api/testimonials"
# Nur aktive 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) # 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 # 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) ### Testimonial erstellen (Auth erforderlich)
```bash ```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 "Authorization: JWT your-token" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{ -d '{
@ -252,7 +252,7 @@ curl -X POST "https://pl.c2sgmbh.de/api/testimonials" \
```bash ```bash
# Einfache Anmeldung # 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" \ -H "Content-Type: application/json" \
-d '{ -d '{
"tenant": 1, "tenant": 1,
@ -261,7 +261,7 @@ curl -X POST "https://pl.c2sgmbh.de/api/newsletter-subscribers" \
}' }'
# Mit Namen und Interessen # 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" \ -H "Content-Type: application/json" \
-d '{ -d '{
"tenant": 1, "tenant": 1,
@ -292,15 +292,15 @@ curl -X POST "https://pl.c2sgmbh.de/api/newsletter-subscribers" \
```bash ```bash
# Alle Subscribers # Alle Subscribers
curl "https://pl.c2sgmbh.de/api/newsletter-subscribers" \ curl "https://pl.porwoll.tech/api/newsletter-subscribers" \
-H "Authorization: JWT your-token" -H "Authorization: JWT your-token"
# Nur bestätigte Subscribers # 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" -H "Authorization: JWT your-token"
# Nach E-Mail suchen # 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" -H "Authorization: JWT your-token"
``` ```
@ -308,7 +308,7 @@ curl "https://pl.c2sgmbh.de/api/newsletter-subscribers?where[email][equals]=kund
```bash ```bash
# Über Token bestätigen # Ü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 "Authorization: JWT your-token" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{ -d '{
@ -319,7 +319,7 @@ curl -X PATCH "https://pl.c2sgmbh.de/api/newsletter-subscribers/1" \
### Subscriber abmelden ### Subscriber abmelden
```bash ```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 "Authorization: JWT your-token" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{ -d '{
@ -335,16 +335,16 @@ curl -X PATCH "https://pl.c2sgmbh.de/api/newsletter-subscribers/1" \
```bash ```bash
# Alle Seiten # Alle Seiten
curl "https://pl.c2sgmbh.de/api/pages" curl "https://pl.porwoll.tech/api/pages"
# Seite nach Slug # 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 # 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 # 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 ### Seite mit Blocks
@ -389,7 +389,7 @@ Die Cookie-Konfiguration wird automatisch nach Domain gefiltert.
```bash ```bash
# Für Frontend Cookie-Banner # 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:** **Response:**
@ -432,13 +432,13 @@ Dokumentation aller verwendeten Cookies für die Datenschutzerklärung.
```bash ```bash
# Alle Cookies eines Tenants # 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 # 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 # 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:** **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. Consent-Logs sind ein WORM (Write-Once-Read-Many) Audit-Trail für DSGVO-Nachweise.
```bash ```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 "Content-Type: application/json" \
-H "X-API-Key: your-consent-api-key" \ -H "X-API-Key: your-consent-api-key" \
-d '{ -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). Konfiguration für die Datenschutzerklärungs-Seite (z.B. Alfright Integration).
```bash ```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:** **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 1. **Admin Panel:** Unter "Settings → Tenants" die ID ablesen
2. **API:** 2. **API:**
```bash ```bash
curl "https://pl.c2sgmbh.de/api/tenants" \ curl "https://pl.porwoll.tech/api/tenants" \
-H "Authorization: JWT your-token" -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: Für direkte API-Zugriffe:
```bash ```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 ### Super Admin
@ -720,7 +720,7 @@ User mit `isSuperAdmin: true` haben Zugriff auf alle Tenants und können neue Te
```typescript ```typescript
// lib/api.ts // lib/api.ts
const API_BASE = 'https://pl.c2sgmbh.de/api' const API_BASE = 'https://pl.porwoll.tech/api'
const TENANT_ID = 1 const TENANT_ID = 1
export async function getPosts(type?: string, limit = 10, locale = 'de') { 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 ## Weitere Ressourcen
- **Admin Panel:** https://pl.c2sgmbh.de/admin - **Admin Panel:** https://pl.porwoll.tech/admin
- **Payload CMS Docs:** https://payloadcms.com/docs - **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 # Security-Richtlinien - Payload CMS Multi-Tenant
> Letzte Aktualisierung: 18.12.2025 > Letzte Aktualisierung: 29.12.2025
## Übersicht ## Übersicht
@ -323,29 +323,13 @@ email=admin@example.com&password=secret
- Rate-Limiting verhindert Brute-Force-Angriffe - Rate-Limiting verhindert Brute-Force-Angriffe
- Open Redirect Prevention durch URL-Validierung - 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 ## Änderungshistorie
| Datum | Änderung | | 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 | | 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) |
@ -362,8 +346,7 @@ src/app/(payload)/admin/login/
| `src/lib/security/ip-allowlist.ts` | IP-basierte Zugriffskontrolle | | `src/lib/security/ip-allowlist.ts` | IP-basierte Zugriffskontrolle |
| `src/lib/security/csrf.ts` | CSRF Token Generation & Validation | | `src/lib/security/csrf.ts` | CSRF Token Generation & Validation |
| `src/lib/security/data-masking.ts` | Sensitive Data Masking | | `src/lib/security/data-masking.ts` | Sensitive Data Masking |
| `src/app/(payload)/api/users/login/route.ts` | Custom Login API | | `src/app/(payload)/api/users/login/route.ts` | Custom Login API mit Audit |
| `src/app/(payload)/admin/login/page.tsx` | Custom Login Page |
| `scripts/detect-secrets.sh` | Pre-Commit Secret Detection | | `scripts/detect-secrets.sh` | Pre-Commit Secret Detection |
| `.github/workflows/security.yml` | CI Security Scanning | | `.github/workflows/security.yml` | CI Security Scanning |
| `tests/unit/security/` | Security Unit Tests | | `tests/unit/security/` | Security Unit Tests |

View file

@ -222,13 +222,31 @@
--- ---
*Letzte Aktualisierung: 27.12.2025* *Letzte Aktualisierung: 29.12.2025*
--- ---
## Changelog ## 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 ### 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:** - **Payload CMS Update 3.68.4 → 3.69.0:**
- Login Redirect Loop behoben (formatAdminURL generiert keine absoluten URLs mehr) - Login Redirect Loop behoben (formatAdminURL generiert keine absoluten URLs mehr)
- Update aller @payloadcms/* Pakete - 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 // Tenant-specific Blocks
export { BeforeAfterBlock } from './BeforeAfterBlock' 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, ComparisonBlock,
// Tenant-specific Blocks // Tenant-specific Blocks
BeforeAfterBlock, BeforeAfterBlock,
// BlogWoman Blocks
FavoritesBlock,
SeriesBlock,
SeriesDetailBlock,
VideoEmbedBlock,
FeaturedContentBlock,
} from '../blocks' } from '../blocks'
import { pagesAccess } from '../lib/access' import { pagesAccess } from '../lib/access'
@ -140,6 +146,12 @@ export const Pages: CollectionConfig = {
ComparisonBlock, ComparisonBlock,
// Tenant-specific Blocks // Tenant-specific Blocks
BeforeAfterBlock, 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 { Certifications } from './collections/Certifications'
import { Projects } from './collections/Projects' import { Projects } from './collections/Projects'
// BlogWoman Collections
import { Favorites } from './collections/Favorites'
import { Series } from './collections/Series'
// Consent Management Collections // Consent Management Collections
import { CookieConfigurations } from './collections/CookieConfigurations' import { CookieConfigurations } from './collections/CookieConfigurations'
import { CookieInventory } from './collections/CookieInventory' import { CookieInventory } from './collections/CookieInventory'
@ -197,6 +201,9 @@ export default buildConfig({
Bookings, Bookings,
Certifications, Certifications,
Projects, Projects,
// BlogWoman Collections
Favorites,
Series,
// Consent Management // Consent Management
CookieConfigurations, CookieConfigurations,
CookieInventory, CookieInventory,
@ -266,6 +273,9 @@ export default buildConfig({
bookings: {}, bookings: {},
certifications: {}, certifications: {},
projects: {}, projects: {},
// BlogWoman Collections
favorites: {},
series: {},
// Consent Management Collections - customTenantField: true weil sie bereits ein tenant-Feld haben // Consent Management Collections - customTenantField: true weil sie bereits ein tenant-Feld haben
'cookie-configurations': { customTenantField: true }, 'cookie-configurations': { customTenantField: true },
'cookie-inventory': { customTenantField: true }, 'cookie-inventory': { customTenantField: true },