mirror of
https://github.com/complexcaresolutions/cms.c2sgmbh.git
synced 2026-03-17 16:14:12 +00:00
- 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>
527 lines
No EOL
14 KiB
Markdown
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> |