cms.c2sgmbh/prompts/blogwoman-payload-development_1.md
Martin Porwoll 15f3fa2481 feat(BlogWoman): add tenant seed scripts and block migrations
- Add migration for BlogWoman page blocks (favorites-block, series-block,
  series-detail-block, featured-content-block) with all required columns
- Add seed scripts for BlogWoman tenant creation with full content:
  - 10 pages (Startseite, Über mich, Newsletter, etc.)
  - 7 blog posts
  - 9 series (GRFI, Investment-Piece, Pleasure P&L, etc.)
  - 4 categories, 10 tags, 1 author
  - Navigation, social links, cookie configuration
- Add Konzept-KI guide for AI-assisted tenant creation
- Add BlogWoman tenant prompt template

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 11:13:32 +00:00

527 lines
No EOL
14 KiB
Markdown

# BlogWoman.de - Payload CMS Erweiterungen
**Projekt:** cms.c2sgmbh (Payload CMS Multi-Tenant)
**Ziel:** Neue Collections und Blocks für blogwoman.de
**Server:** sv-payload (10.10.181.100) / Production: Hetzner 3 (162.55.85.18)
**Repository:** github.com/complexcaresolutions/cms.c2sgmbh
---
## Kontext
Das Payload CMS Multi-Tenant-System wird um Features für blogwoman.de erweitert.
BlogWoman ist ein Lifestyle-Blog für berufstätige Frauen mit Fokus auf:
- Affiliate-Produkte (Favoriten)
- YouTube-Serien mit eigenem Branding
- Newsletter-Conversions
- SEO-optimierter Blog-Content
**Tenant:** blogwoman (muss in Tenants Collection angelegt werden, ID wird dynamisch)
---
## Bestehende Infrastruktur
### Vorhandene Collections (NICHT ändern)
- Users, Tenants, Media, Pages, Posts, Categories
- Testimonials, FAQs, Team, Services
- NewsletterSubscribers, Videos, VideoCategories
- CookieConfigurations, CookieInventory, ConsentLogs
- AuditLogs, FormSubmissions
### Vorhandene Blocks (NICHT ändern)
- HeroBlock, HeroSliderBlock, TextBlock, ImageTextBlock
- CardGridBlock, QuoteBlock, CTABlock, VideoBlock
- PostsListBlock, TestimonialsBlock, NewsletterBlock
- FAQBlock, TeamBlock, ServicesBlock, TimelineBlock
- ProcessStepsBlock, DividerBlock, ContactFormBlock
### Tech Stack
- Payload CMS 3.69.0
- Next.js 15.5.9
- React 19.2.3
- PostgreSQL 17
- TypeScript
- pnpm
---
## Phase 1: Favorites Collection & Block
### 1.1 Collection: `favorites`
**Datei:** `src/collections/Favorites.ts`
```typescript
// Struktur:
import type { CollectionConfig } from 'payload'
export const Favorites: CollectionConfig = {
slug: 'favorites',
admin: {
group: 'Content',
useAsTitle: 'title',
defaultColumns: ['title', 'category', 'featured', 'isActive'],
},
access: {
read: () => true, // Public, aber Tenant-gefiltert
create: ({ req: { user } }) => !!user,
update: ({ req: { user } }) => !!user,
delete: ({ req: { user } }) => !!user,
},
fields: [
// ... siehe Feld-Definition unten
],
}
```
**Felder:**
| Feld | Typ | Config | Pflicht |
|------|-----|--------|---------|
| `title` | text | maxLength: 200 | required |
| `slug` | text | unique, admin.position: 'sidebar' | required |
| `description` | textarea | maxLength: 300 | - |
| `category` | select | options: siehe unten | required |
| `subcategory` | text | maxLength: 100 | - |
| `price` | number | min: 0 | - |
| `priceRange` | select | budget, mid, premium, luxury | - |
| `affiliateUrl` | text | URL-Validierung | required |
| `affiliateNetwork` | select | amazon, awin, ltk, direct, other | - |
| `image` | upload | relationTo: 'media' | required |
| `badge` | select | options: siehe unten | - |
| `featured` | checkbox | defaultValue: false | - |
| `isActive` | checkbox | defaultValue: true | required |
| `order` | number | defaultValue: 0 | - |
| `tenant` | relationship | relationTo: 'tenants' | required |
**Category Options:**
```typescript
const categoryOptions = [
{ label: 'Fashion', value: 'fashion' },
{ label: 'Beauty', value: 'beauty' },
{ label: 'Travel', value: 'travel' },
{ label: 'Tech', value: 'tech' },
{ label: 'Home', value: 'home' },
]
```
**Badge Options:**
```typescript
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:**
```typescript
const priceRangeOptions = [
{ label: 'Budget (< €50)', value: 'budget' },
{ label: 'Mid (€50-150)', value: 'mid' },
{ label: 'Premium (€150-500)', value: 'premium' },
{ label: 'Luxury (> €500)', value: 'luxury' },
]
```
### 1.2 Block: `favorites-block`
**Datei:** `src/blocks/FavoritesBlock.ts`
```typescript
import type { Block } from 'payload'
export const FavoritesBlock: Block = {
slug: 'favorites-block',
labels: {
singular: 'Favoriten',
plural: 'Favoriten',
},
fields: [
// ... siehe unten
],
}
```
**Block-Felder:**
| Feld | Typ | Default |
|------|-----|---------|
| `title` | text | - |
| `subtitle` | text | - |
| `category` | select | 'all' (+ alle category options) |
| `showFeaturedOnly` | checkbox | false |
| `limit` | number | 8 |
| `layout` | select | 'grid' (grid, list, carousel) |
| `columns` | select | '4' (2, 3, 4) |
| `showPrice` | checkbox | true |
| `showBadge` | checkbox | true |
| `backgroundColor` | select | 'white' |
---
## Phase 2: Series Collection & Blocks
### 2.1 Collection: `series`
**Datei:** `src/collections/Series.ts`
**Felder:**
| Feld | Typ | Config | Pflicht |
|------|-----|--------|---------|
| `title` | text | localized: true | required |
| `slug` | text | unique | required |
| `tagline` | text | localized: true, maxLength: 150 | - |
| `description` | richText | localized: true | - |
| `logo` | upload | relationTo: 'media' | - |
| `coverImage` | upload | relationTo: 'media' | - |
| `brandColor` | text | placeholder: '#B08D57' | - |
| `accentColor` | text | placeholder: '#FFFFFF' | - |
| `youtubePlaylistId` | text | - | - |
| `youtubePlaylistUrl` | text | URL-Validierung | - |
| `order` | number | defaultValue: 0 | - |
| `isActive` | checkbox | defaultValue: true | required |
| `tenant` | relationship | relationTo: 'tenants' | required |
**Admin Config:**
```typescript
admin: {
group: 'Content',
useAsTitle: 'title',
defaultColumns: ['title', 'slug', 'brandColor', 'isActive'],
}
```
### 2.2 Block: `series-block`
**Datei:** `src/blocks/SeriesBlock.ts`
**Felder:**
| Feld | Typ | Default |
|------|-----|---------|
| `title` | text | - |
| `subtitle` | text | - |
| `layout` | select | 'grid' (grid, list, featured) |
| `showDescription` | checkbox | true |
| `showLogo` | checkbox | true |
| `limit` | number | 6 |
| `columns` | select | '3' (2, 3, 4) |
| `backgroundColor` | select | 'white' |
### 2.3 Block: `series-detail-block`
**Datei:** `src/blocks/SeriesDetailBlock.ts`
Für Serien-Einzelseiten mit Hero und Content.
**Felder:**
| Feld | Typ | Default |
|------|-----|---------|
| `series` | relationship | relationTo: 'series' |
| `showHero` | checkbox | true |
| `showDescription` | checkbox | true |
| `showRelatedPosts` | checkbox | true |
| `relatedPostsLimit` | number | 6 |
| `layout` | select | 'full' (full, compact) |
---
## Phase 3: Video Embed Block
### 3.1 Block: `video-embed-block`
**Datei:** `src/blocks/VideoEmbedBlock.ts`
**Felder:**
| Feld | Typ | Config |
|------|-----|--------|
| `title` | text | optional |
| `videoSource` | select | youtube, vimeo, custom |
| `youtubeUrl` | text | condition: videoSource === 'youtube' |
| `vimeoUrl` | text | condition: videoSource === 'vimeo' |
| `customUrl` | text | condition: videoSource === 'custom' |
| `thumbnail` | upload | relationTo: 'media', optional |
| `caption` | text | localized: true |
| `privacyMode` | checkbox | defaultValue: true |
| `aspectRatio` | select | '16:9' (16:9, 4:3, 1:1, 9:16) |
| `maxWidth` | select | 'large' (full, large, medium, small) |
| `lazyLoad` | checkbox | defaultValue: true |
**YouTube URL Parser (Utility):**
```typescript
// src/lib/utils/youtube.ts
export function extractYouTubeId(url: string): string | null {
if (!url) return null
const patterns = [
/youtube\.com\/watch\?v=([^&]+)/,
/youtu\.be\/([^?]+)/,
/youtube\.com\/embed\/([^?]+)/,
/youtube\.com\/shorts\/([^?]+)/,
]
for (const pattern of patterns) {
const match = url.match(pattern)
if (match) return match[1]
}
return null
}
export function getYouTubeEmbedUrl(videoId: string, privacyMode = true): string {
const domain = privacyMode ? 'www.youtube-nocookie.com' : 'www.youtube.com'
return `https://${domain}/embed/${videoId}`
}
export function getYouTubeThumbnail(videoId: string, quality: 'default' | 'hq' | 'maxres' = 'hq'): string {
const qualityMap = {
default: 'default',
hq: 'hqdefault',
maxres: 'maxresdefault',
}
return `https://img.youtube.com/vi/${videoId}/${qualityMap[quality]}.jpg`
}
```
---
## Phase 4: Stats Block
### 4.1 Block: `stats-block`
**Datei:** `src/blocks/StatsBlock.ts`
**Felder:**
| Feld | Typ | Config |
|------|-----|--------|
| `title` | text | optional |
| `subtitle` | text | optional |
| `stats` | array | siehe Stats Item |
| `layout` | select | 'row' (row, grid, cards) |
| `columns` | select | '4' (2, 3, 4, 5) |
| `animated` | checkbox | defaultValue: false |
| `backgroundColor` | select | 'white' (white, ivory, sand, dark) |
**Stats Array Item:**
| Feld | Typ | Config |
|------|-----|--------|
| `value` | text | required, z.B. "42.000" |
| `prefix` | text | optional, z.B. "€", ">" |
| `suffix` | text | optional, z.B. "+", "%", "K" |
| `label` | text | required, localized |
| `description` | text | optional, localized |
| `icon` | text | optional, Lucide icon name |
---
## Phase 5: Featured Content Block
### 5.1 Block: `featured-content-block`
**Datei:** `src/blocks/FeaturedContentBlock.ts`
**Felder:**
| Feld | Typ | Config |
|------|-----|--------|
| `title` | text | localized |
| `subtitle` | text | localized |
| `items` | array | siehe Items Array |
| `layout` | select | 'grid' (grid, carousel, list, featured-grid) |
| `columns` | select | '3' (2, 3, 4) |
| `showDates` | checkbox | defaultValue: true |
| `backgroundColor` | select | 'white' |
**Items Array:**
| Feld | Typ | Config |
|------|-----|--------|
| `itemType` | select | required: post, video, series, external |
| `post` | relationship | relationTo: 'posts', condition: itemType === 'post' |
| `video` | relationship | relationTo: 'videos', condition: itemType === 'video' |
| `series` | relationship | relationTo: 'series', condition: itemType === 'series' |
| `externalTitle` | text | condition: itemType === 'external' |
| `externalUrl` | text | condition: itemType === 'external' |
| `externalImage` | upload | relationTo: 'media', condition: itemType === 'external' |
| `externalDescription` | textarea | condition: itemType === 'external' |
| `customLabel` | text | optional, z.B. "NEU", "TRENDING" |
---
## Implementierungs-Reihenfolge
### Schritt 1: Collections erstellen
1. `src/collections/Favorites.ts`
2. `src/collections/Series.ts`
### Schritt 2: Collections in payload.config.ts registrieren
```typescript
// In payload.config.ts
import { Favorites } from './collections/Favorites'
import { Series } from './collections/Series'
// In collections Array hinzufügen:
collections: [
// ... bestehende
Favorites,
Series,
],
```
### Schritt 3: Blocks erstellen
1. `src/blocks/FavoritesBlock.ts`
2. `src/blocks/SeriesBlock.ts`
3. `src/blocks/SeriesDetailBlock.ts`
4. `src/blocks/VideoEmbedBlock.ts`
5. `src/blocks/StatsBlock.ts`
6. `src/blocks/FeaturedContentBlock.ts`
### Schritt 4: Blocks in Pages registrieren
```typescript
// In src/collections/Pages.ts -> layout field -> blocks array
import { FavoritesBlock } from '../blocks/FavoritesBlock'
import { SeriesBlock } from '../blocks/SeriesBlock'
import { SeriesDetailBlock } from '../blocks/SeriesDetailBlock'
import { VideoEmbedBlock } from '../blocks/VideoEmbedBlock'
import { StatsBlock } from '../blocks/StatsBlock'
import { FeaturedContentBlock } from '../blocks/FeaturedContentBlock'
// blocks: [
// ... bestehende,
// FavoritesBlock,
// SeriesBlock,
// SeriesDetailBlock,
// VideoEmbedBlock,
// StatsBlock,
// FeaturedContentBlock,
// ]
```
### Schritt 5: Utility-Funktionen erstellen
1. `src/lib/utils/youtube.ts`
### Schritt 6: Migration erstellen
```bash
pnpm payload migrate:create blogwoman_collections
```
### Schritt 7: Build & Test
```bash
pnpm lint --fix
pnpm build
```
---
## Erfolgskriterien
### Funktional
- [ ] Favorites Collection: CRUD über Admin Panel funktioniert
- [ ] Favorites: Filterung nach Kategorie in API (`/api/favorites?where[category][equals]=fashion`)
- [ ] Favorites: Tenant-Isolation funktioniert
- [ ] Favorites Block: In Pages verwendbar
- [ ] Series Collection: CRUD funktioniert
- [ ] Series: Localization (de/en) funktioniert
- [ ] Series Block: In Pages verwendbar
- [ ] Series Detail Block: Zeigt Serie mit related Posts
- [ ] Video Embed Block: YouTube-URLs werden korrekt geparst
- [ ] Video Embed Block: Privacy Mode (youtube-nocookie) funktioniert
- [ ] Stats Block: Zeigt Statistiken in verschiedenen Layouts
- [ ] Featured Content Block: Verschiedene Content-Typen mischbar
### Technisch
- [ ] `pnpm lint` ohne Errors
- [ ] `pnpm build` erfolgreich
- [ ] Keine TypeScript-Fehler
- [ ] Migration erstellt und ausführbar
- [ ] API-Endpoints erreichbar:
- GET /api/favorites
- GET /api/series
### Admin UX
- [ ] Collections erscheinen unter "Content" Gruppe
- [ ] Blocks haben deutsche Labels
- [ ] Conditional Fields funktionieren (z.B. bei itemType)
---
## Escape Hatch
Falls nach 20+ Iterationen blockiert:
1. **Dokumentiere in BLOCKERS.md:**
- Was genau nicht funktioniert
- Fehlermeldungen (vollständig)
- Versuchte Lösungsansätze
2. **Teile das Problem auf:**
- Nur Collections ohne Blocks
- Nur einen Block isoliert
3. **Prüfe Abhängigkeiten:**
- Ist Payload-Version kompatibel?
- Fehlen Import-Statements?
- Gibt es Naming-Konflikte?
---
## Referenzen
### Bestehende Collection als Vorlage
- `src/collections/Posts.ts` (für Struktur)
- `src/collections/Testimonials.ts` (für einfache Collection)
- `src/collections/Videos.ts` (falls vorhanden)
### Bestehende Blocks als Vorlage
- `src/blocks/PostsListBlock.ts`
- `src/blocks/TestimonialsBlock.ts`
- `src/blocks/CardGridBlock.ts`
### Payload Docs
- Collections: https://payloadcms.com/docs/configuration/collections
- Blocks: https://payloadcms.com/docs/fields/blocks
- Conditional Logic: https://payloadcms.com/docs/fields/overview#conditional-logic
---
## Verzeichnis
```
src/
├── collections/
│ ├── Favorites.ts # NEU
│ └── Series.ts # NEU
├── blocks/
│ ├── FavoritesBlock.ts # NEU
│ ├── SeriesBlock.ts # NEU
│ ├── SeriesDetailBlock.ts # NEU
│ ├── VideoEmbedBlock.ts # NEU
│ ├── StatsBlock.ts # NEU
│ └── FeaturedContentBlock.ts # NEU
├── lib/
│ └── utils/
│ └── youtube.ts # NEU
└── migrations/
└── YYYYMMDD_HHMMSS_blogwoman_collections.ts # NEU (auto-generiert)
```
---
## Fertig?
Wenn ALLE Erfolgskriterien erfüllt sind:
- Alle Collections erstellt und registriert
- Alle Blocks erstellt und in Pages registriert
- `pnpm lint` ohne Errors
- `pnpm build` erfolgreich
- Migration erstellt
Dann schreibe als letzte Zeile:
<promise>BLOGWOMAN_PAYLOAD_COMPLETE</promise>