Compare commits

..

18 Commits

Author SHA1 Message Date
dm
a805610cba Telewizja-możliwości poprawa reguły img_dimensions_present
All checks were successful
Deploy FUZ 2.0 / deploy (push) Successful in 1m57s
2025-12-23 08:49:34 +01:00
dm
40929b3604 Dokumenty = zmiana układu, grupowanie 2025-12-23 08:32:17 +01:00
dm
6b73e91ddb Files ponowne dodanie plików, z małych liter
All checks were successful
Deploy FUZ 2.0 / deploy (push) Successful in 1m52s
2025-12-22 18:04:07 +01:00
dm
abc3bd4d41 Files usuniecie plików
All checks were successful
Deploy FUZ 2.0 / deploy (push) Successful in 2m0s
2025-12-22 18:00:06 +01:00
dm
e0095ee10f Dokumenty scieżki do plików
All checks were successful
Deploy FUZ 2.0 / deploy (push) Successful in 2m16s
2025-12-22 17:18:13 +01:00
dm
bbb6742849 Dokumnety - poprawa nazw plików
All checks were successful
Deploy FUZ 2.0 / deploy (push) Successful in 1m56s
2025-12-22 17:07:45 +01:00
dm
2c30704a11 Dokumenty poprawka scieżki do plików
All checks were successful
Deploy FUZ 2.0 / deploy (push) Successful in 1m59s
2025-12-22 16:56:58 +01:00
dm
95b308455c Dokumenty - usuniecie smiec dodanie nowych plików
All checks were successful
Deploy FUZ 2.0 / deploy (push) Successful in 2m9s
2025-12-22 16:51:25 +01:00
dm
146e738c09 Kontakt - poprawka nagłowka
All checks were successful
Deploy FUZ 2.0 / deploy (push) Successful in 1m56s
2025-12-21 18:57:46 +01:00
dm
92edbad2c2 Poprawki w konfiguracji 2025-12-21 18:52:49 +01:00
dm
b065db4faf Poprawka w rozszerzeniu pliku highlightUtils 2025-12-21 18:32:32 +01:00
dm
ed513957c3 Ulepszona konfiguracja 2025-12-21 18:26:20 +01:00
dm
12be46d038 Sitemap - przywrócenie 2025-12-21 18:17:14 +01:00
dm
0d967ea6c8 Style inline usuniecie i wstawienie do css 2025-12-21 17:28:47 +01:00
dm
259ee007db Dodanie Title i Description strony do yamli oraz jezeli nie ma seo to do strony 2025-12-21 17:24:09 +01:00
dm
e9f440353d Robot.txt 2025-12-21 15:18:02 +01:00
dm
f91b557efd Mapa zasięgu kontener na listę miejscowości 2025-12-21 14:05:17 +01:00
dm
832ee2e796 Kontakt ukruty dymek, Lista miejscowości wyrównywanie pod mapą 2025-12-21 13:00:32 +01:00
43 changed files with 517 additions and 1895 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -17,6 +17,7 @@ export default defineConfig({
build: { build: {
minify: "esbuild", minify: "esbuild",
cssMinify: "esbuild", cssMinify: "esbuild",
chunkSizeWarningLimit: 500,
}, },
}, },
integrations: [ integrations: [

View File

@@ -1,95 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from __future__ import annotations
from pathlib import Path
import argparse
import sys
def iter_astro_files(root: Path) -> list[Path]:
"""Zwraca listę plików .astro w root (rekursywnie), posortowaną stabilnie."""
files = [p for p in root.rglob("*.astro") if p.is_file()]
# sortowanie po ścieżce względnej dla powtarzalności
files.sort(key=lambda p: str(p.as_posix()).lower())
return files
def read_text_fallback(p: Path) -> str:
"""
Czyta plik jako tekst:
- najpierw UTF-8
- jeśli się nie da, to z BOM/latin-1 jako awaryjne (bez crasha)
"""
try:
return p.read_text(encoding="utf-8")
except UnicodeDecodeError:
# Spróbuj UTF-8 z BOM
try:
return p.read_text(encoding="utf-8-sig")
except UnicodeDecodeError:
# Ostatecznie: latin-1 (nie idealne, ale nie przerwie działania)
return p.read_text(encoding="latin-1")
def build_output(pages_dir: Path, files: list[Path]) -> str:
rel_root = pages_dir.parent.parent # zwykle ./src -> parent parent? NIEPEWNE, więc liczymy inaczej
# lepiej: relatywnie do katalogu projektu (cwd)
cwd = Path.cwd()
lines: list[str] = []
lines.append(f"ASTRO DUMP (root: {pages_dir.resolve()})")
lines.append(f"Found files: {len(files)}")
lines.append("=" * 80)
lines.append("")
for f in files:
rel = f.relative_to(cwd) if f.is_relative_to(cwd) else f
content = read_text_fallback(f)
lines.append(f"FILE: {rel.as_posix()}")
lines.append("-" * 80)
lines.append(content.rstrip("\n"))
lines.append("")
lines.append("=" * 80)
lines.append("")
return "\n".join(lines)
def main() -> int:
parser = argparse.ArgumentParser(
description="Zrzuca wszystkie pliki .astro z ./src/pages do jednego pliku txt (ścieżka + zawartość)."
)
parser.add_argument(
"--pages",
default="src/pages",
help="Ścieżka do katalogu pages (domyślnie: src/pages)",
)
parser.add_argument(
"--out",
default="astro-pages-dump.txt",
help="Plik wyjściowy (domyślnie: astro-pages-dump.txt)",
)
args = parser.parse_args()
pages_dir = Path(args.pages).resolve()
out_file = Path(args.out).resolve()
if not pages_dir.exists() or not pages_dir.is_dir():
print(f"[ERR] Nie znaleziono katalogu: {pages_dir}", file=sys.stderr)
return 2
files = iter_astro_files(pages_dir)
dump = build_output(pages_dir, files)
out_file.write_text(dump, encoding="utf-8")
print(f"[OK] Zapisano: {out_file} (files: {len(files)})")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -1,120 +0,0 @@
#!/usr/bin/env python3
import argparse
import os
import re
from pathlib import Path
from typing import Dict, List, Set, Tuple
# --- Regexy do zbierania klas z CSS ---
# Łapie ".class-name" w selektorach (pomija np. ".5" i rzeczy z escapami prosto, ale działa w praktyce)
CSS_CLASS_RE = re.compile(r'(?<![\\\w-])\.([a-zA-Z_-][\w-]*)')
# --- Regexy do zbierania stringów klas z Astro/JSX/TSX ---
# 1) class="..."
CLASS_ATTR_RE = re.compile(r'\bclass\s*=\s*("([^"]*)"|\'([^\']*)\')', re.IGNORECASE)
# 2) className="..."
CLASSNAME_ATTR_RE = re.compile(r'\bclassName\s*=\s*("([^"]*)"|\'([^\']*)\')', re.IGNORECASE)
# Astro: class:list={{ a: cond, "b c": cond2 }} / class:list={[...]} tu łapiemy stringi w środku
STRING_LIT_RE = re.compile(r'("([^"]+)"|\'([^\']+)\')')
# Dla wyszukiwania tokenów klas (żeby "f-card" nie matchowało jako fragment "f-card-x")
def token_pattern(cls: str) -> re.Pattern:
return re.compile(r'(?<![\w-])' + re.escape(cls) + r'(?![\w-])')
def read_text(path: Path) -> str:
try:
return path.read_text(encoding="utf-8", errors="ignore")
except Exception:
return ""
def collect_css_classes(css_dir: Path) -> Tuple[Set[str], Dict[str, Set[Path]]]:
classes: Set[str] = set()
where: Dict[str, Set[Path]] = {}
for p in css_dir.rglob("*.css"):
txt = read_text(p)
for m in CSS_CLASS_RE.finditer(txt):
c = m.group(1)
classes.add(c)
where.setdefault(c, set()).add(p)
return classes, where
def collect_used_classes(code_dir: Path, candidates: Set[str]) -> Tuple[Set[str], Dict[str, Set[Path]]]:
used: Set[str] = set()
used_where: Dict[str, Set[Path]] = {c: set() for c in candidates}
exts = {".astro", ".jsx", ".tsx", ".js", ".ts"}
# prekompilacja patternów dla szybkości
patterns = {c: token_pattern(c) for c in candidates}
for p in code_dir.rglob("*"):
if not p.is_file():
continue
if p.suffix.lower() not in exts:
continue
txt = read_text(p)
if not txt:
continue
# szybki filtr: jeśli żaden kandydat nie ma nawet prefiksu "f-" / "jmb-" itd,
# to i tak skan tokenowy jest ok, ale tu robimy prosty scan wszystkich.
for c, pat in patterns.items():
if pat.search(txt):
used.add(c)
used_where[c].add(p)
# usuń puste wpisy
used_where = {k: v for k, v in used_where.items() if v}
return used, used_where
def main():
ap = argparse.ArgumentParser(description="Znajdź potencjalnie nieużywane klasy CSS w projekcie Astro/JSX.")
ap.add_argument("--css", required=True, help="Katalog z plikami CSS (np. src/styles)")
ap.add_argument("--code", required=True, help="Katalog z kodem (np. src)")
ap.add_argument("--min-len", type=int, default=3, help="Minimalna długość nazwy klasy (domyślnie 3)")
ap.add_argument("--prefix", action="append", default=[], help="Filtruj klasy po prefiksie (np. --prefix f- --prefix jmb-)")
ap.add_argument("--show-where", action="store_true", help="Pokaż gdzie zdefiniowano klasę w CSS")
args = ap.parse_args()
css_dir = Path(args.css).resolve()
code_dir = Path(args.code).resolve()
if not css_dir.exists():
raise SystemExit(f"Brak katalogu CSS: {css_dir}")
if not code_dir.exists():
raise SystemExit(f"Brak katalogu code: {code_dir}")
all_classes, defined_where = collect_css_classes(css_dir)
# filtr długości + prefiksów
classes = {c for c in all_classes if len(c) >= args.min_len}
if args.prefix:
classes = {c for c in classes if any(c.startswith(px) for px in args.prefix)}
used, used_where = collect_used_classes(code_dir, classes)
unused = sorted(classes - used)
print(f"CSS katalog: {css_dir}")
print(f"CODE katalog: {code_dir}")
print(f"Klasy w CSS: {len(classes)} (po filtrach)")
print(f"Użyte w kodzie:{len(used)}")
print(f"NIEUŻYTE: {len(unused)}")
print("-" * 60)
for c in unused:
print(c)
if args.show_where:
files = sorted(defined_where.get(c, []))
for f in files:
rel = f.relative_to(css_dir.parent) if css_dir.parent in f.parents else f
print(f" defined in: {rel}")
print("-" * 60)
# opcjonalnie: pokaż top kilka użyć
# (jak chcesz, dopiszę flagę na raport "gdzie użyte")
if __name__ == "__main__":
main()

View File

@@ -1,5 +1,6 @@
{ {
"name": "fuz20", "name": "fuz-site",
"type": "module",
"version": "1.0.0", "version": "1.0.0",
"private": true, "private": true,
"scripts": { "scripts": {

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
public/files/umowa_tv.pdf Normal file

Binary file not shown.

15
public/robots.txt Normal file
View File

@@ -0,0 +1,15 @@
# robots.txt dla FUZ Adam Rojek
# https://www.fuz.pl/robots.txt
User-agent: *
Allow: /
# Sitemap
Sitemap: https://www.fuz.dariuszm.eu/sitemap.xml
# Crawl-delay (opcjonalnie)
# Crawl-delay: 1
# Zablokuj crawlowanie zbędnych ścieżek (jeśli są)
Disallow: /pages/api/
Disallow: /_astro/

View File

@@ -12,31 +12,3 @@ const sorted = cities.sort((a: string, b: any) => a.localeCompare(b, "pl"));
))} ))}
</div> </div>
</div> </div>
<style>
.fuz-cities-box {
background: var(--f-background);
color: var(--f-text);
padding: 16px;
}
.fuz-cities-title {
font-size: 1.2rem;
font-weight: 600;
margin-bottom: 12px;
}
.fuz-cities-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
gap: 6px 14px;
font-size: 0.95rem;
line-height: 1.4;
}
.fuz-city-item {
color: var(--f-text);
}
</style>

