300 lines
11 KiB
TypeScript
300 lines
11 KiB
TypeScript
import { notFound } from 'next/navigation';
|
|
import JsonLd from '@/components/JsonLd';
|
|
import { SITE_URL } from '@/lib/schema';
|
|
import { getPostBySlug, getAdjacentPosts, getReadingTime } from '@/lib/blog';
|
|
import { Metadata } from 'next';
|
|
import Link from 'next/link';
|
|
import Image from 'next/image';
|
|
import PostNavigation from '@/components/blog/PostNavigation';
|
|
import PowerCTA from '@/components/blog/PowerCTA';
|
|
import { Heading } from '@/components/ui';
|
|
import { setRequestLocale } from 'next-intl/server';
|
|
import BlogEngagementTracker from '@/components/analytics/BlogEngagementTracker';
|
|
|
|
// Payload CMS Imports
|
|
import PayloadRichText from '@/components/PayloadRichText';
|
|
|
|
interface BlogPostProps {
|
|
params: Promise<{
|
|
locale: string;
|
|
slug: string;
|
|
}>;
|
|
}
|
|
|
|
export async function generateMetadata({ params }: BlogPostProps): Promise<Metadata> {
|
|
const { locale, slug } = await params;
|
|
const post = await getPostBySlug(slug, locale);
|
|
|
|
if (!post) return {};
|
|
|
|
const description = post.frontmatter.excerpt || '';
|
|
return {
|
|
title: post.frontmatter.title,
|
|
description: description,
|
|
alternates: {
|
|
canonical: `${SITE_URL}/${locale}/blog/${slug}`,
|
|
},
|
|
openGraph: {
|
|
title: `${post.frontmatter.title} | KLZ Cables`,
|
|
description: description,
|
|
type: 'article',
|
|
publishedTime: post.frontmatter.date,
|
|
authors: ['KLZ Cables'],
|
|
url: `${SITE_URL}/${locale}/blog/${slug}`,
|
|
},
|
|
twitter: {
|
|
card: 'summary_large_image',
|
|
title: `${post.frontmatter.title} | KLZ Cables`,
|
|
description: description,
|
|
},
|
|
};
|
|
}
|
|
|
|
export default async function BlogPost({ params }: BlogPostProps) {
|
|
const { locale, slug } = await params;
|
|
setRequestLocale(locale);
|
|
const post = await getPostBySlug(slug, locale);
|
|
const { prev, next, isPrevRandom, isNextRandom } = await getAdjacentPosts(slug, locale);
|
|
|
|
if (!post) {
|
|
notFound();
|
|
}
|
|
|
|
// Convert Lexical content into a plain string to estimate reading time roughly
|
|
const rawTextContent = JSON.stringify(post.content);
|
|
|
|
return (
|
|
<article className="bg-white min-h-screen font-sans selection:bg-primary/10 selection:text-primary">
|
|
<BlogEngagementTracker
|
|
title={post.frontmatter.title}
|
|
slug={slug}
|
|
category={post.frontmatter.category}
|
|
readingTime={getReadingTime(rawTextContent)}
|
|
/>
|
|
|
|
{/* Featured Image Header */}
|
|
{post.frontmatter.featuredImage ? (
|
|
<div className="relative w-full h-[70vh] min-h-[500px] overflow-hidden group">
|
|
<div className="absolute inset-0 transition-transform duration-[3s] ease-out scale-110 group-hover:scale-100">
|
|
<Image
|
|
src={post.frontmatter.featuredImage.split('?')[0]}
|
|
alt={post.frontmatter.title}
|
|
fill
|
|
priority
|
|
className="object-cover"
|
|
sizes="100vw"
|
|
style={{
|
|
objectPosition: `${post.frontmatter.focalX ?? 50}% ${post.frontmatter.focalY ?? 50}%`,
|
|
}}
|
|
/>
|
|
</div>
|
|
<div className="absolute inset-0 bg-gradient-to-t from-neutral-dark via-neutral-dark/40 to-transparent" />
|
|
|
|
{/* Title overlay on image */}
|
|
<div className="absolute inset-0 flex flex-col justify-end pb-16 md:pb-24">
|
|
<div className="container mx-auto px-4">
|
|
<div className="max-w-4xl">
|
|
{post.frontmatter.category && (
|
|
<div className="overflow-hidden mb-6">
|
|
<span className="inline-block px-4 py-1.5 bg-accent text-neutral-dark text-xs font-bold uppercase tracking-[0.2em] rounded-sm">
|
|
{post.frontmatter.category}
|
|
</span>
|
|
</div>
|
|
)}
|
|
<Heading level={1} className="text-white mb-8 drop-shadow-2xl">
|
|
{post.frontmatter.title}
|
|
</Heading>
|
|
<div className="flex flex-wrap items-center gap-6 text-white/80 text-sm md:text-base font-medium">
|
|
<time dateTime={post.frontmatter.date}>
|
|
{new Date(post.frontmatter.date).toLocaleDateString(locale, {
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric',
|
|
})}
|
|
</time>
|
|
<span className="w-1 h-1 bg-white/30 rounded-full" />
|
|
<span>{getReadingTime(rawTextContent)} min read</span>
|
|
{(new Date(post.frontmatter.date) > new Date() ||
|
|
post.frontmatter.public === false) && (
|
|
<>
|
|
<span className="w-1 h-1 bg-white/30 rounded-full" />
|
|
<span className="px-2 py-0.5 border border-white/40 text-white/80 rounded uppercase tracking-widest text-[10px] md:text-xs font-bold">
|
|
Draft Preview
|
|
</span>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<header className="pt-32 pb-16 bg-neutral-50 border-b border-neutral-100">
|
|
<div className="container mx-auto px-4 max-w-4xl">
|
|
{post.frontmatter.category && (
|
|
<div className="mb-6">
|
|
<span className="inline-block px-4 py-1.5 bg-primary/10 text-primary text-xs font-bold uppercase tracking-[0.2em] rounded-sm">
|
|
{post.frontmatter.category}
|
|
</span>
|
|
</div>
|
|
)}
|
|
<Heading level={1} className="mb-8">
|
|
{post.frontmatter.title}
|
|
</Heading>
|
|
<div className="flex items-center gap-6 text-text-primary/80 font-medium">
|
|
<time dateTime={post.frontmatter.date}>
|
|
{new Date(post.frontmatter.date).toLocaleDateString(locale, {
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric',
|
|
})}
|
|
</time>
|
|
<span className="w-1 h-1 bg-neutral-400 rounded-full" />
|
|
<span>{getReadingTime(rawTextContent)} min read</span>
|
|
{(new Date(post.frontmatter.date) > new Date() ||
|
|
post.frontmatter.public === false) && (
|
|
<>
|
|
<span className="w-1 h-1 bg-neutral-300 rounded-full" />
|
|
<span className="px-2 py-0.5 border border-orange-500/50 text-orange-600 rounded uppercase tracking-widest text-[10px] md:text-xs font-bold">
|
|
Draft Preview
|
|
</span>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</header>
|
|
)}
|
|
|
|
{/* Main Content Area with Sticky Narrative Layout */}
|
|
<div className="container mx-auto px-4 py-16 md:py-24">
|
|
<div className="sticky-narrative-container">
|
|
{/* Left Column: Content */}
|
|
<div className="sticky-narrative-content">
|
|
{/* Excerpt/Lead paragraph if available */}
|
|
{post.frontmatter.excerpt && (
|
|
<div className="mb-16">
|
|
<p className="text-xl md:text-2xl text-text-primary leading-relaxed font-medium border-l-4 border-primary pl-8 py-2 italic">
|
|
{post.frontmatter.excerpt}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Main content with enhanced styling rendering Payload Lexical */}
|
|
<div className="prose prose-lg md:prose-xl max-w-none prose-headings:font-bold prose-headings:text-text-primary prose-p:text-text-secondary prose-p:leading-relaxed prose-a:text-primary prose-a:no-underline hover:prose-a:underline prose-img:rounded-2xl prose-img:shadow-2xl prose-blockquote:border-primary prose-blockquote:bg-primary/5 prose-blockquote:rounded-r-2xl prose-strong:text-primary">
|
|
<PayloadRichText data={post.content} />
|
|
</div>
|
|
|
|
{/* Power CTA */}
|
|
<div className="mt-24 animate-slight-fade-in-from-bottom">
|
|
<PowerCTA locale={locale} />
|
|
</div>
|
|
|
|
{/* Post Navigation */}
|
|
<div className="mt-16">
|
|
<PostNavigation
|
|
prev={prev}
|
|
next={next}
|
|
isPrevRandom={isPrevRandom}
|
|
isNextRandom={isNextRandom}
|
|
locale={locale}
|
|
/>
|
|
</div>
|
|
|
|
{/* Back to blog link */}
|
|
<div className="mt-16 pt-10 border-t border-neutral-100">
|
|
<Link
|
|
href={`/${locale}/blog`}
|
|
className="inline-flex items-center gap-3 text-text-secondary hover:text-primary font-bold text-sm uppercase tracking-widest transition-all group"
|
|
>
|
|
<svg
|
|
className="w-5 h-5 transition-transform group-hover:-translate-x-2"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M15 19l-7-7 7-7"
|
|
/>
|
|
</svg>
|
|
{locale === 'de' ? 'Zurück zur Übersicht' : 'Back to Overview'}
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Right Column: Sticky Sidebar - Temporarily Hidden without ToC */}
|
|
<aside className="sticky-narrative-sidebar hidden lg:block">
|
|
<div className="space-y-12">
|
|
{/* Future Payload Table of Contents Implementation */}
|
|
</div>
|
|
</aside>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Structured Data */}
|
|
<JsonLd
|
|
id={`jsonld-${slug}`}
|
|
data={
|
|
{
|
|
'@context': 'https://schema.org',
|
|
'@type': 'BlogPosting',
|
|
headline: post.frontmatter.title,
|
|
datePublished: post.frontmatter.date,
|
|
dateModified: post.frontmatter.date,
|
|
image: post.frontmatter.featuredImage
|
|
? `${SITE_URL}${post.frontmatter.featuredImage}`
|
|
: undefined,
|
|
author: {
|
|
'@type': 'Organization',
|
|
name: 'KLZ Cables',
|
|
url: SITE_URL,
|
|
logo: `${SITE_URL}/logo-blue.svg`,
|
|
},
|
|
publisher: {
|
|
'@type': 'Organization',
|
|
name: 'KLZ Cables',
|
|
logo: {
|
|
'@type': 'ImageObject',
|
|
url: `${SITE_URL}/logo-blue.svg`,
|
|
},
|
|
},
|
|
description: post.frontmatter.excerpt,
|
|
mainEntityOfPage: {
|
|
'@type': 'WebPage',
|
|
'@id': `${SITE_URL}/${locale}/blog/${slug}`,
|
|
},
|
|
articleSection: post.frontmatter.category,
|
|
wordCount: rawTextContent.split(/\s+/).length,
|
|
timeRequired: `PT${getReadingTime(rawTextContent)}M`,
|
|
} as any
|
|
}
|
|
/>
|
|
<JsonLd
|
|
id={`breadcrumb-${slug}`}
|
|
data={
|
|
{
|
|
'@context': 'https://schema.org',
|
|
'@type': 'BreadcrumbList',
|
|
itemListElement: [
|
|
{
|
|
'@type': 'ListItem',
|
|
position: 1,
|
|
name: 'Blog',
|
|
item: `${SITE_URL}/${locale}/blog`,
|
|
},
|
|
{
|
|
'@type': 'ListItem',
|
|
position: 2,
|
|
name: post.frontmatter.title,
|
|
item: `${SITE_URL}/${locale}/blog/${slug}`,
|
|
},
|
|
],
|
|
} as any
|
|
}
|
|
/>
|
|
</article>
|
|
);
|
|
}
|