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';
|
||||
|
||||
interface BlogPostPageProps {
|
||||
params: {
|
||||
params: Promise<{
|
||||
slug: string;
|
||||
};
|
||||
}>;
|
||||
}
|
||||
|
||||
// Generowanie ścieżek statycznych dla wpisów
|
||||
|
||||
+17
-3
@@ -11,9 +11,9 @@
|
||||
--color-foreground: var(--foreground);
|
||||
--color-text: var(--foreground);
|
||||
--color-text-muted: var(--muted-foreground);
|
||||
--font-sans: var(--font-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
--font-heading: var(--font-heading);
|
||||
--font-sans: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
--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-border: var(--sidebar-border);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
@@ -158,6 +158,20 @@
|
||||
@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 {
|
||||
@apply bg-background text-foreground overflow-x-hidden;
|
||||
}
|
||||
|
||||
@@ -28,6 +28,10 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
||||
return {
|
||||
title: district.seoTitle,
|
||||
description: district.seoDescription,
|
||||
robots: {
|
||||
index: false,
|
||||
follow: false,
|
||||
},
|
||||
alternates: { canonical: `/klimatyzacja/wroclaw/${district.slug}` },
|
||||
openGraph: {
|
||||
title: district.seoTitle,
|
||||
|
||||
+6
-14
@@ -1,5 +1,5 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Inter, Poppins } from "next/font/google";
|
||||
|
||||
import { Header } from "@/components/layout/Header";
|
||||
import { TrustBar } from "@/components/layout/TrustBar";
|
||||
import { Footer } from "@/components/layout/Footer";
|
||||
@@ -10,16 +10,7 @@ import { AnalyticsWrapper } from "@/components/ui/AnalyticsWrapper";
|
||||
import { CookieSettingsFloatingIcon } from "@/components/ui/CookieSettingsFloatingIcon";
|
||||
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 = {
|
||||
title: {
|
||||
@@ -61,13 +52,14 @@ export default function RootLayout({
|
||||
return (
|
||||
<html
|
||||
lang="pl"
|
||||
className={`${fontSans.variable} ${fontHeading.variable} antialiased`}
|
||||
className="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">
|
||||
<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 && (
|
||||
<Script
|
||||
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 />
|
||||
<main className="flex-grow">{children}</main>
|
||||
<main id="main-content" tabIndex={-1} className="flex-grow focus:outline-none">{children}</main>
|
||||
<TrustBar />
|
||||
<Footer />
|
||||
<AnalyticsWrapper />
|
||||
|
||||
+3
-6
@@ -1,10 +1,7 @@
|
||||
import { Metadata } from 'next';
|
||||
import { HeroSection } from '@/components/sections/HeroSection';
|
||||
import { AboutStory } from '@/components/sections/AboutStory';
|
||||
import { StatsBar } from '@/components/sections/StatsBar';
|
||||
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 { companyDetails } from '@/content/company';
|
||||
|
||||
@@ -32,13 +29,13 @@ export default function ONasPage() {
|
||||
|
||||
<AboutStory />
|
||||
|
||||
<StatsBar />
|
||||
{/* <StatsBar /> (Ukryte na czas zbierania autentycznych danych statystycznych) */}
|
||||
|
||||
<ValuesGrid />
|
||||
|
||||
<CertificatesSection />
|
||||
{/* <CertificatesSection /> (Ukryte na czas uzyskania skanów dokumentów) */}
|
||||
|
||||
<Testimonials />
|
||||
{/* <Testimonials /> (Ukryte na czas zbierania autentycznych opinii) */}
|
||||
|
||||
<CtaBanner
|
||||
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 { WhyUs } from "@/components/sections/WhyUs";
|
||||
import { FeaturedProducts } from "@/components/sections/FeaturedProducts";
|
||||
import { Testimonials } from "@/components/sections/Testimonials";
|
||||
import { BlogPreview } from "@/components/sections/BlogPreview";
|
||||
import { CtaBanner } from "@/components/sections/CtaBanner";
|
||||
import { companyDetails } from "@/content/company";
|
||||
import { Wrench, Phone, ArrowRight, Thermometer, Wind, Zap, Calendar, Check } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { Button, buttonClasses } from "@/components/ui/Button";
|
||||
import { JsonLd } from "@/components/ui/JsonLd";
|
||||
|
||||
export default function Home() {
|
||||
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'https://thermcool.pl';
|
||||
@@ -104,7 +104,7 @@ export default function Home() {
|
||||
companyDetails.socials.facebook,
|
||||
companyDetails.socials.instagram,
|
||||
companyDetails.socials.google,
|
||||
],
|
||||
].filter(Boolean),
|
||||
},
|
||||
{
|
||||
"@type": "WebSite",
|
||||
@@ -129,10 +129,7 @@ export default function Home() {
|
||||
return (
|
||||
<>
|
||||
{/* LocalBusiness Schema for Google Search SEO */}
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||
/>
|
||||
<JsonLd data={jsonLd} />
|
||||
|
||||
<HeroSection />
|
||||
<AdvantagesBar />
|
||||
@@ -141,7 +138,7 @@ export default function Home() {
|
||||
<FeaturedProducts />
|
||||
<WhyUs />
|
||||
{/* ProjectsSection will go here in Group B */}
|
||||
<Testimonials />
|
||||
{/* <Testimonials /> (Ukryte na czas zbierania autentycznych opinii) */}
|
||||
|
||||
{/* Serwis LG */}
|
||||
<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.',
|
||||
alternates: {
|
||||
canonical: '/realizacje',
|
||||
},
|
||||
robots: {
|
||||
index: false,
|
||||
follow: false,
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+1
-12
@@ -2,7 +2,6 @@ import { MetadataRoute } from 'next';
|
||||
import { getProducts } from '@/content/products';
|
||||
import { getPosts } from '@/content/posts';
|
||||
import { dolnySlaskCities } from '@/content/cities';
|
||||
import { wroclawDistricts } from '@/content/wroclaw-districts';
|
||||
export default function sitemap(): MetadataRoute.Sitemap {
|
||||
const baseUrl = 'https://thermcool.pl';
|
||||
|
||||
@@ -11,8 +10,6 @@ export default function sitemap(): MetadataRoute.Sitemap {
|
||||
'',
|
||||
'/klimatyzacja',
|
||||
'/pompy-ciepla',
|
||||
|
||||
'/realizacje',
|
||||
'/blog',
|
||||
'/o-nas',
|
||||
'/kalkulator',
|
||||
@@ -68,13 +65,5 @@ export default function sitemap(): MetadataRoute.Sitemap {
|
||||
priority: 0.7,
|
||||
}));
|
||||
|
||||
// Dynamiczne strony dzielnic
|
||||
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];
|
||||
return [...staticRoutes, ...productRoutes, ...postRoutes, ...cityRoutes];
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { getProductBySlug, getProducts } from '@/content/products';
|
||||
import { companyDetails } from '@/content/company';
|
||||
import { ProductPageClient } from '@/components/shop/ProductPageClient';
|
||||
import { getProductVariants } from '@/content/product-variants';
|
||||
import { JsonLd } from '@/components/ui/JsonLd';
|
||||
|
||||
interface Props {
|
||||
params: Promise<{ slug: string }>;
|
||||
@@ -51,17 +52,13 @@ export default async function ProductPage({ params }: Props) {
|
||||
"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 (
|
||||
<>
|
||||
{productJsonLd && (
|
||||
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(productJsonLd) }} />
|
||||
)}
|
||||
{productJsonLd && <JsonLd data={productJsonLd} />}
|
||||
<ProductPageClient
|
||||
product={product}
|
||||
seriesProducts={seriesProducts}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { ArrowLeft, Phone, Zap, Wind, Volume2, Thermometer } from 'lucide-react'
|
||||
import MultisplitContactForm from '@/components/shop/MultisplitContactForm';
|
||||
|
||||
interface Props {
|
||||
params: { slug: string };
|
||||
params: Promise<{ slug: string }>;
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
@@ -18,7 +18,7 @@ export async function generateStaticParams() {
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
||||
const { slug } = params;
|
||||
const { slug } = await params;
|
||||
const product = getProductBySlug(slug);
|
||||
if (!product) return { title: 'Produkt nie znaleziony' };
|
||||
return {
|
||||
@@ -28,7 +28,7 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
||||
}
|
||||
|
||||
export default async function MultisplitInquiryPage({ params }: Props) {
|
||||
const { slug } = params;
|
||||
const { slug } = await params;
|
||||
const product = getProductBySlug(slug);
|
||||
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",
|
||||
socials: {
|
||||
facebook: "https://facebook.com",
|
||||
instagram: "https://instagram.com",
|
||||
google: "https://google.com/maps",
|
||||
facebook: "",
|
||||
instagram: "",
|
||||
google: "",
|
||||
},
|
||||
stats: [
|
||||
{ label: "Lat doświadczenia", value: "15+", number: 15 },
|
||||
{ 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" },
|
||||
],
|
||||
stats: [] as Array<{ label: string; value: string; number: number }>,
|
||||
certificates: [] as Array<{ name: string; image: string }>,
|
||||
};
|
||||
|
||||
@@ -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