Files
e-tib.com/app/[locale]/blog/[slug]/page.tsx
Marc Mintel 85055c8154
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 36s
Build & Deploy / 🧪 QA (push) Failing after 1m27s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
fix(typography): replace generic prose with industrial mdx components on content pages
2026-05-13 22:41:18 +02:00

326 lines
13 KiB
TypeScript

import { notFound, redirect } from 'next/navigation';
import JsonLd from '@/components/JsonLd';
import { SITE_URL } from '@/lib/schema';
import {
getPostBySlug,
getAdjacentPosts,
getReadingTime,
getHeadings,
} 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 TableOfContents from '@/components/blog/TableOfContents';
import { Heading } from '@/components/ui';
import { setRequestLocale } from 'next-intl/server';
import BlogEngagementTracker from '@/components/analytics/BlogEngagementTracker';
import { MDXRemote } from 'next-mdx-remote/rsc';
const mdxComponents = {
Heading,
h1: (props: any) => <Heading level={1} size="2" className="hidden" {...props} />, // Hidden because Hero handles H1
h2: (props: any) => <Heading level={2} size="3" className="mt-16 mb-6 border-b border-neutral-100 pb-4" {...props} />,
h3: (props: any) => <Heading level={3} size="4" className="mt-12 mb-4 text-primary" {...props} />,
h4: (props: any) => <Heading level={4} size="5" className="mt-8 mb-4 uppercase tracking-widest text-neutral-500" {...props} />,
p: (props: any) => <p className="text-base md:text-lg text-text-secondary leading-[1.8] mb-6 font-medium max-w-3xl" {...props} />,
ul: (props: any) => <ul className="list-none mb-8 space-y-3 text-base md:text-lg text-text-secondary font-medium max-w-3xl" {...props} />,
ol: (props: any) => <ol className="list-decimal pl-6 mb-8 space-y-3 text-base md:text-lg text-text-secondary font-medium max-w-3xl" {...props} />,
li: (props: any) => (
<li className="relative pl-6 before:content-[''] before:absolute before:left-0 before:top-[0.6em] before:w-2 before:h-2 before:bg-primary/50 before:rounded-sm" {...props} />
),
a: (props: any) => <a className="text-primary hover:text-primary-dark underline decoration-primary/30 underline-offset-4 hover:decoration-primary transition-all font-bold" {...props} />,
strong: (props: any) => <strong className="font-bold text-neutral-900" {...props} />,
blockquote: (props: any) => (
<blockquote className="border-l-4 border-primary pl-6 py-3 my-10 italic bg-neutral-50 rounded-r-2xl text-neutral-700 font-medium max-w-3xl shadow-sm" {...props} />
),
hr: (props: any) => <hr className="my-16 border-t-2 border-neutral-100 max-w-3xl" {...props} />,
img: (props: any) => <img className="rounded-2xl shadow-2xl my-12 max-w-full h-auto border border-neutral-100" {...props} />,
};
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/${post.slug}`,
},
openGraph: {
title: `${post.frontmatter.title} | E-TIB`,
description: description,
type: 'article',
publishedTime: post.frontmatter.date,
authors: ['E-TIB'],
url: `${SITE_URL}/${locale}/blog/${post.slug}`,
},
twitter: {
card: 'summary_large_image',
title: `${post.frontmatter.title} | E-TIB`,
description: description,
},
};
}
export default async function BlogPost({ params }: BlogPostProps) {
const { locale, slug } = await params;
setRequestLocale(locale);
const post = await getPostBySlug(slug, locale);
if (!post) {
notFound();
}
const { prev, next, isPrevRandom, isNextRandom } = await getAdjacentPosts(post.slug, locale);
const headings = getHeadings(post.content);
const readingTime = getReadingTime(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={readingTime}
/>
{/* 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
quality={100}
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/90 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} suppressHydrationWarning>
{new Date(post.frontmatter.date).toLocaleDateString(locale || 'de', {
year: 'numeric',
month: 'long',
day: 'numeric',
})}
</time>
<span className="w-1 h-1 bg-white/30 rounded-full" />
<span>{readingTime} 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} suppressHydrationWarning>
{new Date(post.frontmatter.date).toLocaleDateString(locale || 'de', {
year: 'numeric',
month: 'long',
day: 'numeric',
})}
</time>
<span className="w-1 h-1 bg-neutral-400 rounded-full" />
<span>{readingTime} 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 */}
<div className="w-full">
<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}
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 - TOC */}
<aside className="sticky-narrative-sidebar hidden lg:block">
<div className="space-y-12 lg:sticky lg:top-32">
<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: 'E-TIB',
url: SITE_URL,
logo: `${SITE_URL}/logo-blue.svg`,
},
publisher: {
'@type': 'Organization',
name: 'E-TIB',
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${readingTime}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>
);
}