mirror of
https://github.com/complexcaresolutions/frontend.blogwoman.de.git
synced 2026-03-17 16:14:00 +00:00
- 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>
29 KiB
29 KiB
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 }
}
Featured Content Block (Mixed Content)
// 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 lintohne Errorspnpm builderfolgreich- 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:
- Dokumentiere Blocker in
BLOCKERS.md - Liste versuchte Lösungen auf
- 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