View File

@@ -102,21 +102,21 @@ const domId = `fuz-map-${Math.random().toString(36).slice(2)}`;
position: { lat, lng: lon } position: { lat, lng: lon }
}); });
if (title || desc) { // if (title || desc) {
const { InfoWindow } = await google.maps.importLibrary("maps"); // const { InfoWindow } = await google.maps.importLibrary("maps");
const info = new InfoWindow({ // const info = new InfoWindow({
content: ` // content: `
<div class="f-info-window"> // <div class="f-info-window">
<div class="f-info-header"> // <div class="f-info-header">
<div class="f-info-city">${title}</div> // <div class="f-info-city">${title}</div>
<div class="f-info-street">${desc}</div> // <div class="f-info-street">${desc}</div>
</div> // </div>
</div> // </div>
` // `
}); // });
info.open({ map, anchor: marker }); // info.open({ map, anchor: marker });
} // }
} }
} catch (error) { } catch (error) {
console.error("Error initializing map:", error); console.error("Error initializing map:", error);

View File

@@ -21,10 +21,10 @@ const sectionImages = import.meta.glob<{ default: ImageMetadata }>(
const sectionImage = section.image ? findSectionImage(sectionImages, section.image) : null; const sectionImage = section.image ? findSectionImage(sectionImages, section.image) : null;
const isAboveFold = index === 0; const isAboveFold = index === 0;
// Konfiguracja wyświetlania // Konfiguracja wyświetlania
const showTitle = section.showTitle !== false; // domyślnie true const showTitle = section.showTitle !== false; // domyślnie true
// Formatowanie listy miejscowości jako string // Formatowanie listy miejscowości jako string
const citiesText = cities.join(", "); const citiesText = cities.join(", ");
--- ---
@@ -66,33 +66,6 @@ const citiesText = cities.join(", ");
</a> </a>
</p> </p>
)} )}
<!-- {
section.button && (
<div class="f-section-nav mt-6">
<a href={section.button.url}
class="btn btn-primary"
title={section.button.title}
>
{section.button.text}
</a>
</div>
)
} -->
</div> </div>
</div> </div>
</section> </section>
<!-- <style>
.f-cities-paragraph {
margin-top: 1.5rem;
font-size: 1rem;
line-height: 1.8;
color: var(--f-text, #212529);
}
:global(.dark) .f-cities-paragraph {
color: var(--f-text-dark, #f7fafc);
}
</style> -->

View File

@@ -1,21 +1,68 @@
tytul: Dokumenty tytul: Dokumenty
opis: Strona zawiera dokumnety do pobrania lub przeczytania opis: |
Poniżej dostępne są dokumenty regulujące zasady świadczenia usług, w szczególności regulaminy, cenniki oraz obowiązujące umowy.
grupy: grupy:
pobierz: # Grupa 1: Polityki i regulaminy
tytul: Pobierz polityki:
pliki: tytul: Polityki i regulaminy
- nazwa: Lista kanałów EVIO TV
file: /public/files/EVIO TV.pdf
- nazwa: Lista kanałów JAMBOX TV
file: /public/files/EVIO TV.pdf
- nazwa: Lista kanałów coś
file: /public/files/EVIO TV.pdf
otworz:
tytul: Przeczytaj
pliki: pliki:
- nazwa: Polityka prywatności - nazwa: Polityka prywatności
slug: polityka-prywatnosci slug: polityka-prywatnosci
- nazwa: Promocja przykład do wyswietlenia
slug: promocja # - nazwa: Regulamin świadczenia usług
# slug: regulamin-uslug
# - nazwa: Informacja o przetwarzaniu danych osobowych
# slug: rodo
# Grupa 2: Internet
internet:
tytul: Internet
pliki:
- nazwa: Umowa Internet
file: /files/umowa_internet.pdf
- nazwa: Informacje przedumowne Internet
file: /files/informacje_przedumowne_net.pdf
- nazwa: Podsumowanie warunków umowy Internet
file: /files/podsumowanie_warunków_umowy_net.pdf
- nazwa: Oświadczenia Internet
file: /files/oświadczenia_net.pdf
- nazwa: Informacje urządzenia Internet
file: /files/informacje_urządzenia_net.pdf
- nazwa: Formularz odstąpienia Internet
file: /files/formularz_odstapienia_net.pdf
# Grupa 3: Telewizja
telewizja:
tytul: Internet i Telewizja
pliki:
- nazwa: Umowa TV
file: /files/umowa_tv.pdf
- nazwa: Podsumowanie warunków umowy TV
file: /files/podsumowanie_warunków_umowy_tv.pdf
- nazwa: Informacje przedumowne TV
file: /files/informacje_przedumowne_tv.pdf
- nazwa: Lista kanałów EVIO TV
file: /files/EVIO TV.pdf
- nazwa: Lista kanałów JAMBOX TV
file: /files/JAMBOX TV.pdf
# Grupa 4: Cenniki
cenniki:
tytul: Cenniki
pliki:
- nazwa: Cennik usług TV
file: /files/cennik_uslug_tv.pdf
- nazwa: Cennik usług dodatkowych
file: /files/cennik_uslug_dodatkowych.pdf

View File

@@ -1,6 +1,8 @@
# tytuł dokumentu jednoczesnie tytułem strony <title></title>
title: "Polityka Prywatności" title: "Polityka Prywatności"
# opis wstawiany w <meta name="description" content=""
description: Polityka prywatności, opisuje zasady ochrony Twoich danych osobowych.
visible: true visible: true
intro: Polityka prywatności, opisuje zasady ochrony Twoich danych osobowych.
content: | content: |
## §1. Informacje podstawowe. ## §1. Informacje podstawowe.

View File

@@ -1,14 +0,0 @@
title: "Promocja świąteczna"
visible: true
intro: Przykładowo gdybysmy dodali promocję do dokumentów
content: |
Jeśli kupujesz w sklepach internetowych, prawdopodobnie co pewien czas natykasz się na opisy, które nie zachęcają do zakupów.
Do najczęściej powtarzanych błędów opisów produktów należą:
- brak konkretów klient chce wiedzieć, z czego produkt jest wykonany, jakie ma wymiary czy funkcje, a nie tylko, że jest „wysokiej jakości”;
- zbyt techniczny język warto dostosować ton komunikacji do odbiorcy, unikając skomplikowanych terminów (i w drugą stronę jeśli sprzedajesz towar skierowany do profesjonalistów, nie trzeba w opisie ze szczegółami wyjaśniać, jak działa czy do czego służy);
- brak narracji storytelling w opisach produktów pomaga zbudować emocjonalne zaangażowanie klienta;
ignorowanie pytań klientów warto analizować najczęstsze pytania i uwzględniać odpowiedzi w opisach; jeśli np. często dostajesz zapytania dotyczące tego, czy produkt jest wodoodporny, lepiej napisać o tym od razu w opisie;
zbyt długie i skomplikowane opisy należy dbać o przejrzystość treści, używać krótkich akapitów i list wypunktowanych; klient poszukuje konkretów, a nie zawiłych opowieści, które trudno się czyta.

View File

@@ -1,6 +1,6 @@
page: page:
title: "Dokumenty - FUZ Adam Rojek | Regulaminy i Umowy" 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." description: "Dokumenty i regulaminy, cenniki oraz umowy związane z usługami oferowanymi przez naszą firmę."
image: "/og/dokumenty-og.png" image: "/og/dokumenty-og.png"
url: "/dokumenty" url: "/dokumenty"
keywords: keywords:

View File

@@ -1,6 +1,6 @@
page: page:
title: "FUZ Adam Rojek - Internet Światłowodowy Wyszków | Szybki i Stabilny" title: "FUZ Adam Rojek - Internet Światłowodowy Wyszków"
description: "Internet światłowodowy w Wyszkowie i okolicach. Lokalny operator z doświadczeniem - stabilne łącze, profesjonalny serwis, konkurencyjne ceny. Sprawdź dostępność!" description: "Internet światłowodowy w Wyszkowie i okolicach. Lokalny operator z doświadczeniem - stabilne łącze, profesjonalny serwis, konkurencyjne ceny."
image: "/og/home-og.png" image: "/og/home-og.png"
url: "/" url: "/"
keywords: keywords:

View File

@@ -4,11 +4,9 @@ opis: |
Wybierz rodzaj budynku i czas trwania umowy Wybierz rodzaj budynku i czas trwania umowy
uwaga: | uwaga: |
Powyższe „ceny brutto z Rabatami 15zł” 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 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 e-mail oraz za pośrednictwem EBOK;
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 -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
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 obowiązującego prawa przewidują wyłącznie zawiadomienia poprzez publiczne ogłoszenie), jak również kontakt w ramach procedur reklamacyjnych, w tym w szczególności przesłania odpowiedzi na reklamację, na podany w Umowie adres poczty elektronicznej).
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." cena_opis: "zł/mies."
cards: cards:

