feat: optymalizacja GlobalSearch, nawigacji oraz filtrów w sklepie

This commit is contained in:
MichalC
2026-06-27 21:12:23 +02:00
parent 0c59086a6b
commit c4294a81cf
43 changed files with 1623 additions and 192 deletions
+51 -1
View File
@@ -22,8 +22,57 @@ export const metadata: Metadata = {
};
export default function KlimatyzacjaPage() {
const faqJsonLd = {
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{
"@type": "Question",
"name": "Ile kosztuje montaż klimatyzacji?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Koszt montażu klimatyzacji split wynosi od 1200 zł netto. Cena zależy od długości instalacji, trudności montażu i wybranego urządzenia. Oferujemy bezpłatną wycenę przed realizacją.",
},
},
{
"@type": "Question",
"name": "Jak długo trwa montaż klimatyzacji?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Standardowy montaż klimatyzatora split trwa od 4 do 8 godzin. Systemy multisplit wymagają 1-2 dni roboczych w zależności od liczby jednostek.",
},
},
{
"@type": "Question",
"name": "Czy potrzebuję uprawnień F-Gaz do montażu klimatyzacji?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Tak, montaż klimatyzacji zawierającej czynniki F-Gaz (R32, R410A) wymaga certyfikatu F-Gaz. Posiadamy wszystkie wymagane uprawnienia UDT.",
},
},
{
"@type": "Question",
"name": "Jak często serwisować klimatyzację?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Zalecamy serwis klimatyzacji 2 razy w roku: przed sezonem letnim (kwiecień-maj) i przed zimowym (wrzesień-październik). Regularny serwis zachowuje gwarancję producenta.",
},
},
{
"@type": "Question",
"name": "Czy klimatyzacja może ogrzewać pomieszczenia zimą?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Tak, nowoczesne klimatyzatory inwerterowe to pompy ciepła powietrze-powietrze, które efektywnie ogrzewają nawet przy -20°C na zewnątrz. Koszt ogrzewania klimatyzacją jest 3-4 razy niższy niż ogrzewaniem elektrycznym.",
},
},
],
};
return (
<main>
<>
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(faqJsonLd) }} />
<main>
<HeroSection
eyebrow="15+ lat doświadczenia · Split i multisplit · Uprawnienia F-gaz"
title="Montaż klimatyzacji — dobór, instalacja, serwis"
@@ -118,5 +167,6 @@ export default function KlimatyzacjaPage() {
ctaLink="/kontakt"
/>
</main>
</>
);
}
+35 -2
View File
@@ -20,8 +20,37 @@ const fontHeading = Poppins({
});
export const metadata: Metadata = {
title: "ThermCool | Klimatyzacja i Pompy Ciepła",
description: "Profesjonalny montaż i serwis klimatyzacji oraz pomp ciepła na Dolnym Śląsku. Uprawnienia F-Gaz, bezpłatny audyt, estetyczny montaż i pełna gwarancja. Zadzwoń po wycenę!",
title: {
default: "ThermCool | Klimatyzacja i Pompy Ciepła — Dolny Śląsk",
template: "%s | ThermCool",
},
description: "Profesjonalny montaż i serwis klimatyzacji oraz pomp ciepła na Dolnym Śląsku. Uprawnienia F-Gaz, bezpłatny audyt, estetyczny montaż. Zadzwoń po wycenę!",
metadataBase: new URL(process.env.NEXT_PUBLIC_BASE_URL || 'https://thermcool.pl'),
openGraph: {
type: 'website',
locale: 'pl_PL',
siteName: 'ThermCool',
title: 'ThermCool | Klimatyzacja i Pompy Ciepła — Dolny Śląsk',
description: 'Profesjonalny montaż i serwis klimatyzacji oraz pomp ciepła na Dolnym Śląsku.',
images: [{ url: '/og-image.jpg', width: 1200, height: 630, alt: 'ThermCool — Klimatyzacja i Pompy Ciepła' }],
},
twitter: {
card: 'summary_large_image',
title: 'ThermCool | Klimatyzacja i Pompy Ciepła',
description: 'Profesjonalny montaż i serwis klimatyzacji oraz pomp ciepła na Dolnym Śląsku.',
images: ['/og-image.jpg'],
},
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
'max-video-preview': -1,
'max-image-preview': 'large',
'max-snippet': -1,
},
},
};
export default function RootLayout({
@@ -34,6 +63,10 @@ export default function RootLayout({
lang="pl"
className={`${fontSans.variable} ${fontHeading.variable} antialiased`}
>
<head>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
</head>
<body className="min-h-screen flex flex-col bg-background text-foreground">
<Header />
<main className="flex-grow">{children}</main>
+105 -26
View File
@@ -9,39 +9,118 @@ import { CtaBanner } from "@/components/sections/CtaBanner";
import { companyDetails } from "@/content/company";
export default function Home() {
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'https://thermcool.pl';
const jsonLd = {
"@context": "https://schema.org",
"@type": "LocalBusiness",
"name": companyDetails.name,
"image": `${process.env.NEXT_PUBLIC_BASE_URL || 'https://thermcool.pl'}/logo.png`,
"telephone": companyDetails.phone,
"email": companyDetails.email,
"address": {
"@type": "PostalAddress",
"streetAddress": companyDetails.address.street,
"addressLocality": companyDetails.address.city,
"postalCode": companyDetails.address.zipCode,
"addressCountry": "PL",
},
"priceRange": "$$",
"openingHoursSpecification": [
"@graph": [
{
"@type": "OpeningHoursSpecification",
"dayOfWeek": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
"opens": "08:00",
"closes": "18:00",
"@type": "HVACBusiness",
"@id": `${baseUrl}/#business`,
"name": companyDetails.name,
"legalName": companyDetails.legalName,
"url": baseUrl,
"logo": {
"@type": "ImageObject",
"url": `${baseUrl}/logo.png`,
"width": 200,
"height": 60,
},
"image": `${baseUrl}/og-image.jpg`,
"telephone": companyDetails.phone,
"email": companyDetails.email,
"address": {
"@type": "PostalAddress",
"streetAddress": companyDetails.address.street,
"addressLocality": companyDetails.address.city,
"postalCode": companyDetails.address.zipCode,
"addressRegion": companyDetails.address.region,
"addressCountry": "PL",
},
"geo": {
"@type": "GeoCoordinates",
"latitude": 51.1079,
"longitude": 17.0385,
},
"priceRange": "$$",
"currenciesAccepted": "PLN",
"paymentAccepted": "Gotówka, Przelew bankowy, Karta",
"openingHoursSpecification": [
{
"@type": "OpeningHoursSpecification",
"dayOfWeek": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
"opens": "08:00",
"closes": "18:00",
},
{
"@type": "OpeningHoursSpecification",
"dayOfWeek": ["Saturday"],
"opens": "09:00",
"closes": "14:00",
},
],
"areaServed": [
{ "@type": "City", "name": "Wrocław" },
{ "@type": "City", "name": "Legnica" },
{ "@type": "City", "name": "Wałbrzych" },
{ "@type": "City", "name": "Jelenia Góra" },
{ "@type": "City", "name": "Lubin" },
{ "@type": "AdministrativeArea", "name": "Dolny Śląsk" },
],
"hasOfferCatalog": {
"@type": "OfferCatalog",
"name": "Usługi HVAC",
"itemListElement": [
{
"@type": "Offer",
"itemOffered": {
"@type": "Service",
"name": "Montaż klimatyzacji",
"description": "Profesjonalny montaż klimatyzatorów split i multisplit z uprawnieniami F-Gaz. Ceny od 1200 zł.",
},
},
{
"@type": "Offer",
"itemOffered": {
"@type": "Service",
"name": "Montaż pomp ciepła",
"description": "Dobór i montaż pomp ciepła powietrze-woda z pomocą w uzyskaniu dofinansowania z Czystego Powietrza.",
},
},
{
"@type": "Offer",
"itemOffered": {
"@type": "Service",
"name": "Serwis klimatyzacji",
"description": "Przeglądy, czyszczenie, odgrzybianie i naprawy klimatyzatorów. Ceny od 250 zł.",
},
},
],
},
"sameAs": [
companyDetails.socials.facebook,
companyDetails.socials.instagram,
companyDetails.socials.google,
],
},
{
"@type": "OpeningHoursSpecification",
"dayOfWeek": ["Saturday"],
"opens": "09:00",
"closes": "14:00",
"@type": "WebSite",
"@id": `${baseUrl}/#website`,
"url": baseUrl,
"name": "ThermCool",
"description": "Klimatyzacja i pompy ciepła na Dolnym Śląsku",
"inLanguage": "pl-PL",
"publisher": { "@id": `${baseUrl}/#business` },
"potentialAction": {
"@type": "SearchAction",
"target": {
"@type": "EntryPoint",
"urlTemplate": `${baseUrl}/sklep?search={search_term_string}`,
},
"query-input": "required name=search_term_string",
},
},
],
"areaServed": {
"@type": "State",
"name": companyDetails.address.region,
},
};
return (
+51 -1
View File
@@ -19,8 +19,57 @@ export const metadata: Metadata = {
};
export default function PompyCieplaPage() {
const faqPompyJsonLd = {
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{
"@type": "Question",
"name": "Ile kosztuje pompa ciepła?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Koszt pompy ciepła powietrze-woda wraz z montażem wynosi od 25 000 do 60 000 zł. Dzięki dofinansowaniu z programu Czyste Powietrze można uzyskać do 135 000 zł zwrotu kosztów.",
},
},
{
"@type": "Question",
"name": "Jak działa pompa ciepła?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Pompa ciepła pobiera energię cieplną z powietrza zewnętrznego i przetwarza ją na ciepło do ogrzewania budynku i wody użytkowej. Na każdy 1 kW zużytej energii elektrycznej produkuje 3-5 kW ciepła (COP 3-5).",
},
},
{
"@type": "Question",
"name": "Czy pompa ciepła działa przy mrozach?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Nowoczesne pompy ciepła pracują efektywnie nawet przy -20°C/-25°C. Przy bardzo niskich temperaturach urządzenie wspomagane jest grzałką elektryczną.",
},
},
{
"@type": "Question",
"name": "Ile można zaoszczędzić na ogrzewaniu dzięki pompie ciepła?",
"acceptedAnswer": {
"@type": "Answer",
"text": "W porównaniu z ogrzewaniem gazowym można zaoszczędzić 40-60% kosztów rocznych. Dom 150 m² ogrzewany gazem płaci ok. 4 000-6 000 zł/rok, z pompą ciepła ok. 2 000-3 500 zł/rok.",
},
},
{
"@type": "Question",
"name": "Czy można uzyskać dofinansowanie na pompę ciepła?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Tak, program Czyste Powietrze oferuje dofinansowanie do 135 000 zł dla najniższych dochodów. Pomagamy w kompletowaniu dokumentacji i składaniu wniosków.",
},
},
],
};
return (
<main>
<>
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(faqPompyJsonLd) }} />
<main>
<HeroSection
title="Pompy ciepła — niższe rachunki, niezależność, dofinansowanie"
subtitle="Zainwestuj w nowoczesne i ekologiczne ogrzewanie domu. Pomagamy na każdym kroku: od audytu, przez uzyskanie dotacji, aż po montaż."
@@ -63,5 +112,6 @@ export default function PompyCieplaPage() {
ctaLink="/kontakt"
/>
</main>
</>
);
}
+3 -3
View File
@@ -36,7 +36,7 @@ export default function sitemap(): MetadataRoute.Sitemap {
url: `${baseUrl}/sklep/${product.slug}`,
lastModified: new Date().toISOString(),
changeFrequency: 'weekly' as const,
priority: 0.9,
priority: 0.8,
}));
// Dynamiczne artykuły na blogu
@@ -45,7 +45,7 @@ export default function sitemap(): MetadataRoute.Sitemap {
url: `${baseUrl}/blog/${post.slug}`,
lastModified: post.date, // Używamy daty publikacji jako lastModified
changeFrequency: 'monthly' as const,
priority: 0.7,
priority: 0.6,
}));
// Dynamiczne strony miast
@@ -53,7 +53,7 @@ export default function sitemap(): MetadataRoute.Sitemap {
url: `${baseUrl}/klimatyzacja/${city.slug}`,
lastModified: new Date().toISOString(),
changeFrequency: 'monthly' as const,
priority: 0.8,
priority: 0.7,
}));
return [...staticRoutes, ...productRoutes, ...postRoutes, ...cityRoutes];
+28 -102
View File
@@ -1,11 +1,8 @@
import { Metadata } from 'next';
import { notFound } from 'next/navigation';
import Image from 'next/image';
import Link from 'next/link';
import { Phone, ArrowLeft, ShoppingBag } from 'lucide-react';
import { getProductBySlug, getProducts } from '@/content/products';
import { CtaBanner } from '@/components/sections/CtaBanner';
import { companyDetails } from '@/content/company';
import { ProductPageClient } from '@/components/shop/ProductPageClient';
interface Props {
params: Promise<{ slug: string }>;
@@ -31,104 +28,33 @@ export default async function ProductPage({ params }: Props) {
const product = getProductBySlug(slug);
if (!product) notFound();
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'https://thermcool.pl';
const productJsonLd = product.price ? {
"@context": "https://schema.org",
"@type": "Product",
"name": product.name,
"description": product.shortDescription,
"sku": product.sku,
"brand": { "@type": "Brand", "name": product.brand },
"image": product.image.startsWith('http') ? product.image : `${baseUrl}${product.image}`,
"offers": {
"@type": "Offer",
"url": `${baseUrl}/sklep/${product.slug}`,
"priceCurrency": "PLN",
"price": product.price,
"priceValidUntil": "2027-12-31",
"availability": "https://schema.org/InStock",
"seller": { "@type": "Organization", "name": "ThermCool" },
},
} : null;
return (
<main className="bg-slate-50 min-h-screen">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Breadcrumb */}
<Link href="/sklep" className="inline-flex items-center gap-2 text-sm text-text-muted hover:text-accent transition-colors mb-6">
<ArrowLeft className="size-4" />
Wróć do sklepu
</Link>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
{/* Zdjęcie */}
<div className="relative aspect-square bg-white rounded-2xl border border-border overflow-hidden">
<Image
src={product.image}
alt={product.name}
fill
className="object-contain p-8"
priority
unoptimized={product.image.startsWith('http')}
/>
</div>
{/* Info */}
<div className="flex flex-col gap-6">
<div>
<span className="text-sm font-semibold text-accent uppercase tracking-wider">{product.brand}</span>
<h1 className="text-2xl md:text-3xl font-bold text-primary font-heading mt-1 leading-tight">
{product.name}
</h1>
<p className="text-xs text-text-muted mt-1">SKU: {product.sku}</p>
</div>
{product.shortDescription && (
<p className="text-text-muted leading-relaxed">{product.shortDescription}</p>
)}
{/* Cena / CTA */}
<div className="p-5 rounded-xl bg-white border border-border">
{product.price !== null ? (
<div>
<p className="text-3xl font-bold text-primary font-heading">
{product.price.toLocaleString('pl-PL')} zł
</p>
<p className="text-sm text-text-muted mt-1">cena urządzenia brutto (23% VAT)</p>
<p className="text-xs text-text-muted mt-1">Montaż wyceniamy indywidualnie — zadzwoń lub napisz</p>
<div className="flex flex-col sm:flex-row gap-3 mt-4">
<a href={`tel:${companyDetails.phoneRaw}`} className="btn btn-primary font-bold text-white flex items-center gap-2 justify-center">
<Phone className="size-4" />
Zadzwoń i zamów
</a>
<Link href="/kontakt" className="btn btn-outline btn-primary bg-white font-bold flex items-center gap-2 justify-center">
<ShoppingBag className="size-4" />
Zapytaj o montaż
</Link>
</div>
</div>
) : (
<div>
<p className="text-lg font-semibold text-primary font-heading">Cena na zapytanie</p>
<p className="text-sm text-text-muted mt-1">Multisplit — cena zależy od wybranych jednostek wewnętrznych i konfiguracji instalacji.</p>
<a href={`tel:${companyDetails.phoneRaw}`} className="btn btn-primary font-bold text-white flex items-center gap-2 mt-4">
<Phone className="size-4" />
Zapytaj o wycenę
</a>
</div>
)}
</div>
{/* Specyfikacja */}
{Object.keys(product.specs).length > 0 && (
<div>
<h2 className="text-base font-bold text-primary font-heading mb-3">Specyfikacja</h2>
<div className="rounded-xl border border-border overflow-hidden">
<table className="w-full text-sm">
<tbody>
{Object.entries(product.specs).map(([key, val], idx) => (
<tr key={key} className={idx % 2 === 0 ? 'bg-white' : 'bg-slate-50'}>
<td className="px-4 py-2.5 text-text-muted font-medium w-1/2">{key}</td>
<td className="px-4 py-2.5 text-primary">{val}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
</div>
</div>
</div>
<CtaBanner
title="Potrzebujesz montażu?"
subtitle="Sprzedajemy z montażem na terenie Dolnego Śląska. Zadzwoń i ustal termin."
ctaText="Umów montaż"
ctaLink="/kontakt"
/>
</main>
<>
{productJsonLd && (
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(productJsonLd) }} />
)}
<ProductPageClient product={product} companyPhone={companyDetails.phone} companyPhoneRaw={companyDetails.phoneRaw} />
</>
);
}
+15 -2
View File
@@ -9,7 +9,15 @@ export const metadata: Metadata = {
alternates: { canonical: '/sklep' },
};
export default function ShopPage() {
interface Props {
searchParams: Promise<{ search?: string; brand?: string }>;
}
export default async function ShopPage({ searchParams }: Props) {
const params = await searchParams;
const initialSearch = params.search || '';
const initialBrand = params.brand || '';
const products = getProducts();
const brands = getBrands();
const splitCount = products.filter(p => p.category === 'scienne').length;
@@ -27,7 +35,12 @@ export default function ShopPage() {
secondaryCtaLink={undefined}
backgroundImage="https://images.unsplash.com/photo-1621905251189-08b45d6a269e?w=1600&q=80"
/>
<ProductsGrid products={products} brands={brands} />
<ProductsGrid
products={products}
brands={brands}
initialSearch={initialSearch}
initialBrand={initialBrand}
/>
</main>
);
}