# Frontend BlogWoman.de - Entwicklungs-Prompt *Erstellt: 20. Januar 2026* --- ## 1. Kontext & Konfiguration **Projektverzeichnis:** `/home/frontend/frontend.blogwoman.de` **Tech-Stack:** Next.js 15.5.9, React 19.2.3, TypeScript, Tailwind CSS **API-Basis:** https://cms.c2sgmbh.de/api **Tenant-ID:** 9 **Tenant-Slug:** blogwoman **Domain:** blogwoman.de ### Referenz-Dokumente | Dokument | Pfad / URL | |----------|------------| | OpenAPI Spec | https://cms.c2sgmbh.de/api/openapi.json | | Swagger UI | https://cms.c2sgmbh.de/api/docs | | Styleguide | `/docs/guides/styleguide.md` | | Universal Features | `/docs/architecture/UNIVERSAL_FEATURES.md` | | API-Anleitung | `/docs/api/API_ANLEITUNG.md` | | SEO-Erweiterung | `/docs/guides/SEO_ERWEITERUNG.md` | | Analytics | `/docs/architecture/Analytics.md` | ### Zielgruppe **Persona:** Berufstätige Mütter (35-45 Jahre) - Wenig Zeit → **Schnell scanbar, klare CTAs** - Qualität schätzen → **Premium-Look, keine billige Ästhetik** - Professionalität gewohnt → **Seriös, aber nicht steif** --- ## 2. Environment Variables ```env # API-Endpunkte (PRODUKTION) NEXT_PUBLIC_PAYLOAD_URL=https://cms.c2sgmbh.de NEXT_PUBLIC_API_URL=https://cms.c2sgmbh.de/api # Tenant-Konfiguration NEXT_PUBLIC_TENANT_ID=9 NEXT_PUBLIC_TENANT_SLUG=blogwoman NEXT_PUBLIC_SITE_URL=https://blogwoman.de # Analytics (Umami) NEXT_PUBLIC_UMAMI_HOST=https://analytics.c2sgmbh.de NEXT_PUBLIC_UMAMI_WEBSITE_ID= ``` --- ## 3. Design-System (Styleguide-Referenz) ### 3.1 Design-Philosophie: "Editorial Warmth" - **Premium, nicht protzig** – Qualität durch Zurückhaltung - **Warm, nicht kalt** – Einladend, nicht steril - **Klar, nicht überladen** – Luft zum Atmen, klare Hierarchien - **System, nicht Chaos** – Strukturiert, wiedererkennbar, konsistent ### 3.2 Farbsystem #### Primärfarben (60/30/10 Regel) | Name | Hex | Verwendung | |------|-----|------------| | **Ivory** | `#F7F3EC` | Hintergründe, große Flächen (60%) | | **Sand/Camel** | `#C6A47E` | Cards, Module, Labels (30%) | | **Espresso** | `#2B2520` | Text, Headlines, Kontrast | #### Akzentfarben (10%) | Name | Hex | Verwendung | |------|-----|------------| | **Muted Brass** | `#B08D57` | Primary Buttons, Highlights, Premium-Signal | | **Bordeaux/Wine** | `#6B1F2B` | Pleasure-Akzent, P&L-Serie | | **Rosé** | `#D4A5A5` | SPARK-Serie, feminine Akzente | | **Gold** | `#C9A227` | Inner Circle, Premium-Badges | #### Neutrale Farben | Name | Hex | Verwendung | |------|-----|------------| | **Soft White** | `#FBF8F3` | Cards, helle Flächen | | **Warm Gray** | `#DDD4C7` | Borders, Dividers | | **Warm Gray Dark** | `#B8ADA0` | Placeholder-Text | #### Funktionsfarben | Name | Hex | Verwendung | |------|-----|------------| | **Success** | `#4A7C59` | Erfolg, Bestätigung | | **Warning** | `#D4A574` | Hinweise | | **Error** | `#8B3A3A` | Fehler | | **Info** | `#6B8E9B` | Informationen | ### 3.3 CSS Custom Properties ```css :root { /* Primärfarben */ --color-ivory: #F7F3EC; --color-sand: #C6A47E; --color-espresso: #2B2520; /* Akzentfarben */ --color-brass: #B08D57; --color-bordeaux: #6B1F2B; --color-rose: #D4A5A5; --color-gold: #C9A227; /* Neutrale */ --color-soft-white: #FBF8F3; --color-warm-gray: #DDD4C7; --color-warm-gray-dark: #B8ADA0; /* Semantische Aliase */ --color-background: var(--color-ivory); --color-surface: var(--color-soft-white); --color-text-primary: var(--color-espresso); --color-text-secondary: var(--color-warm-gray-dark); --color-border: var(--color-warm-gray); --color-primary: var(--color-brass); --color-primary-hover: #9E7E4D; } ``` ### 3.4 Typografie | Einsatz | Schrift | Import | |---------|---------|--------| | **Headlines** | Playfair Display | `family=Playfair+Display:wght@400;500;600;700` | | **Body/UI** | Inter | `family=Inter:wght@400;500;600;700` | ```css :root { --font-headline: 'Playfair Display', Georgia, serif; --font-body: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; } ``` **Wichtig:** Keine CAPSLOCK Headlines – wirkt unpremium! ### 3.5 Spacing-System (8px Base) ```css :root { --space-1: 0.25rem; /* 4px */ --space-2: 0.5rem; /* 8px */ --space-3: 0.75rem; /* 12px */ --space-4: 1rem; /* 16px */ --space-6: 1.5rem; /* 24px */ --space-8: 2rem; /* 32px */ --space-12: 3rem; /* 48px */ --space-16: 4rem; /* 64px */ --space-20: 5rem; /* 80px */ } ``` ### 3.6 Border Radius ```css :root { --radius-sm: 4px; --radius-md: 8px; --radius-lg: 12px; /* Buttons */ --radius-xl: 16px; --radius-2xl: 20px; /* Cards */ --radius-full: 9999px; /* Pills, Avatare */ } ``` ### 3.7 Schatten (warm getönt) ```css :root { --shadow-sm: 0 1px 2px rgba(43, 37, 32, 0.05); --shadow-md: 0 4px 12px rgba(43, 37, 32, 0.08); --shadow-lg: 0 8px 24px rgba(43, 37, 32, 0.1); --shadow-xl: 0 12px 40px rgba(43, 37, 32, 0.12); } ``` --- ## 4. Projektstruktur ``` src/ ├── app/ │ ├── layout.tsx # Root-Layout mit Header/Footer/Analytics │ ├── page.tsx # Startseite │ ├── [slug]/ │ │ └── page.tsx # Dynamische Seiten │ ├── blog/ │ │ ├── page.tsx # Blog-Übersicht │ │ └── [slug]/ │ │ └── page.tsx # Blog-Artikel │ ├── serien/ # BlogWoman-spezifisch │ │ ├── page.tsx # Serien-Übersicht │ │ └── [slug]/ │ │ └── page.tsx # Serien-Detailseite │ ├── favoriten/ # BlogWoman-spezifisch │ │ └── page.tsx # Affiliate-Produkte │ └── api/ # Optional: Proxy-Routes ├── components/ │ ├── layout/ │ │ ├── Header.tsx │ │ ├── Footer.tsx │ │ ├── Navigation.tsx │ │ ├── MobileMenu.tsx │ │ └── CookieBanner.tsx │ ├── blocks/ # Block-Komponenten │ │ ├── HeroBlock.tsx │ │ ├── HeroSliderBlock.tsx │ │ ├── TextBlock.tsx │ │ ├── ImageTextBlock.tsx │ │ ├── CardGridBlock.tsx │ │ ├── CTABlock.tsx │ │ ├── DividerBlock.tsx │ │ ├── PostsListBlock.tsx │ │ ├── TestimonialsBlock.tsx │ │ ├── FAQBlock.tsx │ │ ├── NewsletterBlock.tsx │ │ ├── ContactFormBlock.tsx │ │ ├── VideoBlock.tsx │ │ ├── TimelineBlock.tsx │ │ ├── ProcessStepsBlock.tsx │ │ │ │ │ │ # BlogWoman-spezifische Blocks │ │ ├── FavoritesBlock.tsx │ │ ├── SeriesBlock.tsx │ │ ├── SeriesDetailBlock.tsx │ │ ├── VideoEmbedBlock.tsx │ │ ├── FeaturedContentBlock.tsx │ │ │ │ │ └── index.tsx # Block-Renderer │ ├── ui/ # Wiederverwendbare UI-Komponenten │ │ ├── Button.tsx │ │ ├── Card.tsx │ │ ├── VideoCard.tsx │ │ ├── ProductCard.tsx │ │ ├── SeriesPill.tsx │ │ ├── Badge.tsx │ │ ├── Input.tsx │ │ ├── Textarea.tsx │ │ └── ... │ └── analytics/ │ └── UmamiScript.tsx ├── lib/ │ ├── api.ts # API-Funktionen │ ├── utils.ts # Hilfsfunktionen │ ├── types.ts # TypeScript-Typen │ └── structuredData.ts # JSON-LD Helpers ├── hooks/ │ ├── useAnalytics.ts │ └── useCookieConsent.ts ├── config/ │ └── analytics.ts └── styles/ └── globals.css # Tailwind + Custom Properties ``` --- ## 5. API-Client Implementation **Datei:** `src/lib/api.ts` ```typescript const PAYLOAD_URL = process.env.NEXT_PUBLIC_PAYLOAD_URL const TENANT_ID = process.env.NEXT_PUBLIC_TENANT_ID // KRITISCH: Immer Tenant-Filter verwenden! // Ohne Tenant-Filter: 403 Forbidden oder leere Ergebnisse export async function getPage(slug: string, locale = 'de') { const res = await fetch( `${PAYLOAD_URL}/api/pages?` + `where[tenant][equals]=${TENANT_ID}&` + `where[slug][equals]=${slug}&` + `where[status][equals]=published&` + `locale=${locale}&` + `depth=2`, { next: { revalidate: 60 } } ) const data = await res.json() return data.docs[0] || null } export async function getPosts(options: { type?: 'blog' | 'news' | 'press' category?: string limit?: number page?: number locale?: string }) { const params = new URLSearchParams({ 'where[tenant][equals]': TENANT_ID!, 'where[status][equals]': 'published', 'sort': '-publishedAt', 'limit': String(options.limit || 10), 'page': String(options.page || 1), 'locale': options.locale || 'de', 'depth': '1' }) if (options.type) { params.append('where[type][equals]', options.type) } if (options.category) { params.append('where[categories][contains]', options.category) } const res = await fetch(`${PAYLOAD_URL}/api/posts?${params}`) return res.json() } export async function getNavigation(type: 'header' | 'footer' | 'mobile') { const res = await fetch( `${PAYLOAD_URL}/api/navigations?` + `where[tenant][equals]=${TENANT_ID}&` + `where[type][equals]=${type}&` + `depth=2` ) const data = await res.json() return data.docs[0] || null } export async function getSiteSettings() { const res = await fetch( `${PAYLOAD_URL}/api/site-settings?` + `where[tenant][equals]=${TENANT_ID}&` + `depth=2` ) const data = await res.json() return data.docs[0] || null } // BlogWoman-spezifisch: Favorites export async function getFavorites(options: { category?: 'fashion' | 'beauty' | 'travel' | 'tech' | 'home' badge?: 'investment-piece' | 'daily-driver' | 'grfi-approved' | 'new' | 'bestseller' limit?: number locale?: string }) { const params = new URLSearchParams({ 'where[tenant][equals]': TENANT_ID!, 'where[isActive][equals]': 'true', 'limit': String(options.limit || 12), 'locale': options.locale || 'de', 'depth': '1' }) if (options.category) { params.append('where[category][equals]', options.category) } if (options.badge) { params.append('where[badge][equals]', options.badge) } const res = await fetch(`${PAYLOAD_URL}/api/favorites?${params}`) return res.json() } // BlogWoman-spezifisch: Series export async function getSeries(options?: { limit?: number locale?: string }) { const params = new URLSearchParams({ 'where[tenant][equals]': TENANT_ID!, 'where[isActive][equals]': 'true', 'locale': options?.locale || 'de', 'depth': '2' }) const res = await fetch(`${PAYLOAD_URL}/api/series?${params}`) return res.json() } export async function getSeriesBySlug(slug: string, locale = 'de') { const res = await fetch( `${PAYLOAD_URL}/api/series?` + `where[tenant][equals]=${TENANT_ID}&` + `where[slug][equals]=${slug}&` + `locale=${locale}&` + `depth=2` ) const data = await res.json() return data.docs[0] || null } // Newsletter-Anmeldung export async function subscribeNewsletter(email: string, firstName?: string, source?: string) { const res = await fetch(`${PAYLOAD_URL}/api/newsletter/subscribe`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, firstName, tenantId: Number(TENANT_ID), source: source || 'website' }) }) return res.json() } // Kontaktformular export async function submitContactForm(data: { name: string email: string phone?: string subject: string message: string }) { const res = await fetch(`${PAYLOAD_URL}/api/form-submissions`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ form: 1, // Form-ID aus Payload submissionData: [ { field: 'name', value: data.name }, { field: 'email', value: data.email }, { field: 'phone', value: data.phone || '' }, { field: 'subject', value: data.subject }, { field: 'message', value: data.message } ] }) }) return res.json() } ``` --- ## 6. Block-Komponenten ### 6.1 Block-Renderer **Datei:** `src/components/blocks/index.tsx` ```typescript import { HeroBlock } from './HeroBlock' import { HeroSliderBlock } from './HeroSliderBlock' import { TextBlock } from './TextBlock' import { ImageTextBlock } from './ImageTextBlock' import { CardGridBlock } from './CardGridBlock' import { CTABlock } from './CTABlock' import { DividerBlock } from './DividerBlock' import { PostsListBlock } from './PostsListBlock' import { TestimonialsBlock } from './TestimonialsBlock' import { FAQBlock } from './FAQBlock' import { NewsletterBlock } from './NewsletterBlock' import { ContactFormBlock } from './ContactFormBlock' import { VideoBlock } from './VideoBlock' import { TimelineBlock } from './TimelineBlock' import { ProcessStepsBlock } from './ProcessStepsBlock' // BlogWoman-spezifisch import { FavoritesBlock } from './FavoritesBlock' import { SeriesBlock } from './SeriesBlock' import { SeriesDetailBlock } from './SeriesDetailBlock' import { VideoEmbedBlock } from './VideoEmbedBlock' import { FeaturedContentBlock } from './FeaturedContentBlock' const blockComponents: Record> = { 'hero-block': HeroBlock, 'hero-slider-block': HeroSliderBlock, 'text-block': TextBlock, 'image-text-block': ImageTextBlock, 'card-grid-block': CardGridBlock, 'cta-block': CTABlock, 'divider-block': DividerBlock, 'posts-list-block': PostsListBlock, 'testimonials-block': TestimonialsBlock, 'faq-block': FAQBlock, 'newsletter-block': NewsletterBlock, 'contact-form-block': ContactFormBlock, 'video-block': VideoBlock, 'timeline-block': TimelineBlock, 'process-steps-block': ProcessStepsBlock, // BlogWoman-spezifisch 'favorites-block': FavoritesBlock, 'series-block': SeriesBlock, 'series-detail-block': SeriesDetailBlock, 'video-embed-block': VideoEmbedBlock, 'featured-content-block': FeaturedContentBlock, } interface BlockRendererProps { blocks: Array<{ blockType: string; [key: string]: any }> } export function BlockRenderer({ blocks }: BlockRendererProps) { if (!blocks || blocks.length === 0) return null return ( <> {blocks.map((block, index) => { const Component = blockComponents[block.blockType] if (!Component) { console.warn(`Unknown block type: ${block.blockType}`) return null } return })} ) } ``` ### 6.2 Standard Blocks #### Hero Block ```typescript // src/components/blocks/HeroBlock.tsx interface HeroBlockProps { heading: string subheading?: string backgroundImage?: { url: string } ctaText?: string ctaLink?: string alignment?: 'left' | 'center' | 'right' overlay?: boolean overlayOpacity?: number } ``` #### Posts List Block ```typescript // src/components/blocks/PostsListBlock.tsx interface PostsListBlockProps { title?: string subtitle?: string postType?: 'blog' | 'news' | 'press' | 'announcement' layout?: 'grid' | 'list' | 'featured' | 'compact' | 'masonry' columns?: 2 | 3 | 4 limit?: number showFeaturedOnly?: boolean filterByCategory?: string showExcerpt?: boolean showDate?: boolean showAuthor?: boolean showCategory?: boolean showPagination?: boolean backgroundColor?: 'white' | 'ivory' | 'sand' } ``` #### FAQ Block (mit Schema.org) ```typescript // src/components/blocks/FAQBlock.tsx interface FAQBlockProps { title?: string subtitle?: string displayMode: 'all' | 'selected' | 'byCategory' selectedFaqs?: Array<{ id: string; question: string; answer: any }> filterCategory?: string layout?: 'accordion' | 'list' | 'grid' expandFirst?: boolean showSchema?: boolean // JSON-LD für SEO } ``` #### Newsletter Block ```typescript // src/components/blocks/NewsletterBlock.tsx interface NewsletterBlockProps { title?: string subtitle?: string buttonText?: string layout?: 'inline' | 'stacked' | 'with-image' | 'minimal' | 'card' backgroundImage?: { url: string } showPrivacyNote?: boolean } ``` ### 6.3 BlogWoman-spezifische Blocks #### Favorites Block (Affiliate-Produkte) ```typescript // src/components/blocks/FavoritesBlock.tsx interface FavoritesBlockProps { title?: string subtitle?: string displayMode: 'all' | 'selected' | 'byCategory' selectedFavorites?: Array<{ id: string title: string description?: any image?: { url: string } affiliateUrl: string price?: string category?: 'fashion' | 'beauty' | 'travel' | 'tech' | 'home' badge?: 'investment-piece' | 'daily-driver' | 'grfi-approved' | 'new' | 'bestseller' priceRange?: 'budget' | 'mid' | 'premium' | 'luxury' }> filterCategory?: string layout?: 'grid' | 'list' | 'carousel' columns?: 2 | 3 | 4 limit?: number showPrice?: boolean showBadge?: boolean } ``` #### Series Block (YouTube-Serien) ```typescript // src/components/blocks/SeriesBlock.tsx interface SeriesBlockProps { title?: string subtitle?: string displayMode: 'all' | 'selected' selectedSeries?: Array<{ id: string title: string slug: string description?: any logo?: { url: string } coverImage?: { url: string } brandColor?: string youtubePlaylistId?: string }> layout?: 'grid' | 'list' | 'featured' showDescription?: boolean } ``` #### Series Detail Block (Hero für Serien-Seiten) ```typescript // src/components/blocks/SeriesDetailBlock.tsx interface SeriesDetailBlockProps { series: { id: string title: string description?: any logo?: { url: string } coverImage?: { url: string } brandColor?: string youtubePlaylistId?: string } layout?: 'hero' | 'compact' | 'sidebar' showLogo?: boolean showPlaylistLink?: boolean useBrandColor?: boolean } ``` #### Video Embed Block (Privacy Mode) ```typescript // src/components/blocks/VideoEmbedBlock.tsx interface VideoEmbedBlockProps { videoUrl: string title?: string aspectRatio?: '16:9' | '4:3' | '1:1' | '9:16' privacyMode?: boolean // youtube-nocookie.com autoplay?: boolean showControls?: boolean thumbnailImage?: { url: string } } ``` #### Featured Content Block (Mixed Content) ```typescript // src/components/blocks/FeaturedContentBlock.tsx interface FeaturedContentBlockProps { title?: string subtitle?: string items: Array<{ type: 'post' | 'video' | 'favorite' | 'series' | 'external' post?: { id: string; title: string; slug: string; excerpt?: string; featuredImage?: { url: string } } video?: { id: string; title: string; videoUrl: string; thumbnailImage?: { url: string } } favorite?: { id: string; title: string; affiliateUrl: string; image?: { url: string } } series?: { id: string; title: string; slug: string; logo?: { url: string } } externalUrl?: string externalTitle?: string externalImage?: { url: string } }> layout?: 'grid' | 'masonry' | 'featured' } ``` --- ## 7. Serien-Pills (UI-Komponente) BlogWoman nutzt farbcodierte Pills für die YouTube-Serien: ```typescript // src/components/ui/SeriesPill.tsx const seriesColors: Record = { 'grfi': { bg: 'bg-sand', text: 'text-espresso' }, 'investment': { bg: 'bg-brass', text: 'text-soft-white' }, 'pl': { bg: 'bg-bordeaux', text: 'text-soft-white' }, // Pleasure & Loss 'spark': { bg: 'bg-rose', text: 'text-espresso' }, 'inner-circle': { bg: 'bg-gold', text: 'text-espresso' }, 'reset': { bg: 'bg-sand', text: 'text-espresso' }, 'decision': { bg: 'bg-sand', text: 'text-espresso' }, 'regeneration': { bg: 'bg-sand', text: 'text-espresso' }, 'm2m': { bg: 'bg-sand', text: 'text-espresso' }, // Mom to Mom 'backstage': { bg: 'bg-warm-gray', text: 'text-espresso' }, } interface SeriesPillProps { series: string children: React.ReactNode size?: 'sm' | 'md' | 'lg' } export function SeriesPill({ series, children, size = 'md' }: SeriesPillProps) { const colors = seriesColors[series.toLowerCase()] || seriesColors['grfi'] const sizeClasses = { sm: 'px-2 py-1 text-[10px]', md: 'px-3.5 py-1.5 text-[11px]', lg: 'px-6 py-2.5 text-[13px]', } return ( {children} ) } ``` --- ## 8. Seiten-Implementierung ### 8.1 Root Layout **Datei:** `src/app/layout.tsx` ```typescript import { Inter, Playfair_Display } from 'next/font/google' import { Header } from '@/components/layout/Header' import { Footer } from '@/components/layout/Footer' import { CookieBanner } from '@/components/layout/CookieBanner' import { UmamiScript } from '@/components/analytics/UmamiScript' import { getNavigation, getSiteSettings } from '@/lib/api' import './globals.css' const inter = Inter({ subsets: ['latin'], variable: '--font-body' }) const playfair = Playfair_Display({ subsets: ['latin'], variable: '--font-headline' }) export default async function RootLayout({ children }: { children: React.ReactNode }) { const [headerNav, footerNav, siteSettings] = await Promise.all([ getNavigation('header'), getNavigation('footer'), getSiteSettings(), ]) return (
{children}