View File

@@ -1,3 +1,5 @@
title: Możliwości telewizji JAMBOX
description: Poznaj funkcje i udogodnienia dostępne na dekoderach telewizji JAMBOX.
sections: sections:
- title: CatchUp - Archiwum TV - title: CatchUp - Archiwum TV
image: https://www.jambox.pl/sites/default/files/jambox-kyanit-catchup1.png image: https://www.jambox.pl/sites/default/files/jambox-kyanit-catchup1.png

View File

@@ -1,7 +1,7 @@
import { useMemo } from "preact/hooks"; import { useMemo } from "preact/hooks";
import { marked } from "marked"; import { marked } from "marked";
import { useLocalSearch } from "../../hooks/useLocalSearch.js"; import { useLocalSearch } from "../../hooks/useLocalSearch.js";
import { highlightText, highlightHtml } from "../../lib/highlightUtils.js"; import { highlightText, highlightHtml } from "../../lib/highlightUtils.jsx";
import "../../styles/jambox-search.css"; import "../../styles/jambox-search.css";
/** /**
@@ -46,6 +46,7 @@ export default function JamboxMozliwosciSearch({ items = [] }) {
<div className="f-chsearch__top"> <div className="f-chsearch__top">
<div className="f-chsearch__inputwrap"> <div className="f-chsearch__inputwrap">
<input <input
name="search"
className="f-chsearch__input" className="f-chsearch__input"
type="search" type="search"
value={search.query} value={search.query}
@@ -85,6 +86,8 @@ export default function JamboxMozliwosciSearch({ items = [] }) {
className={`f-section-image ${reverse ? "md:order-1" : "md:order-2"}`} className={`f-section-image ${reverse ? "md:order-1" : "md:order-2"}`}
loading="lazy" loading="lazy"
decoding="async" decoding="async"
width="800"
height="600"
/> />
)} )}

View File

@@ -52,6 +52,7 @@ const jsonExtra = meta.extraSchema ? JSON.stringify(meta.extraSchema) : null;
/> />
<link rel="canonical" href={meta.canonical} /> <link rel="canonical" href={meta.canonical} />
<link rel="sitemap" type="application/xml" title="Sitemap" href="/sitemap.xml" />
<!-- OpenGraph --> <!-- OpenGraph -->
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />

View File

@@ -11,13 +11,44 @@ import Cookie from "../islands/Cookie.jsx";
import rawCookie from "../content/site/cookie.yaml?raw"; import rawCookie from "../content/site/cookie.yaml?raw";
const cookieCfg = yaml.parse(rawCookie); const cookieCfg = yaml.parse(rawCookie);
const { seo } = Astro.props;
// ✅ Pobierz wszystkie możliwe props
const {
seo, // Pełny obiekt SEO (stary sposób)
title, // Indywidualny title (nowy sposób)
description, // Indywidualny description (nowy sposób)
image, // Opcjonalny image
keywords, // Opcjonalne keywords
url, // Opcjonalny url
} = Astro.props;
// ✅ PRIORYTET: title/description → seo → undefined
let finalSeo;
// Jeśli mamy indywidualne pola (title lub description) - użyj ich
if (title || description) {
finalSeo = {
page: {
title,
description,
image,
keywords,
url,
},
};
}
// Jeśli nie ma indywidualnych, ale jest seo object - użyj go
else if (seo) {
finalSeo = seo;
}
// Jeśli nic nie ma - undefined (użyje global defaults z BaseHead)
else {
finalSeo = undefined;
}
--- ---
<html lang="pl" class="scroll-smooth"> <html lang="pl" class="scroll-smooth">
<head> <BaseHead seo={finalSeo} />
<BaseHead seo={seo} />
</head>
<body class="min-h-screen flex flex-col"> <body class="min-h-screen flex flex-col">
<Header /> <Header />

View File

@@ -396,3 +396,90 @@ export function getRequiredEnv(key: string): string {
return value; return value;
} }
// ==================== SITEMAP HELPERS ====================
export type SitemapUrl = {
loc: string;
lastmod?: string;
changefreq?: 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never';
priority?: number;
};
/**
* Generuje sitemap XML z listy URL-i
* @param urls - Tablica URL-i do sitemap
* @returns String z XML sitemap
*
* @example
* const urls = [
* { loc: 'https://example.com/', priority: 1.0, changefreq: 'daily' },
* { loc: 'https://example.com/about', priority: 0.8, changefreq: 'monthly' }
* ];
* const xml = generateSitemapXml(urls);
*/
export function generateSitemapXml(urls: SitemapUrl[]): string {
const urlEntries = urls.map(url => {
const parts = [
' <url>',
` <loc>${url.loc}</loc>`,
];
if (url.lastmod) {
parts.push(` <lastmod>${url.lastmod}</lastmod>`);
}
if (url.changefreq) {
parts.push(` <changefreq>${url.changefreq}</changefreq>`);
}
if (url.priority !== undefined) {
parts.push(` <priority>${url.priority.toFixed(1)}</priority>`);
}
parts.push(' </url>');
return parts.join('\n');
}).join('\n');
return `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${urlEntries}
</urlset>`;
}
/**
* Automatyczne określenie changefreq na podstawie ścieżki URL
* @param path - Ścieżka URL (np. "/internet-swiatlowodowy")
* @returns Częstotliwość zmian
*
* @example
* inferChangeFreq('/') // => 'daily'
* inferChangeFreq('/dokumenty/regulamin') // => 'yearly'
*/
export function inferChangeFreq(path: string): SitemapUrl['changefreq'] {
if (path === '/') return 'daily';
if (path.includes('/internet-') || path.includes('/telefon')) return 'weekly';
if (path.includes('/mapa-zasiegu') || path.includes('/kontakt')) return 'monthly';
if (path.includes('/dokumenty')) return 'yearly';
if (path.includes('/premium')) return 'monthly';
return 'weekly';
}
/**
* Automatyczne określenie priority na podstawie ścieżki URL
* @param path - Ścieżka URL
* @returns Priorytet (0.0 - 1.0)
*
* @example
* inferPriority('/') // => 1.0
* inferPriority('/dokumenty/polityka-prywatnosci') // => 0.5
*/
export function inferPriority(path: string): number {
if (path === '/') return 1.0;
if (path.includes('/internet-') || path.includes('/telefon')) return 0.9;
if (path.includes('/mapa-zasiegu') || path.includes('/kontakt')) return 0.8;
if (path.includes('/premium')) return 0.7;
if (path.includes('/telewizja-mozliwosci')) return 0.7;
if (path.includes('/dokumenty')) return 0.5;
return 0.7;
}

