frontend.blogwoman.de/src/app/news/[slug]/page.tsx
CCS Admin 3a8693289f feat: migrate all types from local bridge pattern to @c2s/payload-contracts
Complete type migration removing all 33+ local interfaces and as-unknown-as
casts. All components now use contracts types directly with type-safe
relationship resolution via payload-helpers.ts.

Key changes:
- New payload-helpers.ts: resolveRelation, getMediaUrl, getMediaAlt, socialLinksToMap
- types.ts: thin re-export layer from contracts (backward-compatible aliases)
- api.ts: direct contracts types, no bridge casts, typed getSeoSettings
- All 17 block components: correct CMS field names (headline, subline, cta group, etc.)
- All route files: page.seo.metaTitle (not page.meta.title), getMediaUrl for unions
- structuredData.ts: proper types for all schema generators
- Footer: social links from separate collection via socialLinksToMap()
- Header/Footer: resolveMedia for logo

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 15:25:16 +00:00

106 lines
3.2 KiB
TypeScript

import Image from 'next/image'
import type { Metadata } from 'next'
import { notFound } from 'next/navigation'
import { getPost, getSeoSettings, getSiteSettings } from '@/lib/api'
import { formatDate } from '@/lib/utils'
import { getMediaUrl, getMediaAlt } from '@/lib/payload-helpers'
import { RichTextRenderer } from '@/components/blocks'
import { generateBlogPostingSchema, generateBreadcrumbSchema } from '@/lib/structuredData'
interface NewsPostPageProps {
params: Promise<{ slug: string }>
}
export async function generateMetadata({ params }: NewsPostPageProps): Promise<Metadata> {
const { slug } = await params
const [post, seoSettings] = await Promise.all([
getPost(slug),
getSeoSettings(),
])
if (!post) return {}
const titleSuffix = seoSettings?.metaDefaults?.titleSuffix || ''
const titleBase = post.seo?.metaTitle || post.title
const title = titleSuffix ? `${titleBase} ${titleSuffix}` : titleBase
const description =
post.seo?.metaDescription || post.excerpt || seoSettings?.metaDefaults?.defaultDescription
const image =
getMediaUrl(post.seo?.ogImage) ||
getMediaUrl(post.featuredImage) ||
getMediaUrl(seoSettings?.metaDefaults?.defaultOgImage)
return {
title,
description,
openGraph: {
title,
description: description || undefined,
images: image ? [{ url: image }] : undefined,
type: 'article',
},
twitter: {
card: 'summary_large_image',
title,
description: description || undefined,
images: image ? [image] : undefined,
},
}
}
export default async function NewsPostPage({ params }: NewsPostPageProps) {
const { slug } = await params
const [post, settings] = await Promise.all([
getPost(slug),
getSiteSettings(),
])
if (!post) {
notFound()
}
const imageUrl = getMediaUrl(post.featuredImage)
const blogSchema = generateBlogPostingSchema(post, settings)
const breadcrumbSchema = generateBreadcrumbSchema([
{ name: 'Startseite', url: '/' },
{ name: 'News', url: '/news' },
{ name: post.title, url: `/news/${post.slug}` },
])
return (
<article className="py-12 md:py-16">
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify([blogSchema, breadcrumbSchema]),
}}
/>
<div className="container max-w-3xl">
<header className="mb-10 text-center">
<p className="text-sm text-warm-gray-dark mb-3">
{post.publishedAt ? formatDate(post.publishedAt) : formatDate(post.createdAt)}
</p>
<h1 className="mb-6">{post.title}</h1>
{post.excerpt && (
<p className="text-lg text-espresso/80">{post.excerpt}</p>
)}
</header>
{imageUrl && (
<div className="relative aspect-[16/9] rounded-2xl overflow-hidden bg-warm-gray mb-10">
<Image
src={imageUrl}
alt={getMediaAlt(post.featuredImage, post.title)}
fill
className="object-cover"
priority
/>
</div>
)}
{post.content && <RichTextRenderer content={post.content} />}
</div>
</article>
)
}