feat: SEO cleanup, A11y, Next.js 16 dynamic routes, hide unverified content
This commit is contained in:
@@ -8,9 +8,9 @@ import { Calendar, User, ArrowLeft, ArrowRight } from 'lucide-react';
|
|||||||
import { companyDetails } from '@/content/company';
|
import { companyDetails } from '@/content/company';
|
||||||
|
|
||||||
interface BlogPostPageProps {
|
interface BlogPostPageProps {
|
||||||
params: {
|
params: Promise<{
|
||||||
slug: string;
|
slug: string;
|
||||||
};
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generowanie ścieżek statycznych dla wpisów
|
// Generowanie ścieżek statycznych dla wpisów
|
||||||
|
|||||||
+17
-3
@@ -11,9 +11,9 @@
|
|||||||
--color-foreground: var(--foreground);
|
--color-foreground: var(--foreground);
|
||||||
--color-text: var(--foreground);
|
--color-text: var(--foreground);
|
||||||
--color-text-muted: var(--muted-foreground);
|
--color-text-muted: var(--muted-foreground);
|
||||||
--font-sans: var(--font-sans);
|
--font-sans: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||||
--font-mono: var(--font-geist-mono);
|
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
--font-heading: var(--font-heading);
|
--font-heading: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||||
--color-sidebar-ring: var(--sidebar-ring);
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
--color-sidebar-border: var(--sidebar-border);
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
@@ -158,6 +158,20 @@
|
|||||||
@apply border-border outline-ring/50;
|
@apply border-border outline-ring/50;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
*:focus-visible {
|
||||||
|
outline: 2px solid var(--accent);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
*, ::before, ::after {
|
||||||
|
animation-duration: 0.01ms !important;
|
||||||
|
animation-iteration-count: 1 !important;
|
||||||
|
transition-duration: 0.01ms !important;
|
||||||
|
scroll-behavior: auto !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground overflow-x-hidden;
|
@apply bg-background text-foreground overflow-x-hidden;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|||||||
return {
|
return {
|
||||||
title: district.seoTitle,
|
title: district.seoTitle,
|
||||||
description: district.seoDescription,
|
description: district.seoDescription,
|
||||||
|
robots: {
|
||||||
|
index: false,
|
||||||
|
follow: false,
|
||||||
|
},
|
||||||
alternates: { canonical: `/klimatyzacja/wroclaw/${district.slug}` },
|
alternates: { canonical: `/klimatyzacja/wroclaw/${district.slug}` },
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: district.seoTitle,
|
title: district.seoTitle,
|
||||||
|
|||||||
+6
-14
@@ -1,5 +1,5 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { Inter, Poppins } from "next/font/google";
|
|
||||||
import { Header } from "@/components/layout/Header";
|
import { Header } from "@/components/layout/Header";
|
||||||
import { TrustBar } from "@/components/layout/TrustBar";
|
import { TrustBar } from "@/components/layout/TrustBar";
|
||||||
import { Footer } from "@/components/layout/Footer";
|
import { Footer } from "@/components/layout/Footer";
|
||||||
@@ -10,16 +10,7 @@ import { AnalyticsWrapper } from "@/components/ui/AnalyticsWrapper";
|
|||||||
import { CookieSettingsFloatingIcon } from "@/components/ui/CookieSettingsFloatingIcon";
|
import { CookieSettingsFloatingIcon } from "@/components/ui/CookieSettingsFloatingIcon";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
|
||||||
const fontSans = Inter({
|
|
||||||
subsets: ["latin"],
|
|
||||||
variable: "--font-sans",
|
|
||||||
});
|
|
||||||
|
|
||||||
const fontHeading = Poppins({
|
|
||||||
subsets: ["latin"],
|
|
||||||
weight: ["600", "700", "800"],
|
|
||||||
variable: "--font-heading",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: {
|
title: {
|
||||||
@@ -61,13 +52,14 @@ export default function RootLayout({
|
|||||||
return (
|
return (
|
||||||
<html
|
<html
|
||||||
lang="pl"
|
lang="pl"
|
||||||
className={`${fontSans.variable} ${fontHeading.variable} antialiased`}
|
className="antialiased"
|
||||||
>
|
>
|
||||||
<head>
|
<head>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
|
||||||
</head>
|
</head>
|
||||||
<body className="min-h-screen flex flex-col bg-background text-foreground">
|
<body className="min-h-screen flex flex-col bg-background text-foreground">
|
||||||
|
<a href="#main-content" className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-[9999] focus:px-4 focus:py-2 focus:bg-accent focus:text-white focus:rounded-md focus:font-bold">
|
||||||
|
Przejdź do głównej treści
|
||||||
|
</a>
|
||||||
{process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY && (
|
{process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY && (
|
||||||
<Script
|
<Script
|
||||||
src={`https://www.google.com/recaptcha/api.js?render=${process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY}`}
|
src={`https://www.google.com/recaptcha/api.js?render=${process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY}`}
|
||||||
@@ -75,7 +67,7 @@ export default function RootLayout({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Header />
|
<Header />
|
||||||
<main className="flex-grow">{children}</main>
|
<main id="main-content" tabIndex={-1} className="flex-grow focus:outline-none">{children}</main>
|
||||||
<TrustBar />
|
<TrustBar />
|
||||||
<Footer />
|
<Footer />
|
||||||
<AnalyticsWrapper />
|
<AnalyticsWrapper />
|
||||||
|
|||||||
+3
-6
@@ -1,10 +1,7 @@
|
|||||||
import { Metadata } from 'next';
|
import { Metadata } from 'next';
|
||||||
import { HeroSection } from '@/components/sections/HeroSection';
|
import { HeroSection } from '@/components/sections/HeroSection';
|
||||||
import { AboutStory } from '@/components/sections/AboutStory';
|
import { AboutStory } from '@/components/sections/AboutStory';
|
||||||
import { StatsBar } from '@/components/sections/StatsBar';
|
|
||||||
import { ValuesGrid } from '@/components/sections/ValuesGrid';
|
import { ValuesGrid } from '@/components/sections/ValuesGrid';
|
||||||
import { CertificatesSection } from '@/components/sections/CertificatesSection';
|
|
||||||
import { Testimonials } from '@/components/sections/Testimonials';
|
|
||||||
import { CtaBanner } from '@/components/sections/CtaBanner';
|
import { CtaBanner } from '@/components/sections/CtaBanner';
|
||||||
import { companyDetails } from '@/content/company';
|
import { companyDetails } from '@/content/company';
|
||||||
|
|
||||||
@@ -32,13 +29,13 @@ export default function ONasPage() {
|
|||||||
|
|
||||||
<AboutStory />
|
<AboutStory />
|
||||||
|
|
||||||
<StatsBar />
|
{/* <StatsBar /> (Ukryte na czas zbierania autentycznych danych statystycznych) */}
|
||||||
|
|
||||||
<ValuesGrid />
|
<ValuesGrid />
|
||||||
|
|
||||||
<CertificatesSection />
|
{/* <CertificatesSection /> (Ukryte na czas uzyskania skanów dokumentów) */}
|
||||||
|
|
||||||
<Testimonials />
|
{/* <Testimonials /> (Ukryte na czas zbierania autentycznych opinii) */}
|
||||||
|
|
||||||
<CtaBanner
|
<CtaBanner
|
||||||
title="Szukasz sprawdzonej ekipy do swojego projektu?"
|
title="Szukasz sprawdzonej ekipy do swojego projektu?"
|
||||||
|
|||||||
+4
-7
@@ -3,13 +3,13 @@ import { AdvantagesBar } from "@/components/sections/AdvantagesBar";
|
|||||||
import { ServicesGrid } from "@/components/sections/ServicesGrid";
|
import { ServicesGrid } from "@/components/sections/ServicesGrid";
|
||||||
import { WhyUs } from "@/components/sections/WhyUs";
|
import { WhyUs } from "@/components/sections/WhyUs";
|
||||||
import { FeaturedProducts } from "@/components/sections/FeaturedProducts";
|
import { FeaturedProducts } from "@/components/sections/FeaturedProducts";
|
||||||
import { Testimonials } from "@/components/sections/Testimonials";
|
|
||||||
import { BlogPreview } from "@/components/sections/BlogPreview";
|
import { BlogPreview } from "@/components/sections/BlogPreview";
|
||||||
import { CtaBanner } from "@/components/sections/CtaBanner";
|
import { CtaBanner } from "@/components/sections/CtaBanner";
|
||||||
import { companyDetails } from "@/content/company";
|
import { companyDetails } from "@/content/company";
|
||||||
import { Wrench, Phone, ArrowRight, Thermometer, Wind, Zap, Calendar, Check } from "lucide-react";
|
import { Wrench, Phone, ArrowRight, Thermometer, Wind, Zap, Calendar, Check } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Button, buttonClasses } from "@/components/ui/Button";
|
import { Button, buttonClasses } from "@/components/ui/Button";
|
||||||
|
import { JsonLd } from "@/components/ui/JsonLd";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'https://thermcool.pl';
|
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'https://thermcool.pl';
|
||||||
@@ -104,7 +104,7 @@ export default function Home() {
|
|||||||
companyDetails.socials.facebook,
|
companyDetails.socials.facebook,
|
||||||
companyDetails.socials.instagram,
|
companyDetails.socials.instagram,
|
||||||
companyDetails.socials.google,
|
companyDetails.socials.google,
|
||||||
],
|
].filter(Boolean),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"@type": "WebSite",
|
"@type": "WebSite",
|
||||||
@@ -129,10 +129,7 @@ export default function Home() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* LocalBusiness Schema for Google Search SEO */}
|
{/* LocalBusiness Schema for Google Search SEO */}
|
||||||
<script
|
<JsonLd data={jsonLd} />
|
||||||
type="application/ld+json"
|
|
||||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<HeroSection />
|
<HeroSection />
|
||||||
<AdvantagesBar />
|
<AdvantagesBar />
|
||||||
@@ -141,7 +138,7 @@ export default function Home() {
|
|||||||
<FeaturedProducts />
|
<FeaturedProducts />
|
||||||
<WhyUs />
|
<WhyUs />
|
||||||
{/* ProjectsSection will go here in Group B */}
|
{/* ProjectsSection will go here in Group B */}
|
||||||
<Testimonials />
|
{/* <Testimonials /> (Ukryte na czas zbierania autentycznych opinii) */}
|
||||||
|
|
||||||
{/* Serwis LG */}
|
{/* Serwis LG */}
|
||||||
<section className="py-16 bg-white">
|
<section className="py-16 bg-white">
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ export const metadata: Metadata = {
|
|||||||
description: 'Zobacz nasze wybrane realizacje z zakresu klimatyzacji, pomp ciepła, remontów i usług hydraulicznych.',
|
description: 'Zobacz nasze wybrane realizacje z zakresu klimatyzacji, pomp ciepła, remontów i usług hydraulicznych.',
|
||||||
alternates: {
|
alternates: {
|
||||||
canonical: '/realizacje',
|
canonical: '/realizacje',
|
||||||
|
},
|
||||||
|
robots: {
|
||||||
|
index: false,
|
||||||
|
follow: false,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+1
-12
@@ -2,7 +2,6 @@ import { MetadataRoute } from 'next';
|
|||||||
import { getProducts } from '@/content/products';
|
import { getProducts } from '@/content/products';
|
||||||
import { getPosts } from '@/content/posts';
|
import { getPosts } from '@/content/posts';
|
||||||
import { dolnySlaskCities } from '@/content/cities';
|
import { dolnySlaskCities } from '@/content/cities';
|
||||||
import { wroclawDistricts } from '@/content/wroclaw-districts';
|
|
||||||
export default function sitemap(): MetadataRoute.Sitemap {
|
export default function sitemap(): MetadataRoute.Sitemap {
|
||||||
const baseUrl = 'https://thermcool.pl';
|
const baseUrl = 'https://thermcool.pl';
|
||||||
|
|
||||||
@@ -11,8 +10,6 @@ export default function sitemap(): MetadataRoute.Sitemap {
|
|||||||
'',
|
'',
|
||||||
'/klimatyzacja',
|
'/klimatyzacja',
|
||||||
'/pompy-ciepla',
|
'/pompy-ciepla',
|
||||||
|
|
||||||
'/realizacje',
|
|
||||||
'/blog',
|
'/blog',
|
||||||
'/o-nas',
|
'/o-nas',
|
||||||
'/kalkulator',
|
'/kalkulator',
|
||||||
@@ -68,13 +65,5 @@ export default function sitemap(): MetadataRoute.Sitemap {
|
|||||||
priority: 0.7,
|
priority: 0.7,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Dynamiczne strony dzielnic
|
return [...staticRoutes, ...productRoutes, ...postRoutes, ...cityRoutes];
|
||||||
const districtUrls = wroclawDistricts.map((d) => ({
|
|
||||||
url: `${baseUrl}/klimatyzacja/wroclaw/${d.slug}`,
|
|
||||||
lastModified: new Date().toISOString(),
|
|
||||||
changeFrequency: 'monthly' as const,
|
|
||||||
priority: 0.7,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return [...staticRoutes, ...productRoutes, ...postRoutes, ...cityRoutes, ...districtUrls];
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { getProductBySlug, getProducts } from '@/content/products';
|
|||||||
import { companyDetails } from '@/content/company';
|
import { companyDetails } from '@/content/company';
|
||||||
import { ProductPageClient } from '@/components/shop/ProductPageClient';
|
import { ProductPageClient } from '@/components/shop/ProductPageClient';
|
||||||
import { getProductVariants } from '@/content/product-variants';
|
import { getProductVariants } from '@/content/product-variants';
|
||||||
|
import { JsonLd } from '@/components/ui/JsonLd';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
params: Promise<{ slug: string }>;
|
params: Promise<{ slug: string }>;
|
||||||
@@ -51,17 +52,13 @@ export default async function ProductPage({ params }: Props) {
|
|||||||
"url": `${baseUrl}/sklep/${product.slug}`,
|
"url": `${baseUrl}/sklep/${product.slug}`,
|
||||||
"priceCurrency": "PLN",
|
"priceCurrency": "PLN",
|
||||||
"price": product.price,
|
"price": product.price,
|
||||||
"priceValidUntil": "2027-12-31",
|
|
||||||
"availability": "https://schema.org/InStock",
|
|
||||||
"seller": { "@type": "Organization", "name": "ThermCool" },
|
"seller": { "@type": "Organization", "name": "ThermCool" },
|
||||||
},
|
},
|
||||||
} : null;
|
} : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{productJsonLd && (
|
{productJsonLd && <JsonLd data={productJsonLd} />}
|
||||||
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(productJsonLd) }} />
|
|
||||||
)}
|
|
||||||
<ProductPageClient
|
<ProductPageClient
|
||||||
product={product}
|
product={product}
|
||||||
seriesProducts={seriesProducts}
|
seriesProducts={seriesProducts}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { ArrowLeft, Phone, Zap, Wind, Volume2, Thermometer } from 'lucide-react'
|
|||||||
import MultisplitContactForm from '@/components/shop/MultisplitContactForm';
|
import MultisplitContactForm from '@/components/shop/MultisplitContactForm';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
params: { slug: string };
|
params: Promise<{ slug: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateStaticParams() {
|
export async function generateStaticParams() {
|
||||||
@@ -18,7 +18,7 @@ export async function generateStaticParams() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
||||||
const { slug } = params;
|
const { slug } = await params;
|
||||||
const product = getProductBySlug(slug);
|
const product = getProductBySlug(slug);
|
||||||
if (!product) return { title: 'Produkt nie znaleziony' };
|
if (!product) return { title: 'Produkt nie znaleziony' };
|
||||||
return {
|
return {
|
||||||
@@ -28,7 +28,7 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default async function MultisplitInquiryPage({ params }: Props) {
|
export default async function MultisplitInquiryPage({ params }: Props) {
|
||||||
const { slug } = params;
|
const { slug } = await params;
|
||||||
const product = getProductBySlug(slug);
|
const product = getProductBySlug(slug);
|
||||||
if (!product || product.category !== 'multisplit') notFound();
|
if (!product || product.category !== 'multisplit') notFound();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
export function JsonLd({ data }: { data: Record<string, unknown> }) {
|
||||||
|
return (
|
||||||
|
<script
|
||||||
|
type="application/ld+json"
|
||||||
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
+5
-15
@@ -15,20 +15,10 @@ export const companyDetails = {
|
|||||||
},
|
},
|
||||||
hours: "Pon - Pt: 8:00 - 18:00, Sob: 9:00 - 14:00",
|
hours: "Pon - Pt: 8:00 - 18:00, Sob: 9:00 - 14:00",
|
||||||
socials: {
|
socials: {
|
||||||
facebook: "https://facebook.com",
|
facebook: "",
|
||||||
instagram: "https://instagram.com",
|
instagram: "",
|
||||||
google: "https://google.com/maps",
|
google: "",
|
||||||
},
|
},
|
||||||
stats: [
|
stats: [] as Array<{ label: string; value: string; number: number }>,
|
||||||
{ label: "Lat doświadczenia", value: "15+", number: 15 },
|
certificates: [] as Array<{ name: string; image: string }>,
|
||||||
{ label: "Ukończonych realizacji", value: "3500+", number: 3500 },
|
|
||||||
{ label: "Zadowolonych klientów", value: "99%", number: 99 },
|
|
||||||
{ label: "Obsługiwanych miast", value: "12+", number: 12 },
|
|
||||||
],
|
|
||||||
certificates: [
|
|
||||||
{ name: "Certyfikat F-Gaz dla przedsiębiorcy", image: "/placeholder-fgaz.png" },
|
|
||||||
{ name: "Autoryzowany Instalator Daikin", image: "/placeholder-daikin.png" },
|
|
||||||
{ name: "Autoryzowany Instalator Mitsubishi Electric", image: "/placeholder-mitsubishi.png" },
|
|
||||||
{ name: "Certyfikat Rzetelna Firma", image: "/placeholder-rzetelna.png" },
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,134 +0,0 @@
|
|||||||
/* eslint-disable */
|
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
const directories = ['app', 'components'];
|
|
||||||
const patterns = [
|
|
||||||
{
|
|
||||||
regex: /className=["']flex items-center gap-2 px-6 py-3 bg-accent text-white rounded-xl font-bold hover:bg-accent\/90 transition-colors["']/g,
|
|
||||||
replace: "className={buttonClasses({ variant: 'primary', size: 'md' })}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
regex: /className=["']flex items-center gap-2 px-6 py-3 bg-white\/10 text-white rounded-xl font-bold hover:bg-white\/20 transition-colors border border-white\/20["']/g,
|
|
||||||
replace: "className={buttonClasses({ variant: 'outline', size: 'md' })}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
regex: /className=["']flex items-center gap-2 px-8 py-4 bg-accent text-white rounded-xl font-bold text-lg hover:bg-accent\/90 transition-colors["']/g,
|
|
||||||
replace: "className={buttonClasses({ variant: 'primary', size: 'lg' })}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
regex: /className=["']flex items-center gap-2 px-8 py-4 bg-white\/10 text-white rounded-xl font-bold text-lg hover:bg-white\/20 transition-colors border border-white\/20["']/g,
|
|
||||||
replace: "className={buttonClasses({ variant: 'outline', size: 'lg' })}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
regex: /className=["']btn btn-primary text-white font-bold["']/g,
|
|
||||||
replace: "className={buttonClasses({ variant: 'primary', size: 'md' })}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
regex: /className=["']btn btn-primary font-bold["']/g,
|
|
||||||
replace: "className={buttonClasses({ variant: 'primary', size: 'md' })}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
regex: /className=["']flex items-center gap-1\.5 px-4 py-2 bg-red-500 text-white rounded-\[var\(--radius-field\)\] font-bold text-sm hover:bg-red-600 transition-colors whitespace-nowrap["']/g,
|
|
||||||
replace: "className={buttonClasses({ variant: 'error', size: 'sm' })}"
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
// In case there are some dynamic classNames (template literals):
|
|
||||||
const dynamicPatterns = [
|
|
||||||
{
|
|
||||||
regex: /className=\{`flex items-center gap-2 px-6 py-3 bg-accent text-white rounded-xl font-bold hover:bg-accent\/90 transition-colors (.*?)`\}/g,
|
|
||||||
replace: "className={buttonClasses({ variant: 'primary', size: 'md', className: `$1` })}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
regex: /className=\{`btn btn-primary font-bold (.*?)`\}/g,
|
|
||||||
replace: "className={buttonClasses({ variant: 'primary', size: 'md', className: `$1` })}"
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
function processDirectory(dir) {
|
|
||||||
const files = fs.readdirSync(dir);
|
|
||||||
|
|
||||||
for (const file of files) {
|
|
||||||
const fullPath = path.join(dir, file);
|
|
||||||
const stat = fs.statSync(fullPath);
|
|
||||||
|
|
||||||
if (stat.isDirectory()) {
|
|
||||||
processDirectory(fullPath);
|
|
||||||
} else if (file.endsWith('.tsx') || file.endsWith('.jsx')) {
|
|
||||||
// Don't process the Button.tsx itself
|
|
||||||
if (fullPath.includes('Button.tsx')) continue;
|
|
||||||
|
|
||||||
let content = fs.readFileSync(fullPath, 'utf8');
|
|
||||||
let originalContent = content;
|
|
||||||
|
|
||||||
let changed = false;
|
|
||||||
let usedButtonClasses = false;
|
|
||||||
let usedButtonComponent = false;
|
|
||||||
|
|
||||||
// 1. Tag conversion: <button className={...} ...> to <Button ...>
|
|
||||||
// We'll first apply the className replacements
|
|
||||||
for (const p of patterns) {
|
|
||||||
if (p.regex.test(content)) {
|
|
||||||
content = content.replace(p.regex, p.replace);
|
|
||||||
changed = true;
|
|
||||||
usedButtonClasses = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const p of dynamicPatterns) {
|
|
||||||
if (p.regex.test(content)) {
|
|
||||||
content = content.replace(p.regex, p.replace);
|
|
||||||
changed = true;
|
|
||||||
usedButtonClasses = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Convert `<button className={buttonClasses({ variant: 'X', size: 'Y' })}>` to `<Button variant="X" size="Y">`
|
|
||||||
// This is a bit tricky with regex, but let's try safely:
|
|
||||||
// Pattern: <button (.*?)className=\{buttonClasses\(\{ variant: '([^']+)', size: '([^']+)' \}\)\}(.*?)>
|
|
||||||
const buttonToComponentRegex = /<button([^>]*)className=\{buttonClasses\(\{\s*variant:\s*'([^']+)',\s*size:\s*'([^']+)'\s*\}\)\}([^>]*)>/g;
|
|
||||||
if (buttonToComponentRegex.test(content)) {
|
|
||||||
content = content.replace(buttonToComponentRegex, '<Button$1variant="$2" size="$3"$4>');
|
|
||||||
usedButtonComponent = true;
|
|
||||||
}
|
|
||||||
// Also match closing tags
|
|
||||||
if (usedButtonComponent) {
|
|
||||||
content = content.replace(/<\/button>/g, '</Button>');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (changed) {
|
|
||||||
// We still might have used buttonClasses for <Link> tags
|
|
||||||
const needsButtonClasses = content.includes('buttonClasses(');
|
|
||||||
|
|
||||||
// Inject imports
|
|
||||||
const imports = [];
|
|
||||||
if (needsButtonClasses) imports.push('buttonClasses');
|
|
||||||
if (usedButtonComponent) imports.push('Button');
|
|
||||||
|
|
||||||
if (imports.length > 0) {
|
|
||||||
const importStr = `import { ${imports.join(', ')} } from '@/components/ui/Button';\n`;
|
|
||||||
// Find the last import
|
|
||||||
const importRegex = /^import.*$/gm;
|
|
||||||
let match;
|
|
||||||
let lastImportIndex = -1;
|
|
||||||
while ((match = importRegex.exec(content)) !== null) {
|
|
||||||
lastImportIndex = match.index + match[0].length;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lastImportIndex !== -1) {
|
|
||||||
content = content.slice(0, lastImportIndex) + '\n' + importStr + content.slice(lastImportIndex);
|
|
||||||
} else {
|
|
||||||
content = importStr + content;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.writeFileSync(fullPath, content, 'utf8');
|
|
||||||
console.log(`Updated ${fullPath}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const dir of directories) {
|
|
||||||
processDirectory(path.join(__dirname, dir));
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
const filePaths = [
|
|
||||||
'components/sections/PostsGrid.tsx',
|
|
||||||
'components/sections/FeaturedPost.tsx',
|
|
||||||
'components/sections/AboutStory.tsx',
|
|
||||||
'components/layout/GlobalSearch.tsx',
|
|
||||||
'components/shop/ProductGallery.tsx',
|
|
||||||
'app/kontakt/page.tsx',
|
|
||||||
'app/realizacje/page.tsx',
|
|
||||||
'app/blog/page.tsx',
|
|
||||||
'app/blog/[slug]/page.tsx',
|
|
||||||
'app/o-nas/page.tsx'
|
|
||||||
];
|
|
||||||
|
|
||||||
filePaths.forEach(filePath => {
|
|
||||||
const absolutePath = path.join(process.cwd(), filePath);
|
|
||||||
if (!fs.existsSync(absolutePath)) return;
|
|
||||||
|
|
||||||
let content = fs.readFileSync(absolutePath, 'utf8');
|
|
||||||
|
|
||||||
// about story
|
|
||||||
content = content.replace(/https:\/\/images\.unsplash\.com\/photo-1621905251189-08b45d6a269e[^"']*/g, '/images/about/about-story.webp');
|
|
||||||
|
|
||||||
// product gallery and search
|
|
||||||
content = content.replace(/https:\/\/images\.unsplash\.com\/photo-1581094794329-c8112a89af12[^"']*/g, '/images/hero/shop-section-image.webp');
|
|
||||||
content = content.replace(/https:\/\/images\.unsplash\.com\/photo-1584622650111-993a426fbf0a[^"']*/g, '/images/hero/shop-section-image.webp');
|
|
||||||
|
|
||||||
// blog post placeholders
|
|
||||||
content = content.replace(/https:\/\/images\.unsplash\.com\/photo-1516321318423-f06f85e504b3[^"']*/g, '/images/hero/hero-section-image.webp');
|
|
||||||
|
|
||||||
// backgrounds
|
|
||||||
content = content.replace(/https:\/\/images\.unsplash\.com\/photo-1516387938699-a93567ec168e[^"']*/g, '/images/hero/hero-section-image.webp');
|
|
||||||
content = content.replace(/https:\/\/images\.unsplash\.com\/photo-1503387762-592deb58ef4e[^"']*/g, '/images/hero/hero-section-image.webp');
|
|
||||||
content = content.replace(/https:\/\/images\.unsplash\.com\/photo-1504307651254-35680f356f12[^"']*/g, '/images/about/about-story.webp');
|
|
||||||
|
|
||||||
fs.writeFileSync(absolutePath, content, 'utf8');
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Replaced unsplash links');
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
/* eslint-disable */
|
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
const mapping = {
|
|
||||||
'img_6a5d04a21372a8.20397387.webp': 'klimatyzacja-wroclaw-kiedy-montowac',
|
|
||||||
'img_6a5d04a231dac2.11355832.webp': 'cena-klimatyzacji-do-pokoju',
|
|
||||||
'img_6a5d04a25e4b98.52144832.webp': 'klimatyzacja-a-jakos-powietrza',
|
|
||||||
'img_6a5d04a27a2a42.56795579.webp': 'montaz-klimatyzacji-w-bloku',
|
|
||||||
'img_6a5d04a2a89295.39636622.webp': 'czynnik-chlodniczy-r32-r410a',
|
|
||||||
'img_6a5d04a2ce3596.67627277.webp': 'jaki-klimatyzator-do-salonu-30-m2',
|
|
||||||
'img_6a5d04a30d2c52.62595133.webp': 'klimatyzacja-w-biurze-co-warto-wiedziec',
|
|
||||||
'img_6a5d04a32a8e24.64727858.webp': 'pompa-ciepla-powietrze-woda-jak-dziala',
|
|
||||||
'img_6a5d04a354c369.35471968.webp': 'klimatyzacja-dla-alergikow',
|
|
||||||
'img_6a5d04a3772ec5.39684638.webp': 'serwis-klimatyzacji-samemu-co-mozna',
|
|
||||||
'img_6a5d04a3a0ad91.92196165.webp': 'klimatyzator-split-czy-multisplit',
|
|
||||||
'img_6a5d04a3beba26.98450615.webp': 'jak-dobrac-moc-klimatyzatora',
|
|
||||||
'img_6a5d04a3ec8671.37520426.webp': 'pompa-ciepla-vs-gaz-co-wybrac',
|
|
||||||
'img_6a5d04a41b3d33.17571060.webp': 'klimatyzacja-jako-ogrzewanie-zimowe',
|
|
||||||
'img_6a5d04a441d4a9.95117803.webp': 'jaki-klimatyzator-do-pokoju-20-m2',
|
|
||||||
'img_6a5d04a464d615.75587080.webp': 'jaki-klimatyzator-wybrac-do-mieszkania',
|
|
||||||
'img_6a5d04be52b442.44314731.webp': 'ranking-marek-klimatyzacji',
|
|
||||||
'img_6a5d04be6fdb26.32043683.webp': 'gdzie-zamontowac-jednostke-zewnetrzna-klimatyzacji',
|
|
||||||
'img_6a5d04beb3c1f3.98136765.webp': 'co-sprawdzic-przed-montazem-klimatyzacji',
|
|
||||||
'img_6a5d04bed08665.86887655.webp': 'gdzie-nie-montowac-klimatyzacji-w-domu',
|
|
||||||
'img_6a5d04bf25e550.89462427.webp': 'kiedy-najlepiej-zainstalowac-klimatyzacje-w-domu',
|
|
||||||
'img_6a5d04bf45cf49.82956577.webp': 'jak-dziala-inwenter-klimatyzator',
|
|
||||||
'img_6a5d04bf71f7e2.14009785.webp': 'gwarancja-na-montaz-klimatyzacji',
|
|
||||||
'img_6a5d04bfa28c35.65238690.webp': 'klimatyzacja-vs-wentylacja-roznice',
|
|
||||||
'img_6a5d04bfc11633.69161886.webp': 'pompa-ciepla-ile-kosztuje-eksploatacja',
|
|
||||||
'img_6a5d04bfef7be1.18184702.webp': 'halas-klimatyzatora-jak-zmierzyc',
|
|
||||||
'img_6a5d04c02261f0.29970552.webp': 'serwis-klimatyzacji-co-i-kiedy',
|
|
||||||
'img_6a5d04c04b87f9.58656333.webp': 'klimatyzacja-walbrzych-ceny',
|
|
||||||
'img_6a5d04c073be83.21304683.webp': 'klimatyzacja-dom-jednorodzinny-system',
|
|
||||||
'img_6a5d04c09d68c3.59447568.webp': 'pompa-ciepla-fotowoltaika-synergia',
|
|
||||||
'img_6a5d04c0c8c1c6.48575540.webp': 'klimatyzacja-nowe-mieszkanie-deweloper',
|
|
||||||
'img_6a5d04c1067c12.79554682.webp': 'pompa-ciepla-stary-dom',
|
|
||||||
'img_6a5d04c12da0c8.50905305.webp': 'ile-kosztuje-godzina-pracy-technika-klimatyzacji',
|
|
||||||
'img_6a5d04c159bcd6.32914629.webp': 'ile-trwa-montaz-klimatyzacji',
|
|
||||||
'img_6a5d04e0bf2493.80472628.webp': 'jak-oszczedzac-na-klimatyzacji',
|
|
||||||
'img_6a5d04e0dd6821.27388481.webp': 'czy-mozna-zainstalowac-klimatyzator-samemu',
|
|
||||||
'img_6a5d04e1172f78.91797716.webp': 'halas-z-klimatyzacji-normy',
|
|
||||||
'img_6a5d04e1319575.94343123.webp': 'objawy-uszkodzonej-klimatyzacji',
|
|
||||||
'img_6a5d04e15a8ce7.38987392.webp': 'okres-gwarancji-na-klimatyzacje',
|
|
||||||
'img_6a5d04e175b984.69402532.webp': 'jak-wybrac-firme-do-montazu-klimatyzacji',
|
|
||||||
'img_6a5d04e1a97cf7.04175498.webp': 'czy-instalacja-klimatyzacji-wymaga-pozwolenia',
|
|
||||||
'img_6a5d04e1cb3269.84592063.webp': 'jak-czyscic-filtry-w-klimatyzacji',
|
|
||||||
'img_6a5d04e2003b93.75200105.webp': 'cena-montazu-klimatyzacji-w-biurze',
|
|
||||||
'img_6a5d04e2233561.05897034.webp': 'czy-klimatyzacja-bez-montazu-warta-inwestycji',
|
|
||||||
'img_6a5d04e25dba63.60274799.webp': 'czy-multisplit-oszczedza-prad',
|
|
||||||
'img_6a5d04e27f4308.72545903.webp': 'ile-kosztuje-montaz-klimatyzacji',
|
|
||||||
'img_6a5d04e2ac7600.19211261.webp': 'czy-mozna-montowac-klimatyzacje-nad-oknem',
|
|
||||||
'img_6a5d04e2ce4529.48716501.webp': 'oznaczenia-wydajnosci-klimatyzatorow',
|
|
||||||
'img_6a5d04e309b541.63791762.webp': 'split-czy-multisplit-ktora-klimatyzacja-lepsza',
|
|
||||||
'img_6a5d04e32f8533.44864660.webp': 'czy-klimatyzacja-multisplit-jest-oplacalna',
|
|
||||||
'img_6a5d04e35a77b6.58886578.webp': 'ile-trwa-demontaz-starej-klimatyzacji',
|
|
||||||
'img_6a5d04e385e4f5.96931089.webp': 'wymogi-elektryczne-do-klimatyzacji',
|
|
||||||
'img_6a5d04e3a4d1e7.98716793.webp': 'ile-kosztuje-montaz-klimatyzacji-w-domu',
|
|
||||||
'img_6a5d04e3ced574.69087334.webp': 'zasada-5-minut-w-montazu-klimatyzacji',
|
|
||||||
'img_6a5d04e4016424.10906606.webp': 'koszt-konsultacji-przed-montazem-klimatyzacji',
|
|
||||||
'img_6a5d04e4315737.26878013.webp': 'klimatyzacja-legnica-montaz',
|
|
||||||
'img_6a5d04e4523360.78733262.webp': 'jak-sprawdzic-czy-technicy-montuja-klimatyzacje-prawidlowo',
|
|
||||||
'img_6a5d04e47b4101.45444854.webp': 'czy-warto-remontowac-stara-klimatyzacje',
|
|
||||||
'img_6a5d04e4978d12.59948415.webp': 'klimatyzacja-a-zdrowie-mity-i-prawda',
|
|
||||||
'img_6a5d04e4c0ab90.56512395.webp': 'koszty-przedluzenia-rur-w-klimatyzacji',
|
|
||||||
'img_6a5d04e4e23ca4.33276359.webp': 'czy-trzeba-uziemic-klimatyzacje',
|
|
||||||
'img_6a5d04e518a696.65815784.webp': 'na-co-zwrocic-uwage-przy-wyborze-klimatyzatora',
|
|
||||||
'img_6a5d04e5377e23.67377862.webp': 'jak-chronic-parapet-przed-kondensacja-z-klimatyzacji'
|
|
||||||
};
|
|
||||||
|
|
||||||
const imagesDir = path.join(__dirname, 'public/images/blogs_images');
|
|
||||||
const postsPath = path.join(__dirname, 'content/posts.ts');
|
|
||||||
|
|
||||||
let postsContent = fs.readFileSync(postsPath, 'utf-8');
|
|
||||||
const missingFiles = [];
|
|
||||||
const missingSlugs = [];
|
|
||||||
const processedSlugs = new Set();
|
|
||||||
|
|
||||||
// Ensure the directory exists
|
|
||||||
if (!fs.existsSync(imagesDir)) {
|
|
||||||
console.log('Error: Directory ' + imagesDir + ' does not exist.');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [filename, slug] of Object.entries(mapping)) {
|
|
||||||
const oldPath = path.join(imagesDir, filename);
|
|
||||||
const newFilename = `${slug}.webp`;
|
|
||||||
const newPath = path.join(imagesDir, newFilename);
|
|
||||||
|
|
||||||
if (!fs.existsSync(oldPath) && !fs.existsSync(newPath)) {
|
|
||||||
missingFiles.push(`${filename} => ${slug}`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find slug in posts.ts
|
|
||||||
const slugRegex = new RegExp(`slug:\\s*['"]${slug}['"]`);
|
|
||||||
if (!slugRegex.test(postsContent)) {
|
|
||||||
missingSlugs.push(`${filename} => ${slug}`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rename file if not already renamed
|
|
||||||
if (fs.existsSync(oldPath)) {
|
|
||||||
fs.renameSync(oldPath, newPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update postsContent
|
|
||||||
// The structure is roughly:
|
|
||||||
// slug: 'slug-name',
|
|
||||||
// title: '...',
|
|
||||||
// category: '...',
|
|
||||||
// excerpt: '...',
|
|
||||||
// date: '...',
|
|
||||||
// author: '...',
|
|
||||||
// image: '',
|
|
||||||
// We need to replace image: '' with image: '/images/blogs_images/slug.webp' for this specific object.
|
|
||||||
// A safe way is to find the object block containing the slug, and replace the image field inside it.
|
|
||||||
|
|
||||||
// We'll split the content by objects or just use a replace with a function.
|
|
||||||
// A simple way is to find the index of the slug, then find the next `image: '',` and replace it.
|
|
||||||
const slugIndex = postsContent.indexOf(`slug: '${slug}'`);
|
|
||||||
if (slugIndex === -1) {
|
|
||||||
missingSlugs.push(`${filename} => ${slug}`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextImageIndex = postsContent.indexOf(`image: ''`, slugIndex);
|
|
||||||
if (nextImageIndex !== -1 && nextImageIndex < slugIndex + 1000) { // arbitrary limit to ensure we are in the same object
|
|
||||||
postsContent = postsContent.slice(0, nextImageIndex) + `image: '/images/blogs_images/${newFilename}'` + postsContent.slice(nextImageIndex + 9);
|
|
||||||
processedSlugs.add(slug);
|
|
||||||
} else {
|
|
||||||
// maybe it already has a value or spacing is different
|
|
||||||
const nextImageRegex = /image:\s*['"][^'"]*['"]/;
|
|
||||||
const substr = postsContent.substring(slugIndex, slugIndex + 1000);
|
|
||||||
const match = substr.match(nextImageRegex);
|
|
||||||
if (match) {
|
|
||||||
const matchIndex = slugIndex + match.index;
|
|
||||||
postsContent = postsContent.slice(0, matchIndex) + `image: '/images/blogs_images/${newFilename}'` + postsContent.slice(matchIndex + match[0].length);
|
|
||||||
processedSlugs.add(slug);
|
|
||||||
} else {
|
|
||||||
console.log('Could not find image field for slug: ' + slug);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.writeFileSync(postsPath, postsContent);
|
|
||||||
|
|
||||||
console.log('Successfully updated posts.ts and renamed images.');
|
|
||||||
if (missingFiles.length > 0) {
|
|
||||||
console.log('\\nBrakujące pliki w folderze public/images/blogs_images:');
|
|
||||||
missingFiles.forEach(f => console.log(f));
|
|
||||||
}
|
|
||||||
if (missingSlugs.length > 0) {
|
|
||||||
console.log('\\nSlugi usunięte z posts.ts lub nieistniejące:');
|
|
||||||
missingSlugs.forEach(s => console.log(s));
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user