View File

@@ -4,14 +4,15 @@ import yaml from "js-yaml";
export type DocYaml = { export type DocYaml = {
title: string; title: string;
description: string;
visible?: boolean; visible?: boolean;
intro?: string;
content: string; content: string;
}; };
export type DocEntry = DocYaml & { export type DocEntry = DocYaml & {
slug: string; slug: string;
file: string; file: string;
description: string;
}; };
const DOCS_DIR = path.join(process.cwd(), "src", "content", "document"); const DOCS_DIR = path.join(process.cwd(), "src", "content", "document");
@@ -34,13 +35,14 @@ export function listDocuments(): DocEntry[] {
if (!data.title || typeof data.title !== "string") continue; if (!data.title || typeof data.title !== "string") continue;
if (!data.content || typeof data.content !== "string") continue; if (!data.content || typeof data.content !== "string") continue;
if (!data.description || typeof data.description !== "string") continue;
items.push({ items.push({
slug, slug,
file, file,
title: data.title, title: data.title,
description: data.description,
visible: data.visible ?? false, visible: data.visible ?? false,
intro: data.intro ?? "",
content: data.content, content: data.content,
}); });
} }
@@ -60,14 +62,15 @@ export function getDocumentBySlug(slug: string): DocEntry | null {
if (!data.title || typeof data.title !== "string") return null; if (!data.title || typeof data.title !== "string") return null;
if (!data.content || typeof data.content !== "string") return null; if (!data.content || typeof data.content !== "string") return null;
if (!data.description || typeof data.description !== "string") continue;
return { return {
slug, slug,
file, file,
title: data.title, title: data.title,
visible: data.visible ?? false, visible: data.visible ?? false,
intro: data.intro ?? "",
content: data.content, content: data.content,
description: data.description,
}; };
} }

