- 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>
14 KiB
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
// 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:
const categoryOptions = [
{ label: 'Fashion', value: 'fashion' },
{ label: 'Beauty', value: 'beauty' },
{ label: 'Travel', value: 'travel' },
{ label: 'Tech', value: 'tech' },
{ label: 'Home', value: 'home' },
]
Badge Options:
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:
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
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:
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):
// 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
src/collections/Favorites.tssrc/collections/Series.ts
Schritt 2: Collections in payload.config.ts registrieren
// 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
src/blocks/FavoritesBlock.tssrc/blocks/SeriesBlock.tssrc/blocks/SeriesDetailBlock.tssrc/blocks/VideoEmbedBlock.tssrc/blocks/StatsBlock.tssrc/blocks/FeaturedContentBlock.ts
Schritt 4: Blocks in Pages registrieren
// 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
src/lib/utils/youtube.ts
Schritt 6: Migration erstellen
pnpm payload migrate:create blogwoman_collections
Schritt 7: Build & Test
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 lintohne Errorspnpm builderfolgreich- 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:
-
Dokumentiere in BLOCKERS.md:
- Was genau nicht funktioniert
- Fehlermeldungen (vollständig)
- Versuchte Lösungsansätze
-
Teile das Problem auf:
- Nur Collections ohne Blocks
- Nur einen Block isoliert
-
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.tssrc/blocks/TestimonialsBlock.tssrc/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 lintohne Errorspnpm builderfolgreich- Migration erstellt
Dann schreibe als letzte Zeile:
BLOGWOMAN_PAYLOAD_COMPLETE