Files
klimat24/app/blog/[slug]/page.tsx
T

239 lines
7.8 KiB
TypeScript

import { Metadata } from 'next';
import Image from 'next/image';
import Link from 'next/link';
import { notFound } from 'next/navigation';
import { getPosts, getPostBySlug, getRelatedPosts } from '@/content/posts';
import { JsonLd } from '@/components/ui/JsonLd';
import { CtaBanner } from '@/components/sections/CtaBanner';
import { Calendar, User, ArrowLeft, ArrowRight } from 'lucide-react';
import { companyDetails } from '@/content/company';
interface BlogPostPageProps {
params: Promise<{
slug: string;
}>;
}
// Generowanie ścieżek statycznych dla wpisów
export async function generateStaticParams() {
const posts = getPosts();
return posts.map((post) => ({
slug: post.slug,
}));
}
// Generowanie metadanych dla danego wpisu
export async function generateMetadata({ params }: BlogPostPageProps): Promise<Metadata> {
const { slug } = await params;
const post = getPostBySlug(slug);
if (!post) {
return {
title: 'Nie znaleziono wpisu',
};
}
const pageTitle = post.metaTitle || post.title;
const pageDescription = post.metaDescription || post.excerpt;
return {
title: pageTitle,
description: pageDescription,
alternates: {
canonical: `/blog/${post.slug}`,
},
openGraph: {
title: pageTitle,
description: pageDescription,
type: 'article',
publishedTime: post.date,
authors: [post.author || companyDetails.shortName],
images: [
{
url: post.image || '/hero.jpg',
},
],
},
};
}
export default async function BlogPostPage({ params }: BlogPostPageProps) {
const { slug } = await params;
const post = getPostBySlug(slug);
if (!post) {
notFound();
}
const relatedPosts = getRelatedPosts(post.slug, post.category, 3);
const dateObj = new Date(post.date);
const formattedDate = dateObj.toLocaleDateString('pl-PL', {
day: 'numeric', month: 'long', year: 'numeric'
});
const categoryLabels: Record<string, string> = {
'klimatyzacja': 'Klimatyzacja',
'remonty': 'Remonty',
'hydraulika': 'Hydraulika',
'porady': 'Porady'
};
// Ustawienie CTA dopasowanego do kategorii wpisu
const getCtaLink = () => {
switch(post.category) {
case 'klimatyzacja': return '/klimatyzacja';
default: return '/kontakt';
}
};
// --- JSON-LD ---
const jsonLdArticle = {
'@context': 'https://schema.org',
'@type': 'BlogPosting',
headline: post.title,
image: post.image,
datePublished: post.date,
dateModified: post.date,
author: {
'@type': 'Person',
name: post.author || companyDetails.name,
},
publisher: {
'@type': 'Organization',
name: companyDetails.name,
logo: {
'@type': 'ImageObject',
url: 'https://thermcool.pl/logo.png', // MOCK URL
},
},
description: post.excerpt,
};
const jsonLdBreadcrumb = {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: [
{
'@type': 'ListItem',
position: 1,
name: 'Strona Główna',
item: 'https://thermcool.pl',
},
{
'@type': 'ListItem',
position: 2,
name: 'Blog',
item: 'https://thermcool.pl/blog',
},
{
'@type': 'ListItem',
position: 3,
name: post.title,
item: `https://thermcool.pl/blog/${post.slug}`,
},
],
};
return (
<main className="bg-slate-50 pt-24 pb-16">
<JsonLd data={jsonLdArticle} />
<JsonLd data={jsonLdBreadcrumb} />
<article className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<Link
href="/blog"
className="inline-flex items-center gap-2 text-sm font-semibold text-primary/70 hover:text-primary transition-colors mb-8"
>
<ArrowLeft className="w-4 h-4" />
Wróć do listy artykułów
</Link>
<header className="mb-10 text-center md:text-left">
<div className="flex flex-wrap items-center justify-center md:justify-start gap-4 mb-6 text-sm font-medium text-text-muted">
<span className="px-3 py-1 bg-accent/10 text-accent font-bold rounded-full uppercase tracking-wider">
{categoryLabels[post.category] || post.category}
</span>
<div className="flex items-center gap-1.5">
<Calendar className="w-4 h-4" />
<time dateTime={post.date}>{formattedDate}</time>
</div>
{post.author && (
<div className="flex items-center gap-1.5">
<User className="w-4 h-4" />
<span>{post.author}</span>
</div>
)}
</div>
<h1 className="text-3xl md:text-5xl font-extrabold text-primary font-heading leading-tight mb-6">
{post.title}
</h1>
<p className="text-lg md:text-xl text-text-muted leading-relaxed max-w-3xl">
{post.excerpt}
</p>
</header>
<div className="relative w-full aspect-video rounded-2xl overflow-hidden mb-12 shadow-md">
<Image
src={post.image || '/images/hero/hero-section-image.webp'}
alt={post.title}
fill
priority
className="object-cover"
/>
</div>
<div className="prose prose-lg md:prose-xl prose-slate max-w-none mb-16
prose-headings:font-heading prose-headings:text-primary prose-headings:font-bold
prose-a:text-accent hover:prose-a:text-accent-hover prose-a:font-semibold
prose-img:rounded-xl prose-img:shadow-md
marker:text-accent"
>
{/* Dane blogowe są obecnie lokalnym, zaufanym contentem; nie dodajemy ciężkiej biblioteki DOMPurify bez potrzeby.
Przy wdrożeniu CMS lub wyświetlaniu treści od użytkowników wymagane będzie sanitizowanie HTML na serwerze. */}
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</div>
</article>
<CtaBanner
title="Podobał Ci się ten artykuł?"
subtitle="Jeśli potrzebujesz pomocy specjalistów w podobnej sprawie, sprawdź naszą ofertę lub skontaktuj się z nami bezpośrednio."
ctaText="Zobacz naszą ofertę"
ctaLink={getCtaLink()}
/>
{relatedPosts.length > 0 && (
<section className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mt-16 md:mt-24">
<div className="flex items-center justify-between border-b border-border pb-4 mb-8">
<h2 className="text-2xl font-bold text-primary font-heading">Powiązane artykuły</h2>
<Link href="/blog" className="text-sm font-semibold text-accent hover:text-accent-hover flex items-center gap-1">
Więcej wpisów <ArrowRight className="w-4 h-4" />
</Link>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{relatedPosts.map(related => (
<Link key={related.slug} href={`/blog/${related.slug}`} className="group block">
<div className="relative aspect-[16/10] rounded-xl overflow-hidden mb-4">
<Image
src={related.image || '/images/hero/hero-section-image.webp'}
alt={related.title}
fill
className="object-cover transition-transform duration-500 group-hover:scale-105"
/>
</div>
<h3 className="text-lg font-bold text-primary font-heading group-hover:text-accent transition-colors mb-2 line-clamp-2">
{related.title}
</h3>
<p className="text-sm text-text-muted line-clamp-2">
{related.excerpt}
</p>
</Link>
))}
</div>
</section>
)}
</main>
);
}