View File

@@ -15,7 +15,7 @@ if (!doc || doc.visible !== true) {
const html = marked.parse(doc.content); const html = marked.parse(doc.content);
--- ---
<DefaultLayout title={doc.title}> <DefaultLayout title={doc.title} description={doc.description}>
<section class="f-section"> <section class="f-section">
<div class="f-section-grid-single"> <div class="f-section-grid-single">
<a href="/dokumenty" class="f-document-link"> <a href="/dokumenty" class="f-document-link">

View File

@@ -6,72 +6,94 @@ import {
normalizePublicHref, normalizePublicHref,
type DocumentsYaml, type DocumentsYaml,
} from "../../lib/astro-helpers"; } from "../../lib/astro-helpers";
import "../../styles/document.css";
const doc = loadYaml<DocumentsYaml>("./src/content/document/documents.yaml"); const doc = loadYaml<DocumentsYaml>("./src/content/document/documents.yaml");
const seo = loadYaml("./src/content/document/seo.yaml"); const seo = loadYaml("./src/content/document/seo.yaml");
const pageTitle = doc?.tytul ?? "Dokumenty"; const pageTitle = doc?.tytul ?? "Dokumenty";
const pageDesc = doc?.opis ?? ""; const pageDesc = doc?.opis ?? "";
const groups = doc?.grupy ?? {}; const groups = doc?.grupy ?? {};
const left = groups["otworz"] ?? {};
const right = groups["pobierz"] ?? {};
--- ---
<DefaultLayout seo={seo}> <DefaultLayout seo={seo}>
{/* CONTENT */}
<section class="f-section"> <section class="f-section">
<div class="f-section-grid-top md:grid-cols-2 gap-10 items-start"> <div class="f-section-grid md:grid-cols-1">
{/* ===== LEWA CZYTAJ ===== */} <h1 class="f-section-title">{pageTitle}</h1>
<div> {pageDesc && <Markdown text={pageDesc} />}
<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>
)
}
</div> </div>
</section>
<div> <section class="f-section">
<h3 class="f-section-title">{right.tytul ?? "Pobierz"}</h3> <div class="f-section-documents">
<div class="f-documents-columns">
{ {
!right.pliki?.length ? ( Object.entries(groups).map(([key, group]) => (
<p class="opacity-70 mt-4">Brak plików.</p> <div class="f-documents-group" key={key}>
<h3 class="f-section-title3">{group.tytul}</h3>
{!group.pliki?.length ? (
<p class="f-documents-empty">Brak dokumentów.</p>
) : ( ) : (
<div class="f-documents-grid"> <div class="f-documents-list">
{right.pliki.map((p) => { {group.pliki.map((p) => {
const href = normalizePublicHref(p.file); // Określ czy to slug (czytaj) czy file (pobierz)
const isRead = !!p.slug;
const href = isRead
? `/dokumenty/${p.slug}`
: normalizePublicHref(p.file);
if (!href) return null; if (!href) return null;
return ( return (
<a <a
class="f-document-card" class="f-document-link"
href={href} href={href}
download {...(isRead ? {} : { download: true })}
title={p.nazwa} title={p.nazwa}
> >
<div class="f-document-title">{p.nazwa}</div> <span class="f-document-icon">
{isRead ? (
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z" />
<circle cx="12" cy="12" r="3" />
</svg>
) : (
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<polyline points="7 10 12 15 17 10" />
<line x1="12" x2="12" y1="15" y2="3" />
</svg>
)}
</span>
<span class="f-document-name">{p.nazwa}</span>
</a> </a>
); );
})} })}
</div> </div>
) )}
</div>
))
} }
</div> </div>
</div> </div>

