Logo świąteczne, poprawka w SEO, oraz wyszukiwaniu kanałów

This commit is contained in:
dm
2025-12-17 06:11:33 +01:00
parent 4f0f171bdc
commit ce8b627160
11 changed files with 104 additions and 57 deletions

View File

@@ -5,7 +5,10 @@
"scripts": { "scripts": {
"dev": "astro dev", "dev": "astro dev",
"build": "astro build", "build": "astro build",
"preview": "astro preview" "preview": "astro preview",
"clean": "rimraf node_modules .astro .vite dist .cache .turbo package-lock.json",
"fresh": "npm run clean && npm install",
"dev:clean": "npm run clean && npm install && astro dev"
}, },
"dependencies": { "dependencies": {
"@astrojs/node": "^9.5.1", "@astrojs/node": "^9.5.1",
@@ -26,6 +29,7 @@
"@types/better-sqlite3": "^7.6.13", "@types/better-sqlite3": "^7.6.13",
"autoprefixer": "^10.4.0", "autoprefixer": "^10.4.0",
"postcss": "^8.4.0", "postcss": "^8.4.0",
"rimraf": "^6.1.2",
"tailwindcss": "^3.4.0" "tailwindcss": "^3.4.0"
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
public/logon.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -1,20 +1,20 @@
title: title:
- "Internet i Telewizja FUZ" - Internet i Telewizja FUZ
subtitle: subtitle:
- Doskanały internet światłowodowy i telewizja w Wyszkowie i okolicach, - Doskonały internet światłowodowy i telewizja w Wyszkowie i okolicach,
- "Lokalny operator, znamy Twoją okolicę," - Lokalny operator, znamy Twoją okolicę,
- "Realny serwis, szybkie wsparcie," - Realny serwis, szybkie wsparcie,
- "Stabilna infrastruktura światłowodowa," - Stabilna infrastruktura światłowodowa,
description: | description: |
imageUrl: "home.webp" imageUrl: home.webp
ctas: ctas:
- label: "Zobacz ofertę Internetu" - label: Zobacz ofertę Internetu
href: "/internet-swiatlowodowy" href: /internet-swiatlowodowy
title: "Przejdź do oferty Internetu światłowodowego" title: Przejdź do oferty Internetu światłowodowego
primary: false primary: false
- label: "Zobacz ofertę Telewizji" - label: Zobacz ofertę Telewizji
href: "/internet-telewizja" href: /internet-telewizja
title: "Przejdź do oferty Internet + Telewizja w FUZ" title: Przejdź do oferty Internet + Telewizja w FUZ
primary: false primary: false

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -126,17 +126,34 @@ export default function JamboxChannelsSearch() {
setWanted([]); setWanted([]);
} }
// =========================================
// ✅ sugestie pakietów dla koszyka
// - GŁÓWNE: exact/ranked (z count)
// - TEMATYCZNE: dodatki do dokupienia (bez liczenia)
// =========================================
const packageSuggestions = useMemo(() => { const packageSuggestions = useMemo(() => {
if (!wanted.length) return { exact: [], ranked: [], thematic: [] }; if (!wanted.length) return { exact: [], ranked: [], thematic: [], baseWantedLen: 0, wantedLen: 0 };
// ✅ kanały, które mają pakiety główne (tylko te liczymy w dopasowaniu "głównych")
const baseWanted = wanted.filter((ch) => Array.isArray(ch.packages) && ch.packages.length > 0);
const baseWantedLen = baseWanted.length;
// ======= GŁÓWNE ======= // ======= GŁÓWNE =======
const counts = new Map(); // key = packageName // jeśli nie ma żadnego kanału "bazowego", nie ma co liczyć dopasowania bazowych
if (baseWantedLen === 0) {
// nadal zwracamy tematyczne
const thematicMap = new Map();
for (const ch of wanted) { for (const ch of wanted) {
const tp = Array.isArray(ch.thematic_packages) ? ch.thematic_packages : [];
for (const p of tp) {
const tid = String(p?.tid ?? "").trim();
const name = String(p?.name ?? "").trim();
if (!tid || !name) continue;
if (!thematicMap.has(tid)) thematicMap.set(tid, { tid, name });
}
}
const thematic = Array.from(thematicMap.values()).sort((a, b) => a.name.localeCompare(b.name, "pl"));
return { exact: [], ranked: [], thematic, baseWantedLen, wantedLen: wanted.length };
}
const counts = new Map(); // key = packageName
for (const ch of baseWanted) {
const pkgs = Array.isArray(ch.packages) ? ch.packages : []; const pkgs = Array.isArray(ch.packages) ? ch.packages : [];
for (const p of pkgs) { for (const p of pkgs) {
const name = String(p?.name ?? "").trim(); const name = String(p?.name ?? "").trim();
@@ -150,20 +167,18 @@ export default function JamboxChannelsSearch() {
const all = Array.from(counts.values()); const all = Array.from(counts.values());
const exact = all const exact = all
.filter((p) => p.count === wanted.length) .filter((p) => p.count === baseWantedLen)
.sort((a, b) => a.name.localeCompare(b.name, "pl")); .sort((a, b) => a.name.localeCompare(b.name, "pl"));
const ranked = all const ranked = all
.filter((p) => p.count < wanted.length) .filter((p) => p.count < baseWantedLen)
.sort((a, b) => b.count - a.count || a.name.localeCompare(b.name, "pl")) .sort((a, b) => b.count - a.count || a.name.localeCompare(b.name, "pl"))
.slice(0, 12); .slice(0, 12);
// ======= TEMATYCZNE (dodatki) ======= // ======= TEMATYCZNE (dodatki) =======
const thematicMap = new Map(); // key = tid const thematicMap = new Map(); // key = tid
for (const ch of wanted) { for (const ch of wanted) {
const tp = Array.isArray(ch.thematic_packages) const tp = Array.isArray(ch.thematic_packages) ? ch.thematic_packages : [];
? ch.thematic_packages
: [];
for (const p of tp) { for (const p of tp) {
const tid = String(p?.tid ?? "").trim(); const tid = String(p?.tid ?? "").trim();
const name = String(p?.name ?? "").trim(); const name = String(p?.name ?? "").trim();
@@ -176,9 +191,10 @@ export default function JamboxChannelsSearch() {
a.name.localeCompare(b.name, "pl") a.name.localeCompare(b.name, "pl")
); );
return { exact, ranked, thematic }; return { exact, ranked, thematic, baseWantedLen, wantedLen: wanted.length };
}, [wanted]); }, [wanted]);
return ( return (
<div class="f-chsearch"> <div class="f-chsearch">
<h1 class="f-section-title">Wyszukiwanie kanałów w pakietach telewizji</h1> <h1 class="f-section-title">Wyszukiwanie kanałów w pakietach telewizji</h1>
@@ -260,7 +276,7 @@ export default function JamboxChannelsSearch() {
onClick={() => scrollToPackage(p.name)} onClick={() => scrollToPackage(p.name)}
title={`Zawiera ${p.count}/${wanted.length} wybranych kanałów`} title={`Zawiera ${p.count}/${wanted.length} wybranych kanałów`}
> >
{p.name} ({p.count}/{wanted.length}) {p.name} ({p.count}/{packageSuggestions.baseWantedLen})
</button> </button>
))} ))}
</div> </div>

View File

@@ -4,54 +4,80 @@ import fs from "fs";
const seo = Astro.props.seo ?? {}; const seo = Astro.props.seo ?? {};
const globalSeo = yaml.load( const globalSeo = yaml.load(
fs.readFileSync("./src/content/home/seo.yaml", "utf8") fs.readFileSync("./src/content/home/seo.yaml", "utf8"),
); );
const { site, company } = globalSeo; const { site, company } = globalSeo;
const page = seo.page ?? {}; const page = seo.page ?? {};
// ===== helpers =====
function stripTrailingSlash(s = "") {
return String(s).replace(/\/$/, "");
}
function stripLeadingSlash(s = "") {
return String(s).replace(/^\//, "");
}
function isAbsoluteUrl(s = "") {
return /^https?:\/\//i.test(String(s));
}
function joinUrl(base = "", path = "") {
const b = stripTrailingSlash(base);
const p = String(path || "");
if (!p) return b;
if (isAbsoluteUrl(p)) return p;
return `${b}/${stripLeadingSlash(p)}`;
}
// ===== origin / base for meta =====
// Astro.url.origin daje aktualny host (test/prod) dokładnie to chcemy do OG/WhatsApp
const origin = Astro.url?.origin || site.url;
const baseUrl = stripTrailingSlash(origin);
// ===== page fields =====
const title = page.title ?? site.name; const title = page.title ?? site.name;
const description = page.description ?? site.description; const description = page.description ?? site.description;
const image = page.image ?? site.logo;
const canonical = site.url + (page.url ?? "/"); const rawImage = page.image ?? site.logo;
const image = joinUrl(baseUrl, rawImage);
const canonical = joinUrl(baseUrl, page.url ?? "/");
const keywords = page.keywords ?? []; const keywords = page.keywords ?? [];
const extraSchema = page.schema ?? null; const extraSchema = page.schema ?? null;
// JSON-LD objects // JSON-LD objects (tu też używamy baseUrl, żeby nie rozjeżdżało się między test/prod)
const schemaWebsite = { const schemaWebsite = {
"@context": "https://schema.org", "@context": "https://schema.org",
"@type": "WebSite", "@type": "WebSite",
"url": site.url, url: baseUrl,
"name": site.name, name: site.name,
"potentialAction": { potentialAction: {
"@type": "SearchAction", "@type": "SearchAction",
"target": `${site.url}/wyszukiwarka?query={search_term_string}`, target: `${baseUrl}/wyszukiwarka?query={search_term_string}`,
"query-input": "required name=search_term_string" "query-input": "required name=search_term_string",
} },
}; };
const schemaLocalBusiness = { const schemaLocalBusiness = {
"@context": "https://schema.org", "@context": "https://schema.org",
"@type": "LocalBusiness", "@type": "LocalBusiness",
"name": company.name, name: company.name,
"image": site.url + company.logo, image: joinUrl(baseUrl, company.logo),
"telephone": company.phone, telephone: company.phone,
"email": company.email, email: company.email,
"address": { address: {
"@type": "PostalAddress", "@type": "PostalAddress",
"streetAddress": company.street, streetAddress: company.street,
"addressLocality": company.city, addressLocality: company.city,
"postalCode": company.postal, postalCode: company.postal,
"addressCountry": company.country addressCountry: company.country,
}, },
"geo": { geo: {
"@type": "GeoCoordinates", "@type": "GeoCoordinates",
"latitude": company.lat, latitude: company.lat,
"longitude": company.lon longitude: company.lon,
}, },
"url": site.url url: baseUrl,
}; };
// JSON strings // JSON strings
@@ -67,9 +93,7 @@ const jsonExtra = extraSchema ? JSON.stringify(extraSchema) : null;
<title>{title}</title> <title>{title}</title>
<meta name="description" content={description} /> <meta name="description" content={description} />
{keywords.length > 0 && ( {keywords.length > 0 && <meta name="keywords" content={keywords.join(", ")} />}
<meta name="keywords" content={keywords.join(", ")} />
)}
<link rel="canonical" href={canonical} /> <link rel="canonical" href={canonical} />
@@ -79,7 +103,12 @@ const jsonExtra = extraSchema ? JSON.stringify(extraSchema) : null;
<meta property="og:description" content={description} /> <meta property="og:description" content={description} />
<meta property="og:url" content={canonical} /> <meta property="og:url" content={canonical} />
<meta property="og:site_name" content={site.name} /> <meta property="og:site_name" content={site.name} />
<meta property="og:image" content={image} /> <meta property="og:image" content={image} />
<meta property="og:image:secure_url" content={image} />
<meta property="og:image:type" content="image/png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<!-- Twitter --> <!-- Twitter -->
<meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:card" content="summary_large_image" />
@@ -94,9 +123,7 @@ const jsonExtra = extraSchema ? JSON.stringify(extraSchema) : null;
<script type="application/ld+json" set:html={jsonBusiness}></script> <script type="application/ld+json" set:html={jsonBusiness}></script>
<!-- JSON-LD: Extra schema --> <!-- JSON-LD: Extra schema -->
{jsonExtra && ( {jsonExtra && <script type="application/ld+json" set:html={jsonExtra}></script>}
<script type="application/ld+json" set:html={jsonExtra}></script>
)}
<slot /> <slot />
</head> </head>