feat: SEO cleanup, A11y, Next.js 16 dynamic routes, hide unverified content

This commit is contained in:
MichalC
2026-08-01 13:01:08 +02:00
parent 88cb4c516d
commit f07f2c2407
15 changed files with 59 additions and 398 deletions
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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">
+4
View File
@@ -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
View File
@@ -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];
} }
+2 -5
View File
@@ -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}
+3 -3
View File
@@ -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();
+8
View File
@@ -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
View File
@@ -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" },
],
}; };
-134
View File
@@ -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));
}
-41
View File
@@ -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');
-156
View File
@@ -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));
}