View File

@@ -12,6 +12,8 @@ type YamlSection = {
}; };
type YamlData = { type YamlData = {
title?: string;
description?: string;
sections?: YamlSection[]; sections?: YamlSection[];
}; };
@@ -22,10 +24,16 @@ let items: Array<{
content: string; content: string;
}> = []; }> = [];
let pageTitle = "";
let pageDescription = "";
let err = ""; let err = "";
try { try {
const data = loadYaml<YamlData>("./src/content/internet-telewizja/telewizja-mozliwosci.yaml"); const data = loadYaml<YamlData>(
"./src/content/internet-telewizja/telewizja-mozliwosci.yaml",
);
pageTitle = data?.title || pageTitle;
pageDescription = data?.description || pageDescription;
const sections = safeArray<YamlSection>(data?.sections); const sections = safeArray<YamlSection>(data?.sections);
items = sections items = sections
@@ -42,7 +50,7 @@ try {
} }
--- ---
<DefaultLayout title="Możliwości JAMBOX"> <DefaultLayout title={pageTitle} description={pageDescription}>
<section class="f-section" id="top"> <section class="f-section" id="top">
<div class="f-section-grid-single"> <div class="f-section-grid-single">
<h1 class="f-section-title">Możliwości JAMBOX</h1> <h1 class="f-section-title">Możliwości JAMBOX</h1>

View File

@@ -42,17 +42,20 @@ const form = data.form;
<DefaultLayout seo={seo}> <DefaultLayout seo={seo}>
<section class="f-section"> <section class="f-section">
<!-- ✅ Zmieniona struktura - osobne bloki zamiast grida -->
<div class="f-contact-grid"> <div class="f-contact-grid">
{/* row 1: tytuły */}
<h1 class="f-section-title m-0">{data.title}</h1>
<h1 class="f-section-title m-0">{data.contactFormTitle}</h1>
{/* row 2: treść */} {/* Lewa kolumna: Kontakt */}
<div class="f-contact-column">
<h1 class="f-section-title">{data.title}</h1>
<div class="f-contact-item"> <div class="f-contact-item">
<Markdown text={data.description} /> <Markdown text={data.description} />
</div> </div>
</div>
<div id="form"> {/* Prawa kolumna: Formularz */}
<div class="f-contact-column" id="form">
<h2 class="f-section-title">{data.contactFormTitle}</h2>
<form id="contactForm" class="f-contact-form"> <form id="contactForm" class="f-contact-form">
<div class="f-contact-form-inner"> <div class="f-contact-form-inner">
@@ -135,6 +138,7 @@ const form = data.form;
</button> </button>
</form> </form>
</div> </div>
</div> </div>
<div class="mt-10"> <div class="mt-10">

View File

@@ -28,7 +28,7 @@ const seo = loadYaml("./src/content/mapa-zasiegu/seo.yaml");
class="w-full md:w-[340px] bg-[var(--f-background)] text-[var(--f-text)] 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" pt-6 px-6 flex flex-col gap-6 overflow-y-auto z-40"
> >
<h3 class="text-3xl">Sprawdź dostępność usług</h3> <h1 class="text-3xl">Sprawdź dostępność usług</h1>
<p class="text-sm"> <p class="text-sm">
Wybierz swoją miejscowość i ulicę oraz numer budynku, aby sprawdzić Wybierz swoją miejscowość i ulicę oraz numer budynku, aby sprawdzić
dostępność usług światłowodowych FUZ. dostępność usług światłowodowych FUZ.
@@ -49,7 +49,9 @@ const seo = loadYaml("./src/content/mapa-zasiegu/seo.yaml");
/> />
</div> </div>
</section> </section>
<div class="container md:pl-16">
<SectionRenderer src="./src/content/site/area-section.yaml" /> <SectionRenderer src="./src/content/site/area-section.yaml" />
</div>
<script is:inline> <script is:inline>
let fiberLayer = null; let fiberLayer = null;

View File

@@ -1,49 +0,0 @@
import { globby } from 'globby';
export async function GET() {
const base = "https://www.fuz.pl";
// Pobieramy wszystkie pliki .astro
const files = await globby([
"src/pages/**/*.astro",
"!src/pages/_*.astro", // pomiń pliki zaczynające się od _
"!src/pages/**/[...*", // pomiń catch-all
"!src/pages/**/[**", // pomiń dynamiczne parametry
"!src/pages/sitemap.xml.js", // pomiń samą sitemapę
"!src/pages/api/**" // pomiń API endpoints
]);
// Konwersja ścieżek plikowych → URL-e
const urls = files.map((file) => {
let url = file
.replace("src/pages", "")
.replace(".astro", "");
// obsługa index: /index.astro → /
if (url.endsWith("/index")) {
url = url.replace("/index", "");
}
return url;
});
const body = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${urls
.map((url) => {
return `
<url>
<loc>${base}${url}</loc>
<changefreq>weekly</changefreq>
<priority>${url === "/" ? "1.0" : "0.8"}</priority>
</url>`;
})
.join("")}
</urlset>`;
return new Response(body, {
headers: {
"Content-Type": "application/xml",
},
});
}

100
src/pages/sitemap.xml.ts Normal file
View File

@@ -0,0 +1,100 @@
import type { APIRoute } from 'astro';
import {
getEnv,
generateSitemapXml,
inferChangeFreq,
inferPriority,
type SitemapUrl
} from '../lib/astro-helpers';
import { listDocuments } from '../lib/documents';
/**
* Dynamiczny sitemap generator
* GET /sitemap.xml
*/
export const GET: APIRoute = async ({ site }) => {
const base = site?.toString().replace(/\/$/, '') ||
getEnv('PUBLIC_SITE_URL') ||
"https://www.fuz.pl";
const now = new Date().toISOString();
const urls: SitemapUrl[] = [];
// ========================================
// STATYCZNE STRONY
// ========================================
const staticPages = [
'/',
'/internet-swiatlowodowy',
'/internet-telewizja',
'/telefon',
'/mapa-zasiegu',
'/kontakt',
'/dokumenty',
'/premium',
'/telewizja-mozliwosci',
];
staticPages.forEach(path => {
urls.push({
loc: `${base}${path}`,
lastmod: now,
changefreq: inferChangeFreq(path),
priority: inferPriority(path),
});
});
// ========================================
// DYNAMICZNE STRONY: Dokumenty
// ========================================
try {
const docs = listDocuments();
docs
.filter(d => d.visible === true)
.forEach(d => {
const path = `/dokumenty/${d.slug}`;
urls.push({
loc: `${base}${path}`,
lastmod: now,
changefreq: 'yearly',
priority: 0.5,
});
});
} catch (e) {
console.warn('⚠️ Sitemap: Could not load documents:', e);
}
// ========================================
// DYNAMICZNE STRONY: Premium packages (opcjonalnie)
// ========================================
// TODO: Jeśli masz dynamiczne pakiety premium, dodaj tutaj:
/*
try {
const packages = await loadPremiumPackages();
packages.forEach(p => {
urls.push({
loc: `${base}/premium/${p.tid}`,
lastmod: now,
changefreq: 'monthly',
priority: 0.7,
});
});
} catch (e) {
console.warn('⚠️ Sitemap: Could not load premium packages:', e);
}
*/
// ========================================
// GENERUJ XML
// ========================================
const sitemap = generateSitemapXml(urls);
return new Response(sitemap, {
status: 200,
headers: {
'Content-Type': 'application/xml; charset=utf-8',
'Cache-Control': 'public, max-age=3600', // 1h cache
},
});
};

View File

@@ -1,15 +1,37 @@
.f-document-card { .f-section-documents {
@apply flex items-center gap-2 text-lg; @apply max-w-7xl mx-auto px-4 sm:px-6 lg:px-8;
} }
.f-document-card:hover { .f-documents-columns {
@apply shadow-sm; @apply grid grid-cols-1 gap-8 mt-1;
transform: translateY(-1px);
} }
/* .f-document-icon { @media (min-width: 768px) {
@apply text-2xl leading-none mt-1; .f-documents-columns {
} */ @apply grid-cols-2;
}
.f-document-title { }
@apply font-normal ;
@media (min-width: 1024px) {
.f-documents-columns {
@apply grid-cols-3;
}
}
.f-section-title3{
@apply border-b-[1px] border-[var(--f-offers-border)];
}
.f-documents-group {
@apply bg-[--f-bg] text-[--f-text] border border-[--f-offers-border] rounded-2xl shadow-md p-6 relative flex flex-col gap-4;
}
.f-documents-list {
@apply flex flex-col gap-1;
}
.f-document-link {
@apply flex items-center py-1 gap-2 rounded-lg text-[var(--f-text,#111827)] no-underline transition-all duration-200 bg-transparent border border-transparent;
}
.f-document-icon {
@apply flex-shrink-0 w-5 h-5 transition-all duration-200 text-[var(--f-text-muted,#6b7280)];
} }

View File

@@ -121,3 +121,30 @@
padding: 0 !important; padding: 0 !important;
background: transparent !important; background: transparent !important;
} }
.fuz-cities-box {
background: var(--f-background);
color: var(--f-text);
padding: 16px;
}
.fuz-cities-title {
font-size: 1.2rem;
font-weight: 600;
margin-bottom: 12px;
}
.fuz-cities-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
gap: 6px 14px;
font-size: 0.95rem;
line-height: 1.4;
}
.fuz-city-item {
color: var(--f-text);
}

View File

@@ -41,6 +41,10 @@
@apply text-4xl md:text-5xl font-bold mb-2 text-[--f-header]; @apply text-4xl md:text-5xl font-bold mb-2 text-[--f-header];
} }
.f-section-title3 {
@apply text-2xl md:text-2xl font-bold mb-2 text-[--f-header];
}
.f-section-nav { .f-section-nav {
@apply mt-0 flex justify-center; @apply mt-0 flex justify-center;
} }