Files
klz-cables.com/app/[locale]/blog/[slug]/page.tsx
Marc Mintel c111efae1a
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 32s
Build & Deploy / 🧪 QA (push) Failing after 1m19s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 1s
chore: fix all linting issues and optimize components
2026-02-11 10:40:57 +01:00

267 lines
10 KiB
TypeScript

import { notFound } from 'next/navigation';
import JsonLd from '@/components/JsonLd';
import { SITE_URL } from '@/lib/schema';
import { MDXRemote } from 'next-mdx-remote/rsc';
import { getPostBySlug, getAdjacentPosts, getReadingTime, getHeadings } from '@/lib/blog';
import { Metadata } from 'next';
import Link from 'next/link';
import PostNavigation from '@/components/blog/PostNavigation';
import PowerCTA from '@/components/blog/PowerCTA';
import TableOfContents from '@/components/blog/TableOfContents';
import { mdxComponents } from '@/components/blog/MDXComponents';
import { Heading } from '@/components/ui';
import { getOGImageMetadata } from '@/lib/metadata';
import { setRequestLocale } from 'next-intl/server';
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: `/${locale}/blog/${slug}`,
languages: {
de: `/de/blog/${slug}`,
en: `/en/blog/${slug}`,
'x-default': `/en/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}`,
images: getOGImageMetadata(`blog/${slug}`, post.frontmatter.title, locale),
},
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 } = await getAdjacentPosts(slug, locale);
if (!post) {
notFound();
}
const headings = getHeadings(post.content);
return (
<article className="bg-white min-h-screen font-sans selection:bg-primary/10 selection:text-primary">
{/* Featured Image Header */}
{post.frontmatter.featuredImage ? (
<div className="relative w-full h-[70vh] min-h-[500px] overflow-hidden group">
<div
className="absolute inset-0 bg-cover bg-center transition-transform duration-[3s] ease-out scale-110 group-hover:scale-100"
style={{ backgroundImage: `url(${post.frontmatter.featuredImage})` }}
/>
<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 animate-slight-fade-in-from-bottom">
{post.frontmatter.category}
</span>
</div>
)}
<Heading
level={1}
className="text-white mb-8 drop-shadow-2xl animate-slight-fade-in-from-bottom [animation-delay:200ms]"
>
{post.frontmatter.title}
</Heading>
<div className="flex flex-wrap items-center gap-6 text-white/80 text-sm md:text-base font-medium animate-slight-fade-in-from-bottom [animation-delay:400ms]">
<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(post.content)} min read</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-secondary 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-300 rounded-full" />
<span>{getReadingTime(post.content)} min read</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 animate-slight-fade-in-from-bottom [animation-delay:600ms]">
<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 */}
<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 animate-slight-fade-in-from-bottom [animation-delay:800ms]">
<MDXRemote source={post.content} components={mdxComponents} />
</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} 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 */}
<aside className="sticky-narrative-sidebar hidden lg:block">
<div className="space-y-12">
<TableOfContents headings={headings} locale={locale} />
</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: post.content.split(/\s+/).length,
timeRequired: `PT${getReadingTime(post.content)}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>
);
}