Zastosowanie wszystkich poprawek UI i logiki kalkulatora
This commit is contained in:
@@ -0,0 +1,250 @@
|
||||
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">
|
||||
{/* Skrypty JSON-LD dla SEO */}
|
||||
<JsonLd data={jsonLdArticle} />
|
||||
<JsonLd data={jsonLdBreadcrumb} />
|
||||
|
||||
<article className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
{/* Powrót */}
|
||||
<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 artykułu */}
|
||||
<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>
|
||||
|
||||
{/* Główny obraz */}
|
||||
<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>
|
||||
|
||||
{/* Treść */}
|
||||
<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>
|
||||
|
||||
{/* CTA do usługi */}
|
||||
<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()}
|
||||
/>
|
||||
|
||||
{/* Powiązane artykuły */}
|
||||
{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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Metadata } from 'next';
|
||||
import { HeroSection } from '@/components/sections/HeroSection';
|
||||
import { PostsGrid } from '@/components/sections/PostsGrid';
|
||||
import { CtaBanner } from '@/components/sections/CtaBanner';
|
||||
import { getPosts, getFeaturedPost } from '@/content/posts';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Blog i Porady',
|
||||
description: 'Baza wiedzy na temat klimatyzacji i pomp ciepła na Dolnym Śląsku. Poradniki, porównania urządzeń i aktualności branżowe.',
|
||||
alternates: {
|
||||
canonical: '/blog',
|
||||
}
|
||||
};
|
||||
|
||||
export const revalidate = 86400; // Regenerate every 24 hours
|
||||
|
||||
export default function BlogPage() {
|
||||
const allPosts = getPosts();
|
||||
const featuredPost = getFeaturedPost();
|
||||
|
||||
// Usuwamy z głównej siatki artykuł wyróżniony, żeby się nie powielał (chyba że chcemy, ale zazwyczaj się usuwa)
|
||||
const remainingPosts = allPosts.filter(post => post.slug !== featuredPost.slug);
|
||||
|
||||
return (
|
||||
<main>
|
||||
<HeroSection
|
||||
eyebrow="Baza Wiedzy"
|
||||
title="Blog i poradniki"
|
||||
subtitle="Dzielimy się naszą wiedzą i doświadczeniem. Zobacz, jak dbać o domowe instalacje, na co uważać podczas remontu i jak uzyskać dofinansowanie."
|
||||
ctaText="Skontaktuj się z nami"
|
||||
ctaLink="/kontakt"
|
||||
secondaryCtaText="Nasze usługi"
|
||||
secondaryCtaLink="/uslugi-domowe"
|
||||
backgroundImage="/images/hero/hero-section-image.webp"
|
||||
/>
|
||||
|
||||
<section className="py-16 md:py-24 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<PostsGrid posts={remainingPosts} featuredPost={featuredPost} />
|
||||
</section>
|
||||
|
||||
<CtaBanner
|
||||
title="Potrzebujesz indywidualnej porady?"
|
||||
subtitle="Zadaj nam pytanie, a z przyjemnością udzielimy darmowej konsultacji i doradzimy najlepsze rozwiązanie."
|
||||
ctaText="Zadaj pytanie"
|
||||
ctaLink="/kontakt"
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user