Logo świąteczne, poprawka w SEO, oraz wyszukiwaniu kanałów
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
public/logo.webp
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 5.2 KiB |
BIN
public/logon.webp
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
@@ -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
|
||||||
|
|||||||
@@ -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 =======
|
||||||
|
// 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) {
|
||||||
|
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
|
const counts = new Map(); // key = packageName
|
||||||
for (const ch of wanted) {
|
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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||