diff --git a/gen_images.py b/gen_images.py
new file mode 100644
index 0000000..44d6009
--- /dev/null
+++ b/gen_images.py
@@ -0,0 +1,534 @@
+#!/usr/bin/env python3
+"""Pobiera zdjęcia obiektów noclegowych do katalogu pages/img/.
+
+Dla każdej oferty (`type: oferta*`) z polem `www` we frontmatterze skrypt
+otwiera stronę obiektu i wyciąga z niej 3–5 zdjęć:
+
+ 1. og:image / twitter:image — zdjęcie, które obiekt sam wybrał na wizytówkę,
+ 2. galeria ze strony —
,
+ oraz background-image w stylach inline.
+
+Kandydaci są odsiewani dwoma filtrami, w tej kolejności: po nazwie pliku
+(logotypy, ikony, mapy, piksele śledzące) i po samym pliku — waga > 30 KB
+oraz proporcje w przedziale krajobrazowym (patrz MIN_RATIO/MAX_RATIO,
+MIN_WIDTH). Co przejdzie, ląduje jako JPEG przeskalowany do MAX_WIDTH px
+szerokości, jeśli w systemie jest Pillow; bez Pillow zapisywany jest oryginał
+z własnym rozszerzeniem.
+
+Zapis: pages/img//01.jpg, 02.jpg, ... — `` to pole `id`
+z frontmattera. Mapowanie oferta → zdjęcia jest wyłącznie konwencją
+katalogową: pliki .md NIE są modyfikowane, tak samo jak w gen_pages.py
+i gen_viz.py. Katalog czyta potem gen_pages.py (galeria na karcie, miniatury
+w tabeli porównawczej, tło kafli regionów).
+
+Błąd jednej oferty (strona nie oddaje zdjęć, blokada scrapingu, timeout) nie
+przerywa reszty — trafia na listę braków wypisywaną na końcu. Znany przypadek:
+booking.com odrzuca automatyczne pobrania i taka oferta zostaje bez zdjęć.
+
+Uruchomienie:
+ python3 gen_images.py # pomija oferty, które mają już zdjęcia
+ python3 gen_images.py --force # pobiera wszystko od nowa
+ python3 gen_images.py --only kristall-schattbergxpress
+"""
+
+from __future__ import annotations
+
+import argparse
+import io
+import re
+import ssl
+import sys
+import urllib.error
+import urllib.request
+from dataclasses import dataclass, field
+from html.parser import HTMLParser
+from pathlib import Path
+from urllib.parse import unquote, urljoin, urlsplit
+
+import yaml
+
+BUNDLE_ROOT = Path(__file__).resolve().parent
+IMG_DIR = BUNDLE_ROOT / "pages" / "img"
+
+WANTED = 5 # ile zdjęć chcemy na obiekt
+MIN_WANTED = 3 # poniżej tego oferta idzie na listę „niepełnych”
+MAX_CANDIDATES = 40 # ile adresów maksymalnie próbujemy pobrać
+MIN_BYTES = 30 * 1024
+MIN_WIDTH = 600
+MIN_RATIO = 0.85 # wysokość ≥ szerokości to zwykle baner/logo w pionie
+MAX_RATIO = 2.60 # a bardzo szerokie paski to nagłówki, nie zdjęcia
+MAX_TRANSPARENT = 0.03 # udział pikseli z alfą — powyżej to logo/plakietka
+MAX_FLAT = 0.35 # udział jednego koloru — powyżej to grafika, nie zdjęcie
+MAX_WIDTH = 1200
+TIMEOUT = 20
+
+USER_AGENT = (
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) "
+ "Chrome/126.0 Safari/537.36"
+)
+
+IMAGE_EXTS = (".jpg", ".jpeg", ".png", ".webp")
+
+_RETINA_RE = re.compile(r"@[234]x(?=\.[a-z]+$)")
+MAX_HASH_DISTANCE = 10 # dystans Hamminga, poniżej którego to to samo zdjęcie
+
+# Nazwy plików, które prawie nigdy nie są zdjęciem obiektu.
+NOISE_RE = re.compile(
+ r"(logo|icon|favicon|sprite|pixel|tracking|placeholder|avatar|banner|"
+ r"button|arrow|flag|map|karte|mappa|plan|piktogram|wetter|weather|"
+ r"footer|header|badge|award|siegel|social|whatsapp|facebook|instagram|"
+ r"partner|gutschein|zertifikat|prospekt|katalog)",
+ re.I,
+)
+
+try:
+ from PIL import Image
+except ModuleNotFoundError: # pragma: no cover
+ Image = None
+
+PILLOW = Image is not None
+
+
+# --- oferty z bundle'a -------------------------------------------------
+
+
+@dataclass
+class Offer:
+ concept_id: str
+ offer_id: str
+ name: str
+ url: str
+ operator: str = ""
+
+
+def load_offers() -> list[Offer]:
+ offers: list[Offer] = []
+ for path in sorted(BUNDLE_ROOT.rglob("*.md")):
+ if ".git" in path.parts or "pages" in path.parts:
+ continue
+ text = path.read_text(encoding="utf-8")
+ if not text.startswith("---\n"):
+ continue
+ end = text.find("\n---", 3)
+ if end == -1:
+ continue
+ frontmatter = yaml.safe_load(text[4:end]) or {}
+ if not str(frontmatter.get("type", "")).startswith("oferta"):
+ continue
+ offers.append(
+ Offer(
+ concept_id=path.relative_to(BUNDLE_ROOT).with_suffix("").as_posix(),
+ offer_id=str(frontmatter.get("id") or "").strip(),
+ name=str(frontmatter.get("nazwa") or frontmatter.get("title") or ""),
+ url=str(frontmatter.get("www") or "").strip(),
+ operator=str(frontmatter.get("operator") or ""),
+ )
+ )
+ return offers
+
+
+# --- pobieranie --------------------------------------------------------
+
+_LAX_SSL = ssl.create_default_context()
+_LAX_SSL.check_hostname = False
+_LAX_SSL.verify_mode = ssl.CERT_NONE
+
+
+def fetch(url: str, referer: str = "") -> tuple[bytes, str]:
+ """Zwraca (bajty, content-type). Rzuca OSError/HTTPError przy porażce."""
+ headers = {
+ "User-Agent": USER_AGENT,
+ "Accept": "text/html,application/xhtml+xml,image/avif,image/webp,*/*;q=0.8",
+ "Accept-Language": "pl,en;q=0.8,de;q=0.6",
+ }
+ if referer:
+ headers["Referer"] = referer
+ request = urllib.request.Request(url, headers=headers)
+ try:
+ with urllib.request.urlopen(request, timeout=TIMEOUT) as response:
+ return response.read(), response.headers.get("Content-Type", "")
+ except urllib.error.URLError as exc:
+ # Część małych obiektów ma łańcuch certyfikatów niepełny — dla zdjęć
+ # z publicznej strony to akceptowalne ryzyko, więc jedna próba obok.
+ if isinstance(getattr(exc, "reason", None), ssl.SSLError):
+ with urllib.request.urlopen(
+ request, timeout=TIMEOUT, context=_LAX_SSL
+ ) as response:
+ return response.read(), response.headers.get("Content-Type", "")
+ raise
+
+
+# --- wyciąganie adresów zdjęć ze strony --------------------------------
+
+
+class ImageCollector(HTMLParser):
+ """Zbiera kandydatów na zdjęcia: og:image,
, , tła."""
+
+ def __init__(self) -> None:
+ super().__init__(convert_charrefs=True)
+ self.meta: list[str] = []
+ self.gallery: list[str] = []
+
+ def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
+ data = {k.lower(): (v or "") for k, v in attrs}
+ if tag == "meta":
+ key = (data.get("property") or data.get("name") or "").lower()
+ if key in {"og:image", "og:image:url", "og:image:secure_url", "twitter:image"}:
+ self._add(self.meta, data.get("content", ""))
+ elif tag == "img":
+ for attribute in ("data-src", "data-lazy-src", "data-original", "src"):
+ if data.get(attribute):
+ self._add(self.gallery, data[attribute])
+ break
+ for attribute in ("srcset", "data-srcset"):
+ for part in data.get(attribute, "").split(","):
+ url = part.strip().split(" ")[0]
+ self._add(self.gallery, url)
+ elif tag == "a":
+ href = data.get("href", "")
+ if href.split("?")[0].lower().endswith(IMAGE_EXTS):
+ self._add(self.gallery, href)
+ if "style" in data:
+ for match in re.finditer(r"url\(['\"]?([^)'\"]+)", data["style"]):
+ self._add(self.gallery, match.group(1))
+
+ def _add(self, bucket: list[str], url: str) -> None:
+ url = (url or "").strip()
+ if url and not url.startswith("data:") and url not in bucket:
+ bucket.append(url)
+
+
+def candidates(page_url: str, markup: str, tokens: list[str] = ()) -> list[str]:
+ collector = ImageCollector()
+ try:
+ collector.feed(markup)
+ except Exception: # parser HTML-a nie może wywrócić całego przebiegu
+ pass
+
+ ordered: list[str] = []
+ seen: set[str] = set()
+ for url in collector.meta + collector.gallery:
+ absolute = urljoin(page_url, url)
+ if not absolute.lower().startswith(("http://", "https://")):
+ continue
+ path = urlsplit(absolute).path
+ if path.lower().endswith(".svg") or NOISE_RE.search(path):
+ continue
+ # To samo zdjęcie wraca w srcset w kilku gęstościach (@2x, @3x)
+ # i z parametrami skalowania w query — liczy się raz.
+ key = _RETINA_RE.sub("", unquote(path).lower())
+ if key in seen:
+ continue
+ seen.add(key)
+ ordered.append(absolute)
+
+ # Na stronach portalowych (marka z wieloma obiektami, katalog noclegów)
+ # w tym samym HTML-u siedzą zdjęcia cudzych obiektów. Jeśli którykolwiek
+ # adres niesie w ścieżce nazwę własną obiektu, bierzemy tylko takie —
+ # a gdy żaden jej nie niesie, zostaje pełna lista (strona własna obiektu).
+ if tokens:
+ relevant = [
+ url
+ for url in ordered
+ if any(token in unquote(urlsplit(url).path).lower() for token in tokens)
+ ]
+ if relevant:
+ ordered = relevant
+ return ordered[:MAX_CANDIDATES]
+
+
+# Człony nazw, które nie identyfikują obiektu — są w co drugiej ofercie.
+GENERIC_TOKENS = {
+ "apartment", "apartments", "appartement", "appartementhaus", "apart",
+ "hotel", "house", "residence", "chalet", "lodge", "ferienwohnung",
+ "wohnung", "central", "sport", "sportchalet",
+}
+
+
+def object_tokens(offer: Offer) -> list[str]:
+ """Nazwy własne obiektu, po których poznamy jego zdjęcia na portalu.
+
+ Odpadają człony rodzajowe („apartments”), nazwa operatora („AlpenParks”)
+ i wszystko, co siedzi już w domenie — jeśli cała witryna należy do
+ obiektu, filtrowanie po nazwie nie ma czego zawężać.
+ """
+ host = urlsplit(offer.url).netloc.lower().replace("-", "").replace(".", "")
+ brand = re.sub(r"[^a-z0-9]", "", (offer.operator or "").lower())
+ tokens = []
+ for part in re.split(r"[^a-z0-9]+", offer.offer_id.lower()):
+ if len(part) < 5 or part in GENERIC_TOKENS:
+ continue
+ if part in host or (brand and part in brand):
+ continue
+ tokens.append(part)
+ return tokens
+
+
+def object_page(page_url: str, markup: str, tokens: list[str]) -> str:
+ """Podstrona obiektu wylinkowana ze strony głównej marki/portalu."""
+ if not tokens or urlsplit(page_url).path.strip("/"):
+ return "" # weszliśmy już w konkretną podstronę, nie w korzeń
+ for match in re.finditer(r'href=["\']([^"\'#]+)["\']', markup, re.I):
+ target = urljoin(page_url, match.group(1))
+ if urlsplit(target).netloc != urlsplit(page_url).netloc:
+ continue
+ path = unquote(urlsplit(target).path).lower()
+ if path.strip("/") and any(token in path for token in tokens):
+ return target
+ return ""
+
+
+def decode(raw: bytes, content_type: str) -> str:
+ match = re.search(r"charset=([\w-]+)", content_type, re.I)
+ if not match:
+ match = re.search(rb'charset=["\']?([\w-]+)', raw[:4096], re.I)
+ encoding = match.group(1).decode("ascii", "ignore") if match else "utf-8"
+ else:
+ encoding = match.group(1)
+ try:
+ return raw.decode(encoding, "replace")
+ except LookupError:
+ return raw.decode("utf-8", "replace")
+
+
+# --- zapis -------------------------------------------------------------
+
+
+def acceptable(raw: bytes, seen: list[int] = ()) -> tuple[bool, str, int]:
+ """(czy bierzemy, powód odrzucenia, odcisk percepcyjny)."""
+ if raw[:1024].lstrip().lower().startswith((b" MAX_TRANSPARENT:
+ return False, f"grafika z przezroczystością ({transparent:.0%})", 0
+ if flat > MAX_FLAT:
+ return False, f"płaskie tło ({flat:.0%} jednego koloru) — grafika", 0
+ # To samo zdjęcie bywa na stronie pod kilkoma adresami i w kilku
+ # rozdzielczościach — po odcisku poznajemy je niezależnie od pliku.
+ for other in seen:
+ if bin(fingerprint ^ other).count("1") <= MAX_HASH_DISTANCE:
+ return False, "duplikat wcześniejszego zdjęcia", fingerprint
+ return True, "", fingerprint
+
+
+def _ahash(image: "Image.Image") -> int:
+ """Odcisk percepcyjny (average hash) — odporny na zmianę rozdzielczości."""
+ pixels = image.convert("L").resize((8, 8)).tobytes()
+ average = sum(pixels) / len(pixels)
+ return sum(1 << n for n, value in enumerate(pixels) if value > average)
+
+
+def _transparency(image: "Image.Image") -> float:
+ if image.mode not in ("RGBA", "LA", "P") or (
+ image.mode == "P" and "transparency" not in image.info
+ ):
+ return 0.0
+ alpha = image.convert("RGBA").getchannel("A").resize((48, 48))
+ histogram = alpha.getcolors(256) or []
+ total = sum(count for count, _ in histogram) or 1
+ return sum(count for count, value in histogram if value < 250) / total
+
+
+def _flatness(image: "Image.Image") -> float:
+ small = image.convert("RGB").resize((48, 48))
+ histogram = small.getcolors(48 * 48)
+ if not histogram: # więcej kolorów niż pikseli próbki — na pewno zdjęcie
+ return 0.0
+ total = sum(count for count, _ in histogram) or 1
+ return max(count for count, _ in histogram) / total
+
+
+def save(raw: bytes, directory: Path, index: int, source_url: str) -> Path:
+ directory.mkdir(parents=True, exist_ok=True)
+ if PILLOW:
+ with Image.open(io.BytesIO(raw)) as image:
+ image = image.convert("RGB")
+ if image.width > MAX_WIDTH:
+ height = round(image.height * MAX_WIDTH / image.width)
+ image = image.resize((MAX_WIDTH, height), Image.LANCZOS)
+ out = directory / f"{index:02d}.jpg"
+ image.save(out, "JPEG", quality=82, optimize=True, progressive=True)
+ return out
+ suffix = Path(urlsplit(source_url).path).suffix.lower()
+ out = directory / f"{index:02d}{suffix if suffix in IMAGE_EXTS else '.jpg'}"
+ out.write_bytes(raw)
+ return out
+
+
+# --- przebieg na ofertę ------------------------------------------------
+
+
+@dataclass
+class Result:
+ offer: Offer
+ saved: list[Path] = field(default_factory=list)
+ reason: str = ""
+ source: str = "" # podstrona obiektu, jeśli weszliśmy głębiej niż `www`
+
+
+def collect(offer: Offer, force: bool) -> Result:
+ result = Result(offer=offer)
+ directory = IMG_DIR / offer.offer_id
+
+ if not offer.offer_id:
+ result.reason = "brak pola `id` we frontmatterze"
+ return result
+ if not offer.url:
+ result.reason = "brak pola `www` (oferta bez konkretnego obiektu)"
+ return result
+
+ existing = sorted(directory.glob("*")) if directory.is_dir() else []
+ if existing and not force:
+ result.saved = existing
+ result.reason = "pominięte — zdjęcia już są (--force nadpisze)"
+ return result
+
+ try:
+ raw, content_type = fetch(offer.url)
+ except Exception as exc:
+ result.reason = f"strona nie odpowiada: {_describe(exc)}"
+ return result
+
+ markup = decode(raw, content_type)
+ source_url = offer.url
+ tokens = object_tokens(offer)
+ deeper = object_page(offer.url, markup, tokens)
+ if deeper:
+ try:
+ raw, content_type = fetch(deeper)
+ markup = decode(raw, content_type)
+ source_url = deeper
+ result.source = deeper
+ except Exception:
+ pass # zostajemy przy stronie głównej
+
+ urls = candidates(source_url, markup, tokens)
+ if not urls:
+ result.reason = "strona nie zawiera kandydatów na zdjęcia (HTML bez galerii)"
+ return result
+
+ if force and directory.is_dir():
+ for old in directory.glob("*"):
+ old.unlink()
+
+ rejected: list[str] = []
+ hashes: list[int] = []
+ for url in urls:
+ if len(result.saved) >= WANTED:
+ break
+ try:
+ image_raw, image_type = fetch(url, referer=offer.url)
+ except Exception as exc:
+ rejected.append(f"{_short(url)}: {_describe(exc)}")
+ continue
+ if "image" not in image_type and not urlsplit(url).path.lower().endswith(
+ IMAGE_EXTS
+ ):
+ rejected.append(f"{_short(url)}: nie obrazek ({image_type})")
+ continue
+ ok, why, fingerprint = acceptable(image_raw, hashes)
+ if not ok:
+ rejected.append(f"{_short(url)}: {why}")
+ continue
+ hashes.append(fingerprint)
+ try:
+ result.saved.append(
+ save(image_raw, directory, len(result.saved) + 1, url)
+ )
+ except Exception as exc:
+ rejected.append(f"{_short(url)}: zapis — {_describe(exc)}")
+
+ if not result.saved:
+ head = "; ".join(rejected[:3]) or "brak przyczyny"
+ result.reason = f"żaden kandydat nie przeszedł filtrów ({head})"
+ elif len(result.saved) < MIN_WANTED:
+ result.reason = f"tylko {len(result.saved)} zdj. (cel: {MIN_WANTED}–{WANTED})"
+ return result
+
+
+def _describe(exc: Exception) -> str:
+ if isinstance(exc, urllib.error.HTTPError):
+ return f"HTTP {exc.code}"
+ reason = getattr(exc, "reason", None)
+ return f"{type(exc).__name__}: {reason or exc}"
+
+
+def _short(url: str) -> str:
+ name = urlsplit(url).path.rsplit("/", 1)[-1] or urlsplit(url).netloc
+ return name[:42]
+
+
+# --- main --------------------------------------------------------------
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--force", action="store_true", help="pobierz od nowa")
+ parser.add_argument("--only", metavar="ID", help="tylko ta oferta (pole `id`)")
+ args = parser.parse_args()
+
+ offers = load_offers()
+ if args.only:
+ offers = [o for o in offers if o.offer_id == args.only]
+ if not offers:
+ print(f"Nie ma oferty o id={args.only}", file=sys.stderr)
+ return 1
+
+ print(f"Pillow: {'jest — skalowanie do ' + str(MAX_WIDTH) + 'px' if PILLOW else 'BRAK — zapis oryginałów'}")
+ print(f"Ofert do przetworzenia: {len(offers)}\n")
+
+ results = []
+ for offer in offers:
+ print(f"→ {offer.offer_id or offer.concept_id} ({offer.url or 'brak www'})")
+ result = collect(offer, args.force)
+ results.append(result)
+ if result.saved:
+ names = ", ".join(p.name for p in result.saved)
+ print(f" {len(result.saved)} zdj.: {names}"
+ + (f" [{result.reason}]" if result.reason else ""))
+ else:
+ print(f" ✗ {result.reason}")
+
+ ok = [r for r in results if len(r.saved) >= MIN_WANTED]
+ partial = [r for r in results if 0 < len(r.saved) < MIN_WANTED]
+ missing = [r for r in results if not r.saved]
+
+ print(f"\nZe zdjęciami ({len(ok)}):")
+ for result in ok:
+ print(f" + {result.offer.offer_id} — {len(result.saved)}")
+ if partial:
+ print(f"\nNiepełne ({len(partial)}):")
+ for result in partial:
+ print(f" ~ {result.offer.offer_id} — {result.reason}")
+ if missing:
+ print(f"\nBez zdjęć ({len(missing)}):")
+ for result in missing:
+ print(f" ! {result.offer.offer_id or result.offer.concept_id} — {result.reason}")
+
+ total = sum(p.stat().st_size for p in IMG_DIR.rglob("*") if p.is_file())
+ print(f"\nRazem w {IMG_DIR.relative_to(BUNDLE_ROOT)}: {total / 1024 / 1024:.1f} MiB")
+ print("Teraz: python3 gen_pages.py (wpina zdjęcia w karty i tabelę)")
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/pages/img/alpenparks-rehrenberg/01.jpg b/pages/img/alpenparks-rehrenberg/01.jpg
new file mode 100644
index 0000000..8f1d45d
Binary files /dev/null and b/pages/img/alpenparks-rehrenberg/01.jpg differ
diff --git a/pages/img/alpenparks-rehrenberg/02.jpg b/pages/img/alpenparks-rehrenberg/02.jpg
new file mode 100644
index 0000000..350c96b
Binary files /dev/null and b/pages/img/alpenparks-rehrenberg/02.jpg differ
diff --git a/pages/img/alpenparks-rehrenberg/03.jpg b/pages/img/alpenparks-rehrenberg/03.jpg
new file mode 100644
index 0000000..52607ff
Binary files /dev/null and b/pages/img/alpenparks-rehrenberg/03.jpg differ
diff --git a/pages/img/alpenparks-rehrenberg/04.jpg b/pages/img/alpenparks-rehrenberg/04.jpg
new file mode 100644
index 0000000..4eb7e47
Binary files /dev/null and b/pages/img/alpenparks-rehrenberg/04.jpg differ
diff --git a/pages/img/alpenparks-rehrenberg/05.jpg b/pages/img/alpenparks-rehrenberg/05.jpg
new file mode 100644
index 0000000..cf6e4ab
Binary files /dev/null and b/pages/img/alpenparks-rehrenberg/05.jpg differ
diff --git a/pages/img/alpenparks-sonnleiten/01.jpg b/pages/img/alpenparks-sonnleiten/01.jpg
new file mode 100644
index 0000000..d41fbf6
Binary files /dev/null and b/pages/img/alpenparks-sonnleiten/01.jpg differ
diff --git a/pages/img/alpenparks-sonnleiten/02.jpg b/pages/img/alpenparks-sonnleiten/02.jpg
new file mode 100644
index 0000000..fb864dc
Binary files /dev/null and b/pages/img/alpenparks-sonnleiten/02.jpg differ
diff --git a/pages/img/alpenparks-sonnleiten/03.jpg b/pages/img/alpenparks-sonnleiten/03.jpg
new file mode 100644
index 0000000..468510f
Binary files /dev/null and b/pages/img/alpenparks-sonnleiten/03.jpg differ
diff --git a/pages/img/alpenparks-sonnleiten/04.jpg b/pages/img/alpenparks-sonnleiten/04.jpg
new file mode 100644
index 0000000..6fb265d
Binary files /dev/null and b/pages/img/alpenparks-sonnleiten/04.jpg differ
diff --git a/pages/img/alpenparks-sonnleiten/05.jpg b/pages/img/alpenparks-sonnleiten/05.jpg
new file mode 100644
index 0000000..8469138
Binary files /dev/null and b/pages/img/alpenparks-sonnleiten/05.jpg differ
diff --git a/pages/img/apartment-brandau-kappl/01.jpg b/pages/img/apartment-brandau-kappl/01.jpg
new file mode 100644
index 0000000..735648c
Binary files /dev/null and b/pages/img/apartment-brandau-kappl/01.jpg differ
diff --git a/pages/img/apartment-brandau-kappl/02.jpg b/pages/img/apartment-brandau-kappl/02.jpg
new file mode 100644
index 0000000..4427336
Binary files /dev/null and b/pages/img/apartment-brandau-kappl/02.jpg differ
diff --git a/pages/img/apartment-brandau-kappl/03.jpg b/pages/img/apartment-brandau-kappl/03.jpg
new file mode 100644
index 0000000..83de3af
Binary files /dev/null and b/pages/img/apartment-brandau-kappl/03.jpg differ
diff --git a/pages/img/apartment-brandau-kappl/04.jpg b/pages/img/apartment-brandau-kappl/04.jpg
new file mode 100644
index 0000000..518a8cc
Binary files /dev/null and b/pages/img/apartment-brandau-kappl/04.jpg differ
diff --git a/pages/img/apartment-brandau-kappl/05.jpg b/pages/img/apartment-brandau-kappl/05.jpg
new file mode 100644
index 0000000..23655d6
Binary files /dev/null and b/pages/img/apartment-brandau-kappl/05.jpg differ
diff --git a/pages/img/apartments-kappl/01.jpg b/pages/img/apartments-kappl/01.jpg
new file mode 100644
index 0000000..309ba6d
Binary files /dev/null and b/pages/img/apartments-kappl/01.jpg differ
diff --git a/pages/img/apartments-kappl/02.jpg b/pages/img/apartments-kappl/02.jpg
new file mode 100644
index 0000000..51fee60
Binary files /dev/null and b/pages/img/apartments-kappl/02.jpg differ
diff --git a/pages/img/apartments-kappl/03.jpg b/pages/img/apartments-kappl/03.jpg
new file mode 100644
index 0000000..27123ff
Binary files /dev/null and b/pages/img/apartments-kappl/03.jpg differ
diff --git a/pages/img/apartments-kappl/04.jpg b/pages/img/apartments-kappl/04.jpg
new file mode 100644
index 0000000..846f448
Binary files /dev/null and b/pages/img/apartments-kappl/04.jpg differ
diff --git a/pages/img/apartments-kappl/05.jpg b/pages/img/apartments-kappl/05.jpg
new file mode 100644
index 0000000..8d33d6c
Binary files /dev/null and b/pages/img/apartments-kappl/05.jpg differ
diff --git a/pages/img/glemm-lodge/01.jpg b/pages/img/glemm-lodge/01.jpg
new file mode 100644
index 0000000..9d1832c
Binary files /dev/null and b/pages/img/glemm-lodge/01.jpg differ
diff --git a/pages/img/glemm-lodge/02.jpg b/pages/img/glemm-lodge/02.jpg
new file mode 100644
index 0000000..94bffa4
Binary files /dev/null and b/pages/img/glemm-lodge/02.jpg differ
diff --git a/pages/img/glemm-lodge/03.jpg b/pages/img/glemm-lodge/03.jpg
new file mode 100644
index 0000000..2254d41
Binary files /dev/null and b/pages/img/glemm-lodge/03.jpg differ
diff --git a/pages/img/glemm-lodge/04.jpg b/pages/img/glemm-lodge/04.jpg
new file mode 100644
index 0000000..b863695
Binary files /dev/null and b/pages/img/glemm-lodge/04.jpg differ
diff --git a/pages/img/glemm-lodge/05.jpg b/pages/img/glemm-lodge/05.jpg
new file mode 100644
index 0000000..6989935
Binary files /dev/null and b/pages/img/glemm-lodge/05.jpg differ
diff --git a/pages/img/haeuslerhof-valdaora/01.jpg b/pages/img/haeuslerhof-valdaora/01.jpg
new file mode 100644
index 0000000..f75d9df
Binary files /dev/null and b/pages/img/haeuslerhof-valdaora/01.jpg differ
diff --git a/pages/img/haeuslerhof-valdaora/02.jpg b/pages/img/haeuslerhof-valdaora/02.jpg
new file mode 100644
index 0000000..49d37d6
Binary files /dev/null and b/pages/img/haeuslerhof-valdaora/02.jpg differ
diff --git a/pages/img/haeuslerhof-valdaora/03.jpg b/pages/img/haeuslerhof-valdaora/03.jpg
new file mode 100644
index 0000000..d5fba83
Binary files /dev/null and b/pages/img/haeuslerhof-valdaora/03.jpg differ
diff --git a/pages/img/haeuslerhof-valdaora/04.jpg b/pages/img/haeuslerhof-valdaora/04.jpg
new file mode 100644
index 0000000..04b7727
Binary files /dev/null and b/pages/img/haeuslerhof-valdaora/04.jpg differ
diff --git a/pages/img/haus-central-ladis/01.jpg b/pages/img/haus-central-ladis/01.jpg
new file mode 100644
index 0000000..4e33b83
Binary files /dev/null and b/pages/img/haus-central-ladis/01.jpg differ
diff --git a/pages/img/haus-central-ladis/02.jpg b/pages/img/haus-central-ladis/02.jpg
new file mode 100644
index 0000000..aae72e7
Binary files /dev/null and b/pages/img/haus-central-ladis/02.jpg differ
diff --git a/pages/img/haus-central-ladis/03.jpg b/pages/img/haus-central-ladis/03.jpg
new file mode 100644
index 0000000..4f8fd8e
Binary files /dev/null and b/pages/img/haus-central-ladis/03.jpg differ
diff --git a/pages/img/haus-central-ladis/04.jpg b/pages/img/haus-central-ladis/04.jpg
new file mode 100644
index 0000000..f8f80fa
Binary files /dev/null and b/pages/img/haus-central-ladis/04.jpg differ
diff --git a/pages/img/haus-central-ladis/05.jpg b/pages/img/haus-central-ladis/05.jpg
new file mode 100644
index 0000000..9f6ee59
Binary files /dev/null and b/pages/img/haus-central-ladis/05.jpg differ
diff --git a/pages/img/sportchalet-viehhofen/01.jpg b/pages/img/sportchalet-viehhofen/01.jpg
new file mode 100644
index 0000000..64411df
Binary files /dev/null and b/pages/img/sportchalet-viehhofen/01.jpg differ
diff --git a/pages/img/sportchalet-viehhofen/02.jpg b/pages/img/sportchalet-viehhofen/02.jpg
new file mode 100644
index 0000000..b45df46
Binary files /dev/null and b/pages/img/sportchalet-viehhofen/02.jpg differ
diff --git a/pages/img/sportchalet-viehhofen/03.jpg b/pages/img/sportchalet-viehhofen/03.jpg
new file mode 100644
index 0000000..dbf93b9
Binary files /dev/null and b/pages/img/sportchalet-viehhofen/03.jpg differ
diff --git a/pages/img/sportchalet-viehhofen/04.jpg b/pages/img/sportchalet-viehhofen/04.jpg
new file mode 100644
index 0000000..47597aa
Binary files /dev/null and b/pages/img/sportchalet-viehhofen/04.jpg differ
diff --git a/pages/img/sportchalet-viehhofen/05.jpg b/pages/img/sportchalet-viehhofen/05.jpg
new file mode 100644
index 0000000..a841b4c
Binary files /dev/null and b/pages/img/sportchalet-viehhofen/05.jpg differ