astro - uspojnienie stron, seo unifikacja, favicon

This commit is contained in:
dm
2025-12-21 09:50:58 +01:00
parent de4639d2c7
commit 664acbf86b
47 changed files with 811 additions and 605 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

BIN
public/apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

9
public/browserconfig.xml Normal file
View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/mstile-150x150.png"/>
<TileColor>#da532c</TileColor>
</tile>
</msapplication>
</browserconfig>

BIN
public/favicon-16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 841 B

BIN
public/favicon-32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
public/og/dokumenty-og.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 347 KiB

BIN
public/og/kontakt-og.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 347 KiB

19
public/site.webmanifest Normal file
View File

@@ -0,0 +1,19 @@
{
"name": "FUZ Adam Rojek - Internet Światłowodowy",
"short_name": "FUZ",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

View File

@@ -1,6 +1,7 @@
---
import { Image, getImage } from "astro:assets";
import type { ImageMetadata } from "astro";
import { stripImageExtension, findHeroImages } from "../../lib/astro-helpers";
interface CTA {
label: string;
@@ -26,21 +27,14 @@ const {
ctas = [],
} = Astro.props;
const imageBase = imageUrl.replace(/\.(webp|png|jpg|jpeg)$/i, "");
const imageBase = stripImageExtension(imageUrl);
const images = import.meta.glob<{ default: ImageMetadata }>(
"/src/assets/hero/**/*.webp",
{ eager: true },
);
function findImage(folder: string): ImageMetadata | null {
const key = `/src/assets/hero/${folder}/${imageBase}-${folder}.webp`;
return images[key]?.default ?? null;
}
const mobile = findImage("mobile");
const tablet = findImage("tablet");
const desktop = findImage("desktop");
const { mobile, tablet, desktop } = findHeroImages(images, imageBase);
const mobileSet = mobile
? await getImage({ src: mobile, widths: [480, 640], format: "webp" })
@@ -122,4 +116,4 @@ const desktopImg = desktop
)}
</div>
</div>
</section>
</section>

View File

@@ -1,10 +1,7 @@
---
import yaml from "js-yaml";
import fs from "fs";
import { loadYaml } from "../../lib/astro-helpers";
const footer = yaml.load(
fs.readFileSync("./src/content/site/footer.yaml", "utf8"),
);
const footer = loadYaml("./src/content/site/footer.yaml");
---
<footer class="f-footer">
@@ -55,4 +52,4 @@ const footer = yaml.load(
<div class="f-footer-recaptcha" set:html={footer.recaptcha} />
</div>
</footer>
</footer>

View File

@@ -49,4 +49,4 @@ const links = [
<MobileMenu client:idle links={links} />
</div>
</nav>
</nav>

View File

@@ -39,4 +39,4 @@ const sorted = cities.sort((a: string, b: any) => a.localeCompare(b, "pl"));
.fuz-city-item {
color: var(--f-text);
}
</style>
</style>

View File

@@ -47,7 +47,6 @@ const domId = `fuz-map-${Math.random().toString(36).slice(2)}`;
script.defer = true;
script.onerror = () => reject("Google Maps API failed to load");
// Czekamy na google.maps.importLibrary
script.onload = () => {
const checkReady = () => {
if (window.google?.maps?.importLibrary) {

View File

@@ -24,4 +24,4 @@ const section = props.section ?? {};
</div>
</div>
</div>
</section>
</section>

View File

@@ -2,6 +2,7 @@
import { Image } from "astro:assets";
import type { ImageMetadata } from "astro";
import Markdown from "../../islands/Markdown.jsx";
import { findSectionImage } from "../../lib/astro-helpers";
const { section, index } = Astro.props;
@@ -13,15 +14,8 @@ const sectionImages = import.meta.glob<{ default: ImageMetadata }>(
{ eager: true },
);
let sectionImage: ImageMetadata | null = null;
if (section.image) {
const path = `/src/assets/sections/${section.image}`;
const mod = sectionImages[path];
if (mod) sectionImage = mod.default;
}
const isAboveFold = index === 0; // możesz zmienić warunek jak chcesz
const sectionImage = section.image ? findSectionImage(sectionImages, section.image) : null;
const isAboveFold = index === 0;
---
<section class="f-section">
@@ -63,4 +57,4 @@ const isAboveFold = index === 0; // możesz zmienić warunek jak chcesz
}
</div>
</div>
</section>
</section>

View File

@@ -1,23 +1,13 @@
---
import yaml from "js-yaml";
import fs from "fs";
import { marked } from "marked";
import { loadYaml, processMarkdownSections } from "../../lib/astro-helpers";
import SectionDefault from "./SectionDefault.astro";
const { src } = Astro.props;
const data = yaml.load(fs.readFileSync(src, "utf8")) ?? { sections: [] };
const sections = (data.sections as any[]).map((s: any) => ({
...s,
html: marked(s.content || "")
}));
const data = loadYaml(src) ?? { sections: [] };
const sections = processMarkdownSections(data.sections as any[]);
---
{sections.map((section: any, index: number) => {
const type = section.type || "default";
return <SectionDefault section={section} index={index} />;
})}
})}

View File

@@ -0,0 +1,44 @@
---
type Props = {
text?: string;
star?: boolean;
className?: string;
};
const { text = "", star = true, className = "" } = Astro.props;
const lines = String(text || "")
.replace(/\r\n/g, "\n")
.split("\n")
.map((l) => l.trim());
// usuń puste linie na początku/końcu
while (lines.length && !lines[0]) lines.shift();
while (lines.length && !lines[lines.length - 1]) lines.pop();
const title = lines[0] || "";
// 👉 reszta jako JEDEN ciąg
const body = lines
.slice(1)
.join(" ")
.replace(/\s+/g, " ")
.trim();
const hasBody = body.length > 0;
---
{title && (
<details class={`f-note-acc ${className}`} data-has-body={hasBody ? "1" : "0"}>
<summary class="f-note-acc-summary">
{star && <span class="f-note-acc-star {">*</span>}
<span class="f-note-acc-title">{title} (Szczegóły po rozwinięciu)</span>
{hasBody && <span class="f-note-acc-chev" aria-hidden="true">▾</span>}
</summary>
{hasBody && (
<div class="f-note-acc-body">
<p class="f-note-acc-p">{`${title} ${body}`}</p>
</div>
)}
</details>
)}

View File

@@ -13,6 +13,7 @@ import { moneyWithLabel } from "../../lib/money.js";
* @param {Array} props.features - Lista cech [{label, value}]
* @param {Array} props.actions - Lista akcji (przycisków)
* @param {string} props.cardId - ID dla scrollowania (opcjonalne)
* @param {boolean} props.withStart - Czy wyświetlać gwaizdkę przy cenie
*/
export default function OfferCard({
card,
@@ -22,7 +23,8 @@ export default function OfferCard({
cenaOpis,
features = [],
actions = [],
cardId = null
cardId = null,
withStart = true
}) {
const hasPrice = typeof price === 'number';
@@ -43,7 +45,7 @@ export default function OfferCard({
<div className="f-card-price">
{hasPrice ? (
<>{moneyWithLabel(price, cenaOpis, false)}</>
<>{moneyWithLabel(price, cenaOpis, false, withStart)}</>
) : (
<span className="opacity-70">Wybierz opcje</span>
)}

View File

@@ -1,30 +1,18 @@
site:
name: "FUZ Kontakt"
description: "Skontaktuj się z nami. Stabilny i szybki internet w Wyszkowie i okolicach"
url: "https://www.fuz.pl"
lang: "pl"
company:
name: "FUZ Adam Rojek"
phone: "+48 (29) 643 80 55"
email: "biuro@fuz.pl"
street: "ul. Świętojańska 46"
city: "Wyszków"
postal: "07-200"
country: "PL"
lat: 52.597385
lon: 21.456797
logo: "/logo.webp"
page:
title: "FUZ Kontakt"
description: "Szybki, stabilny internet światłowodowy w Wyszkowie. Lokalny operator, realny serwis, błyskawiczne wsparcie."
image: "/og/home-og.png"
title: "Kontakt - FUZ Adam Rojek | Internet Światłowodowy Wyszków"
description: "Skontaktuj się z FUZ - lokalny operator internetu światłowodowego w Wyszkowie. Biuro obsługi klienta, zgłoszenia awarii, umów instalację. Tel: 29 643 80 55" # ← 160 znaków
image: "/og/kontakt-og.png"
url: "/kontakt"
keywords:
- kontakt internet światłowodowy Wyszków
- internet Wyszków
- światłowód Wyszków
- internet światłowodowy Wyszków
- lokalny operator internetu Wyszków
schema: {}
- kontakt FUZ Wyszków
- biuro obsługi klienta FUZ
- infolinia internet światłowodowy
- zgłoszenie awarii internet Wyszków
- umów instalację światłowodu
- numer telefonu FUZ
- adres biura FUZ Wyszków
- email kontakt operator
- obsługa klienta Wyszków
- serwis techniczny FUZ
- godziny otwarcia biura
- dojazd do biura Wyszków

View File

@@ -0,0 +1,18 @@
page:
title: "Dokumenty - FUZ Adam Rojek | Regulaminy i Umowy"
description: "Dokumenty FUZ: regulamin świadczenia usług, wzór umowy, cennik, polityka prywatności, warunki techniczne. Wszystkie dokumenty do pobrania w formacie PDF."
image: "/og/dokumenty-og.png"
url: "/dokumenty"
keywords:
- dokumenty FUZ Wyszków
- regulamin świadczenia usług internet
- wzór umowy internet światłowodowy
- cennik usług FUZ
- polityka prywatności operator
- warunki świadczenia usług światłowód
- regulamin internetu Wyszków
- ogólne warunki umowy FUZ
- tabela opłat internet
- karta usług telekomunikacyjnych
- dokumenty prawne operator
- instrukcja reklamacji internet

View File

@@ -1,32 +1,19 @@
site:
name: "FUZ Internet światłowodowy w Wyszkowie"
description: "Stabilny i szybki internet"
url: "https://www.fuz.pl"
lang: "pl"
company:
name: "FUZ Adam Rojek"
phone: "+48 (29) 643 80 55"
email: "biuro@fuz.pl"
street: "ul. Świętojańska 46"
city: "Wyszków"
postal: "07-200"
country: "PL"
lat: 52.597385
lon: 21.456797
logo: "/logo.webp"
page:
title: "FUZ Internet światłowodowy w Wyszkowie"
description: "Szybki, stabilny internet światłowodowy w Wyszkowie. Lokalny operator, realny serwis, błyskawiczne wsparcie."
title: "FUZ Adam Rojek - Internet Światłowodowy Wyszków | Szybki i Stabilny"
description: "Internet światłowodowy w Wyszkowie i okolicach. Lokalny operator z doświadczeniem - stabilne łącze, profesjonalny serwis, konkurencyjne ceny. Sprawdź dostępność!"
image: "/og/home-og.png"
url: "/"
keywords:
- internet Wyszków
- światłowód Wyszków
- internet światłowodowy Wyszków
- lokalny operator internetu Wyszków
- światłowód Wyszków
- internet Wyszków
- lokalny operator internet Wyszków
- szybki internet Wyszków
- stabilny internet Wyszków
- internet telewizja Wyszków
- internet światłowodowy telewizja Wyszków
- telefon Wyszków
schema: {}
- pakiety internet TV Wyszków
- internet telefon Wyszków
- FUZ Wyszków
- fiber internet Wyszków
- internet Rząśnik Pułtusk
- internet światłowodowy okolice Wyszkowa

View File

@@ -1,12 +1,14 @@
tytul: Internet światłowodowy
opis: |
Internet światłowodowy w Wyszkowie i okolicach
Wybierz rodzaj budynku i czas trwania umowy
uwaga: |
Powyższe „ceny brutto z Rabatami 15zł” uwzględniają rabat -15 zł (z czego -5 zł - Rabat za wyrażenie zgody na otrzymywanie Rachunków/faktur VAT za świadczone
przez Dostawcę Usług usługi telekomunikacyjne drogą elektroniczną na wskazany w umowie adres mail oraz za pośrednictwem EBOK; -10 zł - Rabat pod warunkiem
złożenia wniosku o dostarczanie przez Dostawcę Usług treści każdej proponowanej zmiany warunków Umowy, w tym określonych w Umowie, Informacjach Przedumownych oraz danych Dostawcy Usług (chyba że przepisy powszechnie obowiązującego prawa przewidują wyłącznie zawiadomienia poprzez publiczne ogłoszenie), jak
również kontaktowanie się ze mną w ramach procedur reklamacyjnych, w tym w szczególności przesłania odpowiedzi na reklamację, na podany w Umowie adres poczty
elektronicznej).
Powyższe „ceny brutto z Rabatami 15zł”
uwzględniają rabat -15 zł (z czego -5 zł - Rabat za wyrażenie zgody na otrzymywanie Rachunków/faktur VAT za świadczone
przez Dostawcę Usług usługi telekomunikacyjne drogą elektroniczną na wskazany w umowie adres mail oraz za pośrednictwem EBOK; -10 zł - Rabat pod warunkiem
złożenia wniosku o dostarczanie przez Dostawcę Usług treści każdej proponowanej zmiany warunków Umowy, w tym określonych w Umowie, Informacjach Przedumownych oraz danych Dostawcy Usług (chyba że przepisy powszechnie obowiązującego prawa przewidują wyłącznie zawiadomienia poprzez publiczne ogłoszenie), jak
również kontaktowanie się ze mną w ramach procedur reklamacyjnych, w tym w szczególności przesłania odpowiedzi na reklamację, na podany w Umowie adres poczty
elektronicznej).
cena_opis: "zł/mies."
cards:

View File

@@ -1,29 +1,19 @@
site:
name: "FUZ Internet światłowodowy w Wyszkowie"
description: "Stabilny i szybki internet w Wyszkowie i okolicach"
url: "https://www.fuz.pl"
lang: "pl"
company:
name: "FUZ Adam Rojek"
phone: "+48 (29) 643 80 55"
email: "biuro@fuz.pl"
street: "ul. Świętojańska 46"
city: "Wyszków"
postal: "07-200"
country: "PL"
lat: 52.597385
lon: 21.456797
logo: "/logo.webp"
page:
title: "FUZ Internet światłowodowy w Wyszkowie"
description: "Szybki, stabilny internet światłowodowy w Wyszkowie. Lokalny operator, realny serwis, błyskawiczne wsparcie."
image: "/images/internet-og.webp"
title: "Internet Światłowodowy Wyszków - Szybkie Łącze bez Limitów | FUZ"
description: "Internet światłowodowy do 1 Gb/s w Wyszkowie. Bez limitów danych, stabilne połączenie, montaż w 48h. Lokalny operator z profesjonalnym serwisem. Sprawdź ceny!"
image: "/og/internet-og.png"
url: "/internet-swiatlowodowy"
keywords:
- internet Wyszków
- światłowód Wyszków
- internet światłowodowy Wyszków
- lokalny operator internetu
schema: {}
- światłowód Wyszków
- szybki internet Wyszków
- internet bez limitu Wyszków
- internet światłowodowy cena Wyszków
- światłowód do domu Wyszków
- fiber internet Wyszków
- operator światłowodu Wyszków
- instalacja internetu Wyszków
- internet światłowodowy oferta
- stabilny internet Wyszków
- internet światłowodowy Rząśnik
- internet światłowodowy Pułtusk

View File

@@ -2,7 +2,8 @@ tytul: "Internet z telewizją"
opis: |
Wybierz rodzaj budynku i czas trwania umowy
uwaga: |
Powyższe „ceny brutto z Rabatami 15zł” uwzględniają rabat -15 zł (z czego -5 zł - Rabat za wyrażenie zgody na otrzymywanie Rachunków/faktur VAT za świadczone
Powyższe „ceny brutto z Rabatami 15zł”
uwzględniają rabat -15 zł (z czego -5 zł - Rabat za wyrażenie zgody na otrzymywanie Rachunków/faktur VAT za świadczone
przez Dostawcę Usług usługi telekomunikacyjne drogą elektroniczną na wskazany w umowie adres mail oraz za pośrednictwem EBOK; -10 zł - Rabat pod warunkiem
złożenia wniosku o dostarczanie przez Dostawcę Usług treści każdej proponowanej zmiany warunków Umowy, w tym określonych w Umowie, Informacjach Przedumownych oraz danych Dostawcy Usług (chyba że przepisy powszechnie obowiązującego prawa przewidują wyłącznie zawiadomienia poprzez publiczne ogłoszenie), jak
również kontaktowanie się ze mną w ramach procedur reklamacyjnych, w tym w szczególności przesłania odpowiedzi na reklamację, na podany w Umowie adres poczty

View File

@@ -1,30 +1,21 @@
site:
name: "FUZ Telewizja z Internetem światłowodowym w Wyszkowie"
description: "Stabilny i szybki internet z telewizją"
url: "https://www.fuz.pl"
lang: "pl"
company:
name: "FUZ Adam Rojek"
phone: "+48 (29) 643 80 55"
email: "biuro@fuz.pl"
street: "ul. Świętojańska 46"
city: "Wyszków"
postal: "07-200"
country: "PL"
lat: 52.597385
lon: 21.456797
logo: "/logo.webp"
page:
title: "FUZ Telewizja z Internetem światłowodowym w Wyszkowie"
description: "Szybki, stabilny internet światłowodowy z telewizją w Wyszkowie. Lokalny operator, realny serwis, błyskawiczne wsparcie."
title: "Internet i Telewizja Wyszków - Pakiety Światłowodowe 2w1 | FUZ"
description: "Pakiety Internet + TV w Wyszkowie. Światłowód do 1 Gb/s + ponad 200 kanałów. Jeden rachunek, niższa cena, lokalny serwis. Sprawdź ofertę pakietów 2w1!"
image: "/og/telewizja-og.png"
url: "/internet-telewizja"
keywords:
- internet z telewiazją Wyszków
- internet telewiazja Wyszków
- internet telewizja Wyszków
- internet z telewizją Wyszków
- pakiet internet TV Wyszków
- światłowód telewizja Wyszków
- internet światłowodowy telewizja Wyszków
- lokalny operator internetu Wyszków
schema: {}
- internet światłowodowy z TV Wyszków
- pakiet 2w1 internet telewizja
- telewizja przez internet Wyszków
- IPTV Wyszków
- telewizja światłowodowa Wyszków
- internet TV cena Wyszków
- pakiet światłowodowy Wyszków
- internet i telewizja oferta
- kanały telewizyjne Wyszków
- internet telewizja Rząśnik
- internet telewizja Pułtusk

View File

@@ -0,0 +1,23 @@
page:
title: "Mapa Zasięgu Światłowodu - Sprawdź Dostępność Internetu | FUZ"
description: "Sprawdź czy Twój adres jest w zasięgu sieci światłowodowej FUZ. Interaktywna mapa pokrycia Wyszków i okolic. Weryfikacja dostępności w parę sekund!"
image: "/og/mapa-og.png"
url: "/mapa-zasiegu"
keywords:
- mapa zasięgu światłowodu
- sprawdź dostępność internetu
- zasięg sieci FUZ Wyszków
- pokrycie światłowodu Wyszków
- mapa pokrycia internetu
- sprawdź zasięg pod adresem
- dostępność internetu Wyszków
- gdzie jest światłowód
- weryfikacja adresu internet
- mapa sieci światłowodowej
- zasięg operatora lokalnego
- interaktywna mapa zasięgu
- sprawdź swój adres
- czy mam światłowód
- mapa zasięgu Wyszków
- światłowód Rząśnik
- światłowód Pułtusk

View File

@@ -0,0 +1,25 @@
site:
name: "FUZ Adam Rojek"
description: "Lokalny operator internetu światłowodowego w Wyszkowie i okolicach. Stabilne łącze, szybki serwis, konkurencyjne ceny."
url: "https://www.fuz.pl"
lang: "pl"
company:
name: "FUZ Adam Rojek"
phone: "+48 29 643 80 55"
email: "biuro@fuz.pl"
street: "ul. Świętojańska 46"
city: "Wyszków"
postal: "07-200"
country: "PL"
lat: 52.597385
lon: 21.456797
logo: "/logo.webp"
schema:
openingHours: "Mo-Fr 09:00-17:00"
priceRange: "$"
areaServed:
- Wyszków
- Rząśnik
- Pułtusk

View File

@@ -1,33 +1,21 @@
site:
name: "FUZ Telefon i Internet światłowodowy w Wyszkowie"
description: "Usługa telefonu wraz z stabilnym i szybkim internet"
url: "https://www.fuz.pl"
lang: "pl"
company:
name: "FUZ Adam Rojek"
phone: "+48 (29) 643 80 55"
email: "biuro@fuz.pl"
street: "ul. Świętojańska 46"
city: "Wyszków"
postal: "07-200"
country: "PL"
lat: 52.597385
lon: 21.456797
logo: "/logo.webp"
page:
title: "FUZ Telefon i Internet światłowodowy w Wyszkowie"
description: "Szybki, stabilny internet światłowodowy w Wyszkowie z usługa telefonu. Lokalny operator, realny serwis, błyskawiczne wsparcie."
title: "Telefon Stacjonarny i Internet Wyszków | FUZ"
description: "Telefon stacjonarny + Internet światłowodowy w Wyszkowie. Niskie ceny międzynarodowych, jeden rachunek. Sprawdź ofertę telefonu!"
image: "/og/telefon-og.png"
url: "/telefon"
keywords:
- telefon
- telefon Wyszków
- telefon stacjonarny Wyszków
- telefon internet Wyszków
- internet Wyszków
- światłowód Wyszków
- internet światłowodowy Wyszków
- lokalny operator internetu Wyszków
schema: {}
- VoIP Wyszków
- telefonia stacjonarna Wyszków
- telefon światłowodowy Wyszków
- pakiet telefon internet
- tani telefon stacjonarny
- telefon przez internet
- telefonia VoIP Wyszków
- telefon światłowód Wyszków
- połączenia międzynarodowe tanie
- telefon domowy Wyszków
- telefon stacjonarny oferta
- internet telefon Rząśnik
- internet telefon Pułtusk

View File

@@ -141,7 +141,7 @@ export default function JamboxChannelsSearch() {
return (
<div class="f-chsearch">
<h1 class="f-section-title">Wyszukiwanie kanałów w pakietach telewizji</h1>
<h2 class="f-section-title">Wyszukiwanie kanałów w pakietach telewizji</h2>
{/* SEKCJA "CHCIAŁBYM MIEĆ TE KANAŁY" */}
<div class="f-chsearch__wanted">

View File

@@ -64,6 +64,7 @@ function PhoneOfferCard({ card }) {
cenaOpis={priceLabel}
features={features}
actions={[]} // Brak akcji - karty telefoniczne tylko informacyjne
withStart={false}
/>
);
}

View File

@@ -1,130 +1,76 @@
---
import yaml from "js-yaml";
import fs from "fs";
import {
loadGlobalSeo,
buildSeoMeta,
mergeFaviconConfig,
type SeoConfig,
type FaviconConfig
} from "../lib/astro-helpers";
const seo = Astro.props.seo ?? {};
const globalSeo = yaml.load(
fs.readFileSync("./src/content/home/seo.yaml", "utf8"),
);
const globalSeo = loadGlobalSeo(); // ← Globalny
const pageSeo = Astro.props.seo; // ← Ze strony
const { site, company } = globalSeo;
const page = seo.page ?? {};
const origin = Astro.url?.origin || globalSeo.site.url;
const meta = buildSeoMeta(origin, globalSeo, pageSeo as SeoConfig);
// ===== 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)}`;
}
// Favicon configuration - można przekazać custom przez props
const faviconConfig = mergeFaviconConfig(Astro.props.favicon as Partial<FaviconConfig>);
// ===== 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 description = page.description ?? site.description;
const rawImage = page.image ?? site.logo;
const image = joinUrl(baseUrl, rawImage);
const canonical = joinUrl(baseUrl, page.url ?? "/");
const keywords = page.keywords ?? [];
const extraSchema = page.schema ?? null;
// JSON-LD objects (tu też używamy baseUrl, żeby nie rozjeżdżało się między test/prod)
const schemaWebsite = {
"@context": "https://schema.org",
"@type": "WebSite",
url: baseUrl,
name: site.name,
potentialAction: {
"@type": "SearchAction",
target: `${baseUrl}/wyszukiwarka?query={search_term_string}`,
"query-input": "required name=search_term_string",
},
};
const schemaLocalBusiness = {
"@context": "https://schema.org",
"@type": "LocalBusiness",
name: company.name,
image: joinUrl(baseUrl, company.logo),
telephone: company.phone,
email: company.email,
address: {
"@type": "PostalAddress",
streetAddress: company.street,
addressLocality: company.city,
postalCode: company.postal,
addressCountry: company.country,
},
geo: {
"@type": "GeoCoordinates",
latitude: company.lat,
longitude: company.lon,
},
url: baseUrl,
};
// JSON strings
const jsonWebsite = JSON.stringify(schemaWebsite);
const jsonBusiness = JSON.stringify(schemaLocalBusiness);
const jsonExtra = extraSchema ? JSON.stringify(extraSchema) : null;
const jsonWebsite = JSON.stringify(meta.schemaWebsite);
const jsonBusiness = JSON.stringify(meta.schemaLocalBusiness);
const jsonExtra = meta.extraSchema ? JSON.stringify(meta.extraSchema) : null;
---
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{title}</title>
<meta name="description" content={description} />
<title>{meta.title}</title>
<meta name="description" content={meta.description} />
{
keywords.length > 0 && (
<meta name="keywords" content={keywords.join(", ")} />
meta.keywords.length > 0 && (
<meta name="keywords" content={meta.keywords.join(", ")} />
)
}
<!-- Favicon -->
<link rel="apple-touch-icon" sizes="180x180" href={faviconConfig.appleTouchIcon} />
<link rel="icon" type="image/png" sizes="32x32" href={faviconConfig.icon32} />
<link rel="icon" type="image/png" sizes="16x16" href={faviconConfig.icon16} />
<link rel="manifest" href={faviconConfig.manifest} />
<link rel="mask-icon" href={faviconConfig.safariPinnedTab} color={faviconConfig.safariPinnedTabColor} />
<meta name="msapplication-TileColor" content={faviconConfig.msApplicationTileColor} />
<meta name="theme-color" content={faviconConfig.themeColor} />
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Nunito:wght@300;400;700&display=swap"
/>
<link rel="canonical" href={canonical} />
<link rel="canonical" href={meta.canonical} />
<!-- OpenGraph -->
<meta property="og:type" content="website" />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:url" content={canonical} />
<meta property="og:site_name" content={site.name} />
<meta property="og:title" content={meta.title} />
<meta property="og:description" content={meta.description} />
<meta property="og:url" content={meta.canonical} />
<meta property="og:site_name" content={globalSeo.site.name} />
<meta property="og:image" content={image} />
<meta property="og:image:secure_url" content={image} />
<meta property="og:image" content={meta.image} />
<meta property="og:image:secure_url" content={meta.image} />
<meta property="og:image:type" content="image/png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={image} />
<meta name="twitter:title" content={meta.title} />
<meta name="twitter:description" content={meta.description} />
<meta name="twitter:image" content={meta.image} />
<!-- JSON-LD: Website -->
<script type="application/ld+json" set:html={jsonWebsite} />
@@ -136,4 +82,4 @@ const jsonExtra = extraSchema ? JSON.stringify(extraSchema) : null;
{jsonExtra && <script type="application/ld+json" set:html={jsonExtra} />}
<slot />
</head>
</head>

View File

@@ -32,4 +32,4 @@ const { seo } = Astro.props;
<ThemeToggle client:idle />
</div>
</body>
</html>
</html>

297
src/lib/astro-helpers.ts Normal file
View File

@@ -0,0 +1,297 @@
import fs from "node:fs";
import yaml from "js-yaml";
import type { ImageMetadata } from "astro";
// ==================== YAML LOADING ====================
export function loadYaml<T = any>(filepath: string): T {
const raw = fs.readFileSync(filepath, "utf8");
return yaml.load(raw) as T;
}
// ==================== IMAGE HANDLING ====================
type ImageGlob = Record<string, { default: ImageMetadata }>;
export function findImageInGlob(
images: ImageGlob,
folder: string,
basename: string
): ImageMetadata | null {
const key = `/src/assets/${folder}/${basename}`;
return images[key]?.default ?? null;
}
export function findHeroImages(
images: ImageGlob,
imageBase: string
): {
mobile: ImageMetadata | null;
tablet: ImageMetadata | null;
desktop: ImageMetadata | null;
} {
return {
mobile: findImageInGlob(images, "hero/mobile", `${imageBase}-mobile.webp`),
tablet: findImageInGlob(images, "hero/tablet", `${imageBase}-tablet.webp`),
desktop: findImageInGlob(images, "hero/desktop", `${imageBase}-desktop.webp`),
};
}
export function findSectionImage(
images: ImageGlob,
imageName: string
): ImageMetadata | null {
const path = `/src/assets/sections/${imageName}`;
return images[path]?.default ?? null;
}
export function stripImageExtension(url: string): string {
return url.replace(/\.(webp|png|jpg|jpeg)$/i, "");
}
// ==================== URL HELPERS ====================
export function stripTrailingSlash(s = ""): string {
return String(s).replace(/\/$/, "");
}
export function stripLeadingSlash(s = ""): string {
return String(s).replace(/^\//, "");
}
export function isAbsoluteUrl(s = ""): boolean {
return /^https?:\/\//i.test(String(s));
}
export function joinUrl(base = "", path = ""): string {
const b = stripTrailingSlash(base);
const p = String(path || "");
if (!p) return b;
if (isAbsoluteUrl(p)) return p;
return `${b}/${stripLeadingSlash(p)}`;
}
export function normalizePublicHref(input?: string): string {
let s = String(input ?? "").trim();
if (!s) return "";
if (s.startsWith("/public/")) s = s.replace("/public", "");
s = s.replace(/ /g, "%20");
return s;
}
// ==================== SEO HELPERS ====================
export type SeoConfig = {
page?: {
title?: string;
description?: string;
image?: string;
url?: string;
keywords?: string[];
schema?: any;
};
};
export type GlobalSeo = {
site: {
url: string;
name: string;
description: string;
logo: string;
};
company: {
name: string;
logo: string;
phone: string;
email: string;
street: string;
city: string;
postal: string;
country: string;
lat: number;
lon: number;
};
};
// ✅ DODANY BRAKUJĄCY TYP
export type SeoMetadata = {
title: string;
description: string;
image: string;
canonical: string;
keywords: string[];
schemaWebsite: any;
schemaLocalBusiness: any;
extraSchema: any;
};
export function loadGlobalSeo(): GlobalSeo {
return loadYaml<GlobalSeo>("./src/content/site/global-seo.yaml");
}
export function buildSeoMeta(
origin: string,
globalSeo: GlobalSeo,
seoConfig: SeoConfig = {}
): SeoMetadata {
const { site, company } = globalSeo;
const page = seoConfig.page ?? {};
const baseUrl = stripTrailingSlash(origin);
const title = page.title ?? site.name;
const description = page.description ?? site.description;
const rawImage = page.image ?? site.logo;
const image = joinUrl(baseUrl, rawImage);
const canonical = joinUrl(baseUrl, page.url ?? "/");
const keywords = page.keywords ?? [];
const schemaWebsite = {
"@context": "https://schema.org",
"@type": "WebSite",
url: baseUrl,
name: site.name,
potentialAction: {
"@type": "SearchAction",
target: `${baseUrl}/wyszukiwarka?query={search_term_string}`,
"query-input": "required name=search_term_string",
},
};
const schemaLocalBusiness = {
"@context": "https://schema.org",
"@type": "LocalBusiness",
name: company.name,
image: joinUrl(baseUrl, company.logo),
telephone: company.phone,
email: company.email,
address: {
"@type": "PostalAddress",
streetAddress: company.street,
addressLocality: company.city,
postalCode: company.postal,
addressCountry: company.country,
},
geo: {
"@type": "GeoCoordinates",
latitude: company.lat,
longitude: company.lon,
},
url: baseUrl,
};
return {
title,
description,
image,
canonical,
keywords,
schemaWebsite,
schemaLocalBusiness,
extraSchema: page.schema ?? null,
};
}
// ==================== MARKDOWN HELPERS ====================
export function processMarkdownSections(sections: any[]): any[] {
return sections.map((s: any) => ({
...s,
html: s.content || "",
}));
}
// ==================== TYPE DEFINITIONS ====================
export type DocumentFile = {
nazwa?: string;
file?: string;
slug?: string;
};
export type DocumentGroup = {
tytul?: string;
pliki?: DocumentFile[];
};
export type DocumentsYaml = {
tytul?: string;
opis?: string;
grupy?: Record<string, DocumentGroup>;
};
// ==================== VALIDATION HELPERS ====================
export function isValidString(value: unknown): value is string {
return typeof value === "string" && value.trim().length > 0;
}
export function isValidNumber(value: unknown): value is number {
return typeof value === "number" && !isNaN(value) && isFinite(value);
}
export function safeArray<T>(value: unknown): T[] {
return Array.isArray(value) ? value : [];
}
// ==================== LOCALE HELPERS ====================
export function sortByLocale(items: string[], locale = "pl"): string[] {
return [...items].sort((a, b) => a.localeCompare(b, locale));
}
// ==================== DATE HELPERS ====================
export function getCurrentYear(): number {
return new Date().getFullYear();
}
export function formatDate(date: Date | string, locale = "pl-PL"): string {
const d = typeof date === "string" ? new Date(date) : date;
return d.toLocaleDateString(locale);
}
// ==================== STRING HELPERS ====================
export function slugify(s: string): string {
return String(s || "")
.toLowerCase()
.replace(/[\u0105]/g, "a")
.replace(/[\u0107]/g, "c")
.replace(/[\u0119]/g, "e")
.replace(/[\u0142]/g, "l")
.replace(/[\u0144]/g, "n")
.replace(/[\u00f3]/g, "o")
.replace(/[\u015b]/g, "s")
.replace(/[\u017a\u017c]/g, "z")
.replace(/[^a-z0-9]+/g, "-")
.replace(/(^-|-$)/g, "");
}
// ==================== FAVICON HELPERS ====================
export type FaviconConfig = {
appleTouchIcon?: string;
icon32?: string;
icon16?: string;
manifest?: string;
safariPinnedTab?: string;
safariPinnedTabColor?: string;
msApplicationTileColor?: string;
themeColor?: string;
};
export const DEFAULT_FAVICON_CONFIG: FaviconConfig = {
appleTouchIcon: "/apple-touch-icon.png",
icon32: "/favicon-32x32.png",
icon16: "/favicon-16x16.png",
manifest: "/site.webmanifest",
safariPinnedTab: "/safari-pinned-tab.svg",
safariPinnedTabColor: "#5bbad5",
msApplicationTileColor: "#da532c",
themeColor: "#ffffff",
};
export function mergeFaviconConfig(custom?: Partial<FaviconConfig>): FaviconConfig {
return { ...DEFAULT_FAVICON_CONFIG, ...custom };
}

View File

@@ -5,8 +5,9 @@ export function money(amount, decimals = true) {
return n;
}
export function moneyWithLabel(v, cenaOpis, decimals = true) {
return `${money(v, decimals)} ${cenaOpis}`;
export function moneyWithLabel(v, cenaOpis, decimals = true, withStart = true ) {
const star = withStart ? " *" : "";
return `${money(v, decimals)} ${cenaOpis} ${star}`;
}
export function moneyPLN(v, decimals = true) {

View File

@@ -31,4 +31,4 @@ const html = marked.parse(doc.content);
</div>
</div>
</section>
</DefaultLayout>
</DefaultLayout>

View File

@@ -1,99 +1,79 @@
---
import DefaultLayout from "../../layouts/DefaultLayout.astro";
import yaml from "js-yaml";
import fs from "node:fs";
import Markdown from "../../islands/Markdown.jsx";
import "../../styles/document.css";
import {
loadYaml,
normalizePublicHref,
type DocumentsYaml,
} from "../../lib/astro-helpers";
type DocFile = {
nazwa?: string;
file?: string;
slug?: string;
};
const doc = loadYaml<DocumentsYaml>("./src/content/document/documents.yaml");
const seo = loadYaml("./src/content/document/seo.yaml");
type DocGroup = {
tytul?: string;
pliki?: DocFile[];
};
type DocsYaml = {
tytul?: string;
opis?: string;
grupy?: Record<string, DocGroup>;
};
const doc = yaml.load(
fs.readFileSync("./src/content/document/documents.yaml", "utf8"),
) as DocsYaml;
const pageTitle = doc?.tytul;
const pageDesc = doc?.opis;
const pageTitle = doc?.tytul ?? "Dokumenty";
const pageDesc = doc?.opis ?? "";
const groups = doc?.grupy ?? {};
const left = groups["otworz"] ?? {};
const right = groups["pobierz"] ?? {};
function normalizePublicHref(input?: string) {
let s = String(input ?? "").trim();
if (!s) return "";
if (s.startsWith("/public/")) s = s.replace("/public", "");
s = s.replace(/ /g, "%20");
return s;
}
---
<DefaultLayout title={pageTitle} description={pageDesc}>
<DefaultLayout seo={seo}>
{/* CONTENT */}
<section class="f-section">
<div class="f-section-grid-top md:grid-cols-2 gap-10">
<div class="f-section-grid-top md:grid-cols-2 gap-10 items-start">
{/* ===== LEWA CZYTAJ ===== */}
<div>
<h3 class="f-section-title mt-0">{left.tytul ?? "Przeczytaj"}</h3>
<h3 class="f-section-title">{left.tytul ?? "Przeczytaj"}</h3>
{!left.pliki?.length ? (
<p class="opacity-70 mt-4">Brak dokumentów.</p>
) : (
<div class="f-documents-grid">
{left.pliki.map((p) => (
p.slug ? (
<a
class="f-document-card"
href={`/dokumenty/${p.slug}`}
title={p.nazwa}
>
<div class="f-document-title">{p.nazwa}</div>
</a>
) : null
))}
</div>
)}
{
!left.pliki?.length ? (
<p class="opacity-70 mt-4">Brak dokumentów.</p>
) : (
<div class="f-documents-grid">
{left.pliki.map((p) =>
p.slug ? (
<a
class="f-document-card"
href={`/dokumenty/${p.slug}`}
title={p.nazwa}
>
<div class="f-document-title">{p.nazwa}</div>
</a>
) : null,
)}
</div>
)
}
</div>
<div>
<h3 class="f-section-title mt-0">{right.tytul ?? "Pobierz"}</h3>
<h3 class="f-section-title">{right.tytul ?? "Pobierz"}</h3>
{!right.pliki?.length ? (
<p class="opacity-70 mt-4">Brak plików.</p>
) : (
<div class="f-documents-grid">
{right.pliki.map((p) => {
const href = normalizePublicHref(p.file);
if (!href) return null;
{
!right.pliki?.length ? (
<p class="opacity-70 mt-4">Brak plików.</p>
) : (
<div class="f-documents-grid">
{right.pliki.map((p) => {
const href = normalizePublicHref(p.file);
if (!href) return null;
return (
<a
class="f-document-card"
href={href}
download
title={p.nazwa}
>
<div class="f-document-title">{p.nazwa}</div>
</a>
);
})}
</div>
)}
return (
<a
class="f-document-card"
href={href}
download
title={p.nazwa}
>
<div class="f-document-title">{p.nazwa}</div>
</a>
);
})}
</div>
)
}
</div>
</div>
</section>
</DefaultLayout>
</DefaultLayout>

View File

@@ -1,13 +1,9 @@
---
import path from "node:path";
import DefaultLayout from "../../layouts/DefaultLayout.astro";
import SectionRenderer from "../../components/sections/SectionRenderer.astro";
import InternetCards from "../../islands/Internet/InternetCards.jsx";
import { loadYamlFile } from "../../lib/loadYaml";
type SeoYaml = any;
import { loadYaml, safeArray } from "../../lib/astro-helpers";
import NoteAccordion from "../../components/ui/NoteAccordion.astro";
type InternetParam = { klucz: string; label: string; value: string | number };
type InternetCena = {
@@ -55,65 +51,6 @@ type Addon = {
};
type AddonsYaml = { cena_opis?: string; dodatki?: Addon[] };
const seo = loadYamlFile<SeoYaml>(
path.join(
process.cwd(),
"src",
"content",
"internet-swiatlowodowy",
"seo.yaml",
),
);
const data = loadYamlFile<InternetCardsYaml>(
path.join(
process.cwd(),
"src",
"content",
"internet-swiatlowodowy",
"cards.yaml",
),
);
const phoneData = loadYamlFile<PhoneCardsYaml>(
path.join(process.cwd(), "src", "content", "telefon", "cards.yaml"),
);
const addonsData = loadYamlFile<AddonsYaml>(
path.join(
process.cwd(),
"src",
"content",
"internet-swiatlowodowy",
"addons.yaml",
),
);
const tytul = data?.tytul ?? "";
const opis = data?.opis ?? "Wybierz rodzaj budynku i czas trwania umowy";
const uwaga = data?.uwaga ?? "";
const waluta = data?.waluta ?? "PLN";
const cenaOpis = data?.cena_opis ?? "zł/mies.";
const cards = (
Array.isArray(data?.cards)
? data.cards.filter((c) => c?.widoczny === true)
: []
) as InternetCard[];
const phoneCards = (
Array.isArray(phoneData?.cards)
? phoneData.cards.filter((c) => c?.widoczny === true)
: []
) as PhoneCard[];
const addons = (
Array.isArray(addonsData?.dodatki) ? addonsData.dodatki : []
) as Addon[];
const addonsCenaOpis = addonsData?.cena_opis ?? cenaOpis;
type SwitchOption = { id: string | number; nazwa: string };
type SwitchDef = {
id: string;
@@ -124,13 +61,32 @@ type SwitchDef = {
};
type SwitchesYaml = { switches?: SwitchDef[] };
const switchesData = loadYamlFile<SwitchesYaml>(
path.join(process.cwd(), "src", "content", "site", "switches.yaml"),
const seo = loadYaml("./src/content/internet-swiatlowodowy/seo.yaml");
const data = loadYaml<InternetCardsYaml>(
"./src/content/internet-swiatlowodowy/cards.yaml",
);
const phoneData = loadYaml<PhoneCardsYaml>("./src/content/telefon/cards.yaml");
const addonsData = loadYaml<AddonsYaml>(
"./src/content/internet-swiatlowodowy/addons.yaml",
);
const switchesData = loadYaml<SwitchesYaml>("./src/content/site/switches.yaml");
const switches = (
Array.isArray(switchesData?.switches) ? switchesData.switches : []
) as SwitchDef[];
const tytul = data?.tytul ?? "";
const opis = data?.opis ?? "Wybierz rodzaj budynku i czas trwania umowy";
const uwaga = data?.uwaga ?? "";
const waluta = data?.waluta ?? "PLN";
const cenaOpis = data?.cena_opis ?? "zł/mies.";
const cards = safeArray<InternetCard>(data?.cards).filter(
(c) => c?.widoczny === true,
);
const phoneCards = safeArray<PhoneCard>(phoneData?.cards).filter(
(c) => c?.widoczny === true,
);
const addons = safeArray<Addon>(addonsData?.dodatki);
const switches = safeArray<SwitchDef>(switchesData?.switches);
const addonsCenaOpis = addonsData?.cena_opis ?? cenaOpis;
---
<DefaultLayout seo={seo}>
@@ -149,7 +105,7 @@ const switches = (
addonsCenaOpis={addonsCenaOpis}
switches={switches}
/>
<p><span class="f-card-price text-sm">* </span>{uwaga}</p>
<NoteAccordion text={uwaga} star={true} />
</div>
</section>

View File

@@ -1,14 +1,10 @@
---
import path from "node:path";
import DefaultLayout from "../../layouts/DefaultLayout.astro";
import SectionRenderer from "../../components/sections/SectionRenderer.astro";
import JamboxCards from "../../islands/jambox/JamboxCards.jsx";
import SectionChannelsSearch from "../../components/sections/SectionChannelsSearch.astro";
import { loadYamlFile } from "../../lib/loadYaml";
type SeoYaml = any;
import { loadYaml, safeArray } from "../../lib/astro-helpers";
import NoteAccordion from "../../components/ui/NoteAccordion.astro";
type Param = { klucz: string; label: string; value: string | number };
type Cena = {
@@ -71,65 +67,6 @@ type AddonsYaml = {
dodatki?: Addon[];
};
const seo = loadYamlFile<SeoYaml>(
path.join(process.cwd(), "src", "content", "internet-telewizja", "seo.yaml"),
);
const data = loadYamlFile<CardsYaml>(
path.join(
process.cwd(),
"src",
"content",
"internet-telewizja",
"cards.yaml",
),
);
const tytul = data?.tytul ?? "";
const opis = data?.opis ?? "Wybierz rodzaj budynku i czas trwania umowy";
const uwaga = data?.uwaga ?? "";
const waluta = data?.waluta ?? "PLN";
const cenaOpis = data?.cena_opis ?? "zł/mies.";
const internetWspolne: Param[] = Array.isArray(data?.internet_parametry_wspolne)
? data.internet_parametry_wspolne
: [];
const cards: Card[] = Array.isArray(data?.cards)
? data.cards.filter((c) => c?.widoczny === true)
: [];
const phoneYaml = loadYamlFile<PhoneYaml>(
path.join(process.cwd(), "src", "content", "telefon", "cards.yaml"),
);
const tvAddonsYaml = loadYamlFile<any>(
path.join(process.cwd(), "src", "content", "internet-telewizja", "tv-addons.yaml"),
);
const phoneCards = Array.isArray(phoneYaml?.cards) ? phoneYaml.cards : [];
const tvAddons = Array.isArray(tvAddonsYaml?.dodatki) ? tvAddonsYaml.dodatki : [];
const addonsYaml = loadYamlFile<AddonsYaml>(
path.join(
process.cwd(),
"src",
"content",
"internet-telewizja",
"addons.yaml",
),
);
const addons: Addon[] = Array.isArray(addonsYaml?.dodatki)
? addonsYaml.dodatki
: [];
const decoders: Decoder[] = Array.isArray(addonsYaml?.dekodery)
? addonsYaml.dekodery
: [];
const addonsCenaOpis = addonsYaml?.cena_opis ?? cenaOpis;
type SwitchOption = { id: string | number; nazwa: string };
type SwitchDef = {
id: string;
@@ -140,14 +77,32 @@ type SwitchDef = {
};
type SwitchesYaml = { switches?: SwitchDef[] };
const switchesYaml = loadYamlFile<SwitchesYaml>(
path.join(process.cwd(), "src", "content", "site", "switches.yaml"),
const seo = loadYaml("./src/content/internet-telewizja/seo.yaml");
const data = loadYaml<CardsYaml>("./src/content/internet-telewizja/cards.yaml");
const phoneYaml = loadYaml<PhoneYaml>("./src/content/telefon/cards.yaml");
const tvAddonsYaml = loadYaml<any>(
"./src/content/internet-telewizja/tv-addons.yaml",
);
const addonsYaml = loadYaml<AddonsYaml>(
"./src/content/internet-telewizja/addons.yaml",
);
const switchesYaml = loadYaml<SwitchesYaml>("./src/content/site/switches.yaml");
const switches: SwitchDef[] = Array.isArray(switchesYaml?.switches)
? switchesYaml.switches
: [];
const tytul = data?.tytul ?? "";
const opis = data?.opis ?? "Wybierz rodzaj budynku i czas trwania umowy";
const uwaga = data?.uwaga ?? "";
const waluta = data?.waluta ?? "PLN";
const cenaOpis = data?.cena_opis ?? "zł/mies.";
const internetWspolne = safeArray<Param>(data?.internet_parametry_wspolne);
const cards = safeArray<Card>(data?.cards).filter((c) => c?.widoczny === true);
const phoneCards = safeArray<PhoneCard>(phoneYaml?.cards);
const tvAddons = safeArray(tvAddonsYaml?.dodatki);
const addons = safeArray<Addon>(addonsYaml?.dodatki);
const decoders = safeArray<Decoder>(addonsYaml?.dekodery);
const switches = safeArray<SwitchDef>(switchesYaml?.switches);
const addonsCenaOpis = addonsYaml?.cena_opis ?? cenaOpis;
---
<DefaultLayout seo={seo}>
@@ -169,9 +124,9 @@ const switches: SwitchDef[] = Array.isArray(switchesYaml?.switches)
addonsCenaOpis={addonsCenaOpis}
switches={switches}
/>
<p><span class="f-card-price text-sm">* </span>{uwaga}</p>
<NoteAccordion text={uwaga} star={true} />
</div>
</section>
<SectionChannelsSearch />
<SectionRenderer src="./src/content/internet-telewizja/section.yaml" />
</DefaultLayout>
</DefaultLayout>

View File

@@ -1,10 +1,9 @@
---
export const prerender = false;
import path from "node:path";
import DefaultLayout from "../../layouts/DefaultLayout.astro";
import { loadYamlFile } from "../../lib/loadYaml";
import MozliwosciSearch from "../../islands/jambox/JamboxMozliwosciSearch.jsx";
import { loadYaml, safeArray, slugify } from "../../lib/astro-helpers";
type YamlSection = {
title: string;
@@ -16,21 +15,6 @@ type YamlData = {
sections?: YamlSection[];
};
function slugify(s: string) {
return String(s || "")
.toLowerCase()
.replace(/[\u0105]/g, "a")
.replace(/[\u0107]/g, "c")
.replace(/[\u0119]/g, "e")
.replace(/[\u0142]/g, "l")
.replace(/[\u0144]/g, "n")
.replace(/[\u00f3]/g, "o")
.replace(/[\u015b]/g, "s")
.replace(/[\u017a\u017c]/g, "z")
.replace(/[^a-z0-9]+/g, "-")
.replace(/(^-|-$)/g, "");
}
let items: Array<{
id: string;
title: string;
@@ -41,17 +25,8 @@ let items: Array<{
let err = "";
try {
const data = loadYamlFile<YamlData>(
path.join(
process.cwd(),
"src",
"content",
"internet-telewizja",
"telewizja-mozliwosci.yaml"
)
);
const sections = Array.isArray(data?.sections) ? data.sections : [];
const data = loadYaml<YamlData>("./src/content/internet-telewizja/telewizja-mozliwosci.yaml");
const sections = safeArray<YamlSection>(data?.sections);
items = sections
.filter((s) => s?.title)
@@ -68,7 +43,6 @@ try {
---
<DefaultLayout title="Możliwości JAMBOX">
<!-- NAGŁÓWEK STRONY zgodny z FUZ -->
<section class="f-section" id="top">
<div class="f-section-grid-single">
<h1 class="f-section-title">Możliwości JAMBOX</h1>
@@ -89,6 +63,4 @@ try {
)
}
</section>
{/* UWAGA: render sekcji przeniesiony do wyspy, żeby filtr działał */}
</DefaultLayout>
</DefaultLayout>

View File

@@ -1,20 +1,35 @@
---
import path from "node:path";
import DefaultLayout from "../../layouts/DefaultLayout.astro";
import MapGoogle from "../../components/maps/MapGoogle.astro";
import Markdown from "../../islands/Markdown.jsx";
import { loadYamlFile } from "../../lib/loadYaml";
import { loadYaml } from "../../lib/astro-helpers";
import "../../styles/contact.css";
type SeoYaml = any;
const seo = loadYamlFile<SeoYaml>(
path.join(process.cwd(), "src", "content", "contact", "seo.yaml"),
);
type ContactData = {
title: string;
description: string;
contactFormTitle: string;
lat: number;
lng: number;
markerTitle: string;
markerAddress: string;
maps: { mapId: string };
form: {
firstName: { placeholder: string };
lastName: { placeholder: string };
email: { placeholder: string };
phone: { placeholder: string };
subject: { placeholder: string };
message: { placeholder: string; rows: number };
rodo: { label: string; policyLink: string; policyTitle: string; policyText: string };
submit: { label: string; title: string };
successMessage: string;
errorMessage: string;
};
};
type ContactData = any;
const data = loadYamlFile<ContactData>(
path.join(process.cwd(), "src", "content", "contact", "contact.yaml"),
);
const seo = loadYaml("./src/content/contact/seo.yaml");
const data = loadYaml<ContactData>("./src/content/contact/contact.yaml");
const apiKey = import.meta.env.PUBLIC_GOOGLE_MAPS_KEY;
const form = data.form;
@@ -83,7 +98,6 @@ const form = data.form;
class="f-input"
required></textarea>
<!-- widoczne tylko gdy jest oferta -->
<div id="offerSummaryWrap" class="hidden">
<textarea
id="offerSummary"
@@ -134,7 +148,6 @@ const form = data.form;
<div id="toast" class="f-toast"></div>
</section>
<!-- ReCaptcha v3 -->
<script
is:inline
define:vars={{ siteKey: import.meta.env.PUBLIC_RECAPTCHA_SITE_KEY }}
@@ -257,4 +270,4 @@ const form = data.form;
});
});
</script>
</DefaultLayout>
</DefaultLayout>

View File

@@ -2,13 +2,14 @@
import DefaultLayout from "../../layouts/DefaultLayout.astro";
import MapGoogle from "../../components/maps/MapGoogle.astro";
import RangeForm from "../../islands/RangeForm.jsx";
import { loadYaml, safeArray } from "../../lib/astro-helpers";
import "../../styles/map-google.css";
const apiKey = import.meta.env.PUBLIC_GOOGLE_MAPS_KEY;
const lat = 52.597388;
const lon = 21.456797;
const mapStyleId = "8e0a97af9476f2d3";
const seo = loadYaml("./src/content/mapa-zasiegu/seo.yaml");
---
<script>
@@ -20,9 +21,8 @@ const mapStyleId = "8e0a97af9476f2d3";
}
</script>
<DefaultLayout title="FUZ Mapa zasięgu sieci światłowodowej">
<DefaultLayout seo={seo}>
<section class="flex flex-col md:flex-row h-full min-h-[80vh]">
<!-- PANEL LEWY -->
<aside
class="w-full md:w-[340px] bg-[var(--f-background)] text-[var(--f-text)]
pt-6 px-6 flex flex-col gap-6 overflow-y-auto z-40"
@@ -36,7 +36,6 @@ const mapStyleId = "8e0a97af9476f2d3";
<RangeForm client:load />
</aside>
<!-- MAPA -->
<div class="flex-1 relative min-h-[50vh] md:min-h-0">
<MapGoogle
apiKey={apiKey}
@@ -73,7 +72,6 @@ const mapStyleId = "8e0a97af9476f2d3";
fiberLayer.setMap(map);
}
// Czekamy aż mapa się załaduje
const int = setInterval(() => {
const map = window.getActiveMap();
if (map && window.google?.maps) {
@@ -88,7 +86,6 @@ const mapStyleId = "8e0a97af9476f2d3";
const map = window.getActiveMap();
if (!map) return;
// Czekamy aż API będzie gotowe
if (!window.google?.maps?.importLibrary) {
await new Promise((resolve) => {
const int = setInterval(() => {
@@ -140,7 +137,6 @@ const mapStyleId = "8e0a97af9476f2d3";
window._activeMarker = marker;
// InfoWindow
const html = `
<div class="f-info-window">
<div class="f-info-header">
@@ -167,4 +163,4 @@ const mapStyleId = "8e0a97af9476f2d3";
info.open({ map, anchor: marker });
};
</script>
</DefaultLayout>
</DefaultLayout>

View File

@@ -1,9 +1,8 @@
---
import DefaultLayout from "../../layouts/DefaultLayout.astro";
import yaml from "js-yaml";
import fs from "node:fs";
import Markdown from "../../islands/Markdown.jsx";
import AddonChannelsGrid from "../../islands/jambox/AddonChannelsModal.jsx";
import { loadYaml, safeArray } from "../../lib/astro-helpers";
import "../../styles/jambox-tematyczne.css";
type AddonPriceRow = {
@@ -43,11 +42,9 @@ type TvAddonsDoc = {
grupy?: Record<string, GroupMeta>;
};
const doc = yaml.load(
fs.readFileSync("./src/content/internet-telewizja/tv-addons.yaml", "utf8"),
) as TvAddonsDoc;
const doc = loadYaml<TvAddonsDoc>("./src/content/internet-telewizja/tv-addons.yaml");
const addons = Array.isArray(doc?.dodatki) ? doc.dodatki : [];
const addons = safeArray<TvAddon>(doc?.dodatki);
const groupMeta = doc?.grupy ?? {};
const tid = Number(Astro.params.tid);
@@ -63,8 +60,7 @@ const viewAddons = pickedGroup
? addons.filter((a) => String(a?.group ?? "").trim() === pickedGroup)
: [picked];
const footerCta =
pickedGroup ? groupMeta[pickedGroup]?.rejestracja : undefined;
const footerCta = pickedGroup ? groupMeta[pickedGroup]?.rejestracja : undefined;
---
<DefaultLayout
@@ -87,7 +83,6 @@ const footerCta =
data-addon-section
data-has-media={assumeHasMedia ? "1" : "0"}
>
{/* MEDIA — odpowiednik <Image /> */}
<div class="f-addon-media md:order-2">
{pkgName ? (
<AddonChannelsGrid
@@ -100,7 +95,6 @@ const footerCta =
) : null}
</div>
{/* TEKST */}
<div class="md:order-1">
{pkgName && <h2 class="f-section-title">{pkgName}</h2>}
{addon?.opis && <Markdown text={addon.opis} />}
@@ -129,4 +123,4 @@ const footerCta =
</div>
</section>
) : null}
</DefaultLayout>
</DefaultLayout>

View File

@@ -1,9 +1,8 @@
---
import DefaultLayout from "../../layouts/DefaultLayout.astro";
import yaml from "js-yaml";
import fs from "node:fs";
import Markdown from "../../islands/Markdown.jsx";
import AddonChannelsGrid from "../../islands/jambox/AddonChannelsModal.jsx";
import { loadYaml, safeArray } from "../../lib/astro-helpers";
import "../../styles/jambox-tematyczne.css";
type AddonPriceRow = {
@@ -24,7 +23,6 @@ type TvAddon = {
group_mode?: string;
};
// ✅ OPCJA A: metadane grupy + CTA
type GroupCta = {
label?: string;
href?: string;
@@ -42,20 +40,16 @@ type TvAddonsDoc = {
opis?: string;
cena_opis?: string;
dodatki?: TvAddon[];
grupy?: Record<string, GroupMeta>; // ✅
grupy?: Record<string, GroupMeta>;
};
const doc = yaml.load(
fs.readFileSync("./src/content/internet-telewizja/tv-addons.yaml", "utf8"),
) as TvAddonsDoc;
const doc = loadYaml<TvAddonsDoc>("./src/content/internet-telewizja/tv-addons.yaml");
const pageTitle = doc?.tytul ?? "Dodatkowe pakiety TV";
const pageDesc = doc?.opis ?? "";
const addons: TvAddon[] = Array.isArray(doc?.dodatki) ? doc.dodatki : [];
const addons = safeArray<TvAddon>(doc?.dodatki);
const detailsBase = "/pakiety-premium";
// ✅ mapa meta grup (np. hbo_max -> { rejestracja: {...} })
const groupMeta: Record<string, GroupMeta> = doc?.grupy ?? {};
type Group = {
@@ -64,6 +58,7 @@ type Group = {
items: TvAddon[];
groupId?: string;
};
const groupsMap = new Map<string, Group>();
for (const a of addons) {
@@ -95,7 +90,7 @@ const groups: Group[] = Array.from(groupsMap.values());
{
groups.map((group, groupIndex) => {
const isSingle = group.key.startsWith("s:");
const gId = String(group.groupId ?? "").trim(); // np. "hbo_max"
const gId = String(group.groupId ?? "").trim();
const meta = gId ? groupMeta[gId] : undefined;
const footerCta = meta?.rejestracja;
@@ -111,7 +106,6 @@ const groups: Group[] = Array.from(groupsMap.values());
const pkgName = String(addon?.nazwa ?? "").trim();
const hasYamlImage = !!String(addon?.image ?? "").trim();
// ✅ zachowanie jak wcześniej (1 kolumna + ukrycie media gdy brak)
const assumeHasMedia = pkgName ? true : hasYamlImage;
const href =
@@ -148,7 +142,6 @@ const groups: Group[] = Array.from(groupsMap.values());
);
})}
{/* ✅ STOPKA GRUPY: przycisk rejestracji dla całej grupy */}
{!isSingle && footerCta?.href && footerCta?.label ? (
<div class="f-addon-group-footer fuz-markdown max-w-none">
{footerCta.opis ? <p>{footerCta.opis}</p> : null}
@@ -167,4 +160,4 @@ const groups: Group[] = Array.from(groupsMap.values());
);
})
}
</DefaultLayout>
</DefaultLayout>

View File

@@ -1,13 +1,8 @@
---
import path from "node:path";
import DefaultLayout from "../../layouts/DefaultLayout.astro";
import SectionRenderer from "../../components/sections/SectionRenderer.astro";
import OffersPhoneCards from "../../islands/phone/PhoneCards.jsx";
import { loadYamlFile } from "../../lib/loadYaml";
type SeoYaml = any;
import { loadYaml, safeArray } from "../../lib/astro-helpers";
type PhoneParam = {
klucz: string;
@@ -29,20 +24,13 @@ type PhoneCardsYaml = {
cards?: PhoneCard[];
};
const seo = loadYamlFile<SeoYaml>(
path.join(process.cwd(), "src", "content", "telefon", "seo.yaml"),
);
const phoneCards = loadYamlFile<PhoneCardsYaml>(
path.join(process.cwd(), "src", "content", "telefon", "cards.yaml"),
);
const seo = loadYaml("./src/content/telefon/seo.yaml");
const phoneCards = loadYaml<PhoneCardsYaml>("./src/content/telefon/cards.yaml");
const tytul = phoneCards?.tytul ?? "";
const opis = phoneCards?.opis ?? "";
const cards: PhoneCard[] = Array.isArray(phoneCards?.cards)
? phoneCards.cards.filter((c) => c?.widoczny === true)
: [];
const cards = safeArray<PhoneCard>(phoneCards?.cards).filter((c) => c?.widoczny === true);
---
<DefaultLayout seo={seo}>
@@ -55,4 +43,4 @@ const cards: PhoneCard[] = Array.isArray(phoneCards?.cards)
</section>
<SectionRenderer src="./src/content/telefon/section.yaml" />
</DefaultLayout>
</DefaultLayout>

View File

@@ -308,17 +308,31 @@
}
.f-addon-below {
grid-column: 1 / -1; /* pełna szerokość */
grid-column: 1 / -1;
/* pełna szerokość */
@apply pt-1;
}
.f-addon-below {
grid-column: 1 / -1; /* od kolumny main */
grid-column: 1 / -1;
/* od kolumny main */
}
.f-radio-check {
grid-area: check;
}
.f-radio-main {
grid-area: main;
min-width: 0;
}
.f-radio-price {
grid-area: price;
justify-self: end;
text-align: right;
}
.f-radio-check { grid-area: check; }
.f-radio-main { grid-area: main; min-width: 0; }
.f-radio-price { grid-area: price; justify-self: end; text-align: right; }
.f-radio-below {
grid-area: below;
@apply text-sm opacity-85;
@@ -335,7 +349,8 @@
/* ✅ DLA QTY — nie trzymaj 140px, bo na mobile wypycha */
.f-addon-item--qty .f-addon-price {
min-width: 110px; /* było 140px */
min-width: 110px;
/* było 140px */
}
/* ✅ DLA QTY na małych ekranach jeszcze ciaśniej */
@@ -349,4 +364,42 @@
.f-addon-item--qty .f-addon-price-total {
font-size: 0.95em;
}
}
.f-note-acc {
@apply mx-20
}
.f-note-acc-summary {
@apply list-none cursor-pointer select-none;
@apply flex items-start;
@apply text-sm opacity-90;
}
.f-note-acc-summary::-webkit-details-marker {
display: none;
}
.f-note-acc-star {
@apply relative -top-2 mr-2;
@apply text-3xl font-extrabold text-[--f-offers-price]
}
.f-note-acc-title {}
.f-note-acc-chev {
@apply text-3xl font-extrabold text-[--f-offers-price] ml-auto transition;
}
.f-note-acc[open] .f-note-acc-chev {
@apply rotate-180;
}
.f-note-acc-body {
@apply text-sm opacity-80 leading-relaxed ml-5;
}
.f-note-acc-p {
@apply mb-1 last:mb-0;
}