frontend.blogwoman.de/prompts/2026-01-20_blogwoman-frontend-entwicklung.md
CCS Admin dcfc48f5ce Add documentation and BlogWoman frontend development prompt
- Add API documentation (API_ANLEITUNG.md)
- Add architecture docs (UNIVERSAL_FEATURES.md, Analytics.md)
- Add guides (FRONTEND.md, SEO_ERWEITERUNG.md, styleguide.md)
- Add Planungs-KI prompt template (ANLEITUNG-PLANUNGS-KI-FRONTEND.md)
- Add BlogWoman frontend development prompt with:
  - Tenant-ID 9 configuration
  - Design system based on styleguide
  - BlogWoman-specific blocks (Favorites, Series, VideoEmbed)
  - API patterns with tenant isolation
  - SEO and Analytics integration

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 21:24:13 +00:00

29 KiB
Raw Permalink Blame History

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

# 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=<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

: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
: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)

: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

: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)

: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

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

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<string, React.ComponentType<any>> = {
  '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 <Component key={`${block.blockType}-${index}`} {...block} />
      })}
    </>
  )
}

6.2 Standard Blocks

Hero Block

// 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

// 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)

// 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

// 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)

// 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)

// 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)

// 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)

// 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 }
}
// 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:

// src/components/ui/SeriesPill.tsx
const seriesColors: Record<string, { bg: string; text: string }> = {
  '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 (
    <span className={`
      inline-flex items-center
      ${sizeClasses[size]}
      ${colors.bg} ${colors.text}
      font-bold tracking-[0.08em] uppercase
      rounded-full
    `}>
      {children}
    </span>
  )
}

8. Seiten-Implementierung

8.1 Root Layout

Datei: src/app/layout.tsx

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 (
    <html lang="de" className={`${inter.variable} ${playfair.variable}`}>
      <body className="bg-ivory text-espresso font-body antialiased">
        <Header navigation={headerNav} settings={siteSettings} />
        <main>{children}</main>
        <Footer navigation={footerNav} settings={siteSettings} />
        <CookieBanner />

        {/* Umami Analytics - Cookieless, DSGVO-konform */}
        <UmamiScript
          websiteId={process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID!}
          host={process.env.NEXT_PUBLIC_UMAMI_HOST}
        />
      </body>
    </html>
  )
}

8.2 Dynamische Seiten

Datei: src/app/[slug]/page.tsx

import { notFound } from 'next/navigation'
import { getPage } from '@/lib/api'
import { BlockRenderer } from '@/components/blocks'
import type { Metadata } from 'next'

interface PageProps {
  params: Promise<{ slug: string }>
}

export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
  const { slug } = await params
  const page = await getPage(slug)

  if (!page) return {}

  return {
    title: page.meta?.title || page.title,
    description: page.meta?.description,
    openGraph: {
      title: page.meta?.title || page.title,
      description: page.meta?.description,
      images: page.meta?.image ? [{ url: page.meta.image.url }] : [],
    },
    robots: {
      index: !page.meta?.noIndex,
      follow: !page.meta?.noFollow,
    },
  }
}

export default async function Page({ params }: PageProps) {
  const { slug } = await params
  const page = await getPage(slug)

  if (!page) notFound()

  return <BlockRenderer blocks={page.layout} />
}

8.3 Serien-Detailseite (BlogWoman)

Datei: src/app/serien/[slug]/page.tsx

import { notFound } from 'next/navigation'
import { getSeriesBySlug } from '@/lib/api'
import { SeriesDetailBlock } from '@/components/blocks/SeriesDetailBlock'
import { VideoEmbedBlock } from '@/components/blocks/VideoEmbedBlock'
import type { Metadata } from 'next'

interface PageProps {
  params: Promise<{ slug: string }>
}

export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
  const { slug } = await params
  const series = await getSeriesBySlug(slug)

  if (!series) return {}

  return {
    title: `${series.title} | BlogWoman`,
    description: series.description,
    openGraph: {
      title: series.title,
      images: series.coverImage ? [{ url: series.coverImage.url }] : [],
    },
  }
}

export default async function SeriesPage({ params }: PageProps) {
  const { slug } = await params
  const series = await getSeriesBySlug(slug)

  if (!series) notFound()

  return (
    <div>
      <SeriesDetailBlock
        series={series}
        layout="hero"
        showLogo
        showPlaylistLink
        useBrandColor
      />

      {/* Videos der Serie werden hier geladen */}
      {/* Implementierung: YouTube API oder Payload YouTube-Content Collection */}
    </div>
  )
}

9. SEO & Meta-Tags

9.1 Metadata API

// src/app/layout.tsx (erweitert)
export async function generateMetadata(): Promise<Metadata> {
  const siteSettings = await getSiteSettings()

  return {
    title: {
      default: 'BlogWoman | Caroline Porwoll',
      template: '%s | BlogWoman',
    },
    description: siteSettings?.description || 'Für Frauen, die Karriere, Familie & Stil ernst nehmen.',
    metadataBase: new URL('https://blogwoman.de'),
    openGraph: {
      type: 'website',
      locale: 'de_DE',
      siteName: 'BlogWoman',
    },
    twitter: {
      card: 'summary_large_image',
    },
    robots: {
      index: true,
      follow: true,
    },
  }
}

9.2 JSON-LD Structured Data

// src/lib/structuredData.ts
export function generateOrganizationSchema(settings: any) {
  return {
    '@context': 'https://schema.org',
    '@type': 'Organization',
    name: 'BlogWoman',
    url: 'https://blogwoman.de',
    logo: settings?.logo?.url,
    sameAs: [
      'https://www.youtube.com/@blogwoman',
      'https://www.instagram.com/blogwoman/',
      // weitere Social Links
    ],
  }
}

export function generateFAQSchema(faqs: Array<{ question: string; answer: string }>) {
  return {
    '@context': 'https://schema.org',
    '@type': 'FAQPage',
    mainEntity: faqs.map(faq => ({
      '@type': 'Question',
      name: faq.question,
      acceptedAnswer: {
        '@type': 'Answer',
        text: faq.answer,
      },
    })),
  }
}

export function generateArticleSchema(post: any, baseUrl: string) {
  return {
    '@context': 'https://schema.org',
    '@type': 'Article',
    headline: post.title,
    description: post.excerpt,
    image: post.featuredImage?.url,
    datePublished: post.publishedAt,
    dateModified: post.updatedAt,
    author: {
      '@type': 'Person',
      name: post.author?.name || 'Caroline Porwoll',
    },
    publisher: {
      '@type': 'Organization',
      name: 'BlogWoman',
      url: baseUrl,
    },
  }
}

10. Analytics (Umami)

10.1 Umami Script Komponente

// src/components/analytics/UmamiScript.tsx
'use client'

import Script from 'next/script'

interface UmamiScriptProps {
  websiteId: string
  host?: string
}

export function UmamiScript({
  websiteId,
  host = 'https://analytics.c2sgmbh.de'
}: UmamiScriptProps) {
  if (!websiteId) return null

  return (
    <Script
      defer
      src={`${host}/script.js`}
      data-website-id={websiteId}
      data-host-url={host}
      strategy="afterInteractive"
    />
  )
}

10.2 Analytics Hook

// src/hooks/useAnalytics.ts
'use client'

import { useCallback } from 'react'

declare global {
  interface Window {
    umami?: {
      track: (eventName: string, eventData?: Record<string, unknown>) => void
    }
  }
}

export function useAnalytics() {
  const trackEvent = useCallback((
    eventName: string,
    eventData?: Record<string, unknown>
  ) => {
    if (typeof window !== 'undefined' && window.umami) {
      window.umami.track(eventName, eventData)
    }
  }, [])

  return {
    trackEvent,
    trackNewsletterSubscribe: (source: string) => trackEvent('newsletter_subscribe', { source }),
    trackCtaClick: (ctaName: string, location: string) => trackEvent('cta_click', { cta_name: ctaName, location }),
    trackAffiliateClick: (productName: string, category: string) => trackEvent('affiliate_click', { product: productName, category }),
    trackSeriesClick: (seriesName: string) => trackEvent('series_click', { series: seriesName }),
    trackVideoPlay: (videoTitle: string, seriesName?: string) => trackEvent('video_play', { title: videoTitle, series: seriesName }),
  }
}

11. Erfolgskriterien

Technisch

  • pnpm lint ohne Errors
  • pnpm build erfolgreich
  • TypeScript ohne Fehler
  • Lighthouse Score > 90 (Performance, Accessibility, Best Practices, SEO)

Funktional

  • Alle Standard-Block-Typen gerendert
  • BlogWoman-spezifische Blocks (Favorites, Series, Video) funktionieren
  • Navigation (Header, Footer, Mobile) funktioniert
  • Blog-Übersicht und Detailseiten
  • Serien-Übersicht und Detailseiten
  • Favoriten-Seite mit Kategorien/Badges
  • Kontaktformular sendet Daten
  • Newsletter-Anmeldung funktioniert (Double Opt-In)
  • Cookie-Banner DSGVO-konform

SEO

  • Meta-Tags korrekt auf allen Seiten
  • Open Graph Tags für Social Sharing
  • JSON-LD Structured Data (Organization, FAQ, Article)
  • Sitemap generiert
  • robots.txt korrekt

Design

  • Styleguide-Farben korrekt implementiert
  • Playfair Display für Headlines
  • Inter für Body/UI
  • Mobile-responsive (alle Breakpoints)
  • Serien-Pills mit korrekten Farben

12. Escape Hatch

Nach 15 Iterationen ohne Fortschritt:

  1. Dokumentiere Blocker in BLOCKERS.md
  2. Liste versuchte Lösungen auf
  3. Output: <promise>BLOCKED</promise>

13. Fertig?

Wenn ALLE Aufgaben erledigt sind:

<promise>BLOGWOMAN_FRONTEND_COMPLETE</promise>

Prompt erstellt: 20. Januar 2026 Für: BlogWoman.de - Payload CMS Multi-Tenant Frontend