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

1114 lines
29 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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=<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<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
```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<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`
```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 (
<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`
```typescript
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`
```typescript
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
```typescript
// 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
```typescript
// 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
```typescript
// 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
```typescript
// 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*