astro - uspojnienie stron, seo unifikacja, favicon
BIN
public/android-chrome-192x192.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
public/android-chrome-512x512.png
Normal file
|
After Width: | Height: | Size: 236 KiB |
BIN
public/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
9
public/browserconfig.xml
Normal 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
|
After Width: | Height: | Size: 841 B |
BIN
public/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
public/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
public/og/dokumenty-og.png
Normal file
|
After Width: | Height: | Size: 347 KiB |
BIN
public/og/kontakt-og.png
Normal file
|
After Width: | Height: | Size: 347 KiB |
19
public/site.webmanifest
Normal 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"
|
||||
}
|
||||
@@ -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" })
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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} />;
|
||||
})}
|
||||
44
src/components/ui/NoteAccordion.astro
Normal 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>
|
||||
)}
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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
|
||||
18
src/content/document/seo.yaml
Normal 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
|
||||
@@ -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
|
||||
@@ -1,8 +1,10 @@
|
||||
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
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
23
src/content/mapa-zasiegu/seo.yaml
Normal 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
|
||||
25
src/content/site/global-seo.yaml
Normal 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
|
||||
@@ -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
|
||||
@@ -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">
|
||||
|
||||
@@ -64,6 +64,7 @@ function PhoneOfferCard({ card }) {
|
||||
cenaOpis={priceLabel}
|
||||
features={features}
|
||||
actions={[]} // Brak akcji - karty telefoniczne są tylko informacyjne
|
||||
withStart={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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} />
|
||||
|
||||
297
src/lib/astro-helpers.ts
Normal 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 };
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -1,59 +1,37 @@
|
||||
---
|
||||
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 ? (
|
||||
{
|
||||
!left.pliki?.length ? (
|
||||
<p class="opacity-70 mt-4">Brak dokumentów.</p>
|
||||
) : (
|
||||
<div class="f-documents-grid">
|
||||
{left.pliki.map((p) => (
|
||||
{left.pliki.map((p) =>
|
||||
p.slug ? (
|
||||
<a
|
||||
class="f-document-card"
|
||||
@@ -62,16 +40,18 @@ function normalizePublicHref(input?: string) {
|
||||
>
|
||||
<div class="f-document-title">{p.nazwa}</div>
|
||||
</a>
|
||||
) : null
|
||||
))}
|
||||
</div>
|
||||
) : 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 ? (
|
||||
{
|
||||
!right.pliki?.length ? (
|
||||
<p class="opacity-70 mt-4">Brak plików.</p>
|
||||
) : (
|
||||
<div class="f-documents-grid">
|
||||
@@ -91,9 +71,9 @@ function normalizePublicHref(input?: string) {
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
</DefaultLayout>
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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,7 +124,7 @@ 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 />
|
||||
|
||||
@@ -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>
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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} />}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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 */
|
||||
@@ -350,3 +365,41 @@
|
||||
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;
|
||||
}
|
||||