Compare commits

...
3 Commits
Author SHA1 Message Date
Michael Stangl f74614672b @
Ferienwohnungs-Ausstattung ueberarbeitet

Ausstattungsangaben der FeWos auf ein gemeinsames Feld
gemeinsame_ausstattung umgestellt und die Detail-/Listen-/Startseiten-
Templates (ferienwohnungen/single.html, _default/list.html,
index.html) entsprechend angepasst.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@
2026-08-28 16:45:20 +02:00
Michael Stangl eeffa578cb @
Blog- und Veranstaltungsbereich hinzugefuegt

Neuer Bereich /blog fuer Veranstaltungstipps und Neuigkeiten aus
Kaufbeuren: content/blog/ (inkl. erstem Beitrag "Veranstaltungen
Herbst 2026" als Page-Bundle mit Bildern), Layouts layouts/blog/
(list + single) und ein bild-Shortcode, der Bilder aus dem Bundle
aufloest und eine Bildquelle anzeigt. Menueintrag "Blog" in hugo.toml,
zugehoeriges Blog-CSS (und Belegungsplan-Band) in style.css.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@
2026-08-28 16:45:09 +02:00
Michael Stangl 2379f4ac9f @
Skripte nach bin/ und lib/ umstrukturiert + WSL-Log-Analyse

Alle .bat-Skripte liegen jetzt in bin/, alle .py in lib/, .env(.example)
in bin/. setenv.bat setzt IWZ_* (inkl. IWZ_ENV) und PYTHONPATH; die
Launcher rufen setenv, aktivieren die .venv falls vorhanden und starten
das zugehoerige lib/-Skript. Die Python-Skripte lesen die .env ueber
IWZ_ENV (Fallback bin/.env).

Neu: Nutzungsanalyse per GoAccess (analyze.py/.bat) - laedt die
IONOS-Access-Logs per SFTP (paramiko) und erzeugt einen HTML-Report.
GoAccess laeuft ueber WSL (GOACCESS_MODE=wsl, Pfad-Uebersetzung nach
/mnt/c). install_py.bat prueft WSL+GoAccess und zeigt sonst den
Installationsweg. Doku in doc/analyse.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@
2026-08-28 16:44:56 +02:00
46 changed files with 1119 additions and 199 deletions
-16
View File
@@ -1,16 +0,0 @@
# Kopieren nach .env und mit echten Werten befuellen.
# .env ist in .gitignore eingetragen und wird NICHT eingecheckt.
# Storage Zone -> Access-Tab im Bunny.net Dashboard
BUNNY_STORAGE_ZONE=iwz-ferienwohnungen
BUNNY_STORAGE_HOST=storage.bunnycdn.com
BUNNY_STORAGE_PASSWORD=
# Pull Zone -> CDN -> Security-Tab im Bunny.net Dashboard (Token Authentication)
BUNNY_PULL_ZONE_URL=https://iwz-ferienwohnungen.b-cdn.net
BUNNY_AUTH_KEY=
# Account-API-Key fuer den CDN-Cache-Purge nach dem Deploy.
# Dashboard -> oben rechts auf den Namen -> Account -> API -> "API Key".
# ACHTUNG: nicht der BUNNY_AUTH_KEY oben, sondern der Account-weite API-Key!
BUNNY_API_KEY=
+7
View File
@@ -4,3 +4,10 @@ resources/
.env .env
.deploy_manifest.json .deploy_manifest.json
__pycache__/ __pycache__/
# WordPress-Export der alten Seite (nur temporär, enthält DB-Zugangsdaten im Klartext)
app816341166/
# Nutzungsanalyse (analyze.py): heruntergeladene Rohlogs + erzeugte Reports
logs/
results/
+34 -6
View File
@@ -14,14 +14,35 @@ Drei Ferienwohnungen in Kaufbeuren-Hirschzell / Allgäu.
- **Buchungskalender**: webplanner.de (externer Dienst, per Link eingebunden) - **Buchungskalender**: webplanner.de (externer Dienst, per Link eingebunden)
- **Git**: eigene Gitea-Instanz (kein GitHub/GitLab, kein CI/CD-Workflow) - **Git**: eigene Gitea-Instanz (kein GitHub/GitLab, kein CI/CD-Workflow)
- **Deploy**: manuell per `deploy.bat` oder `deploy.py` → `hugo --gc --minify` + Upload nach Bunny.net Storage Zone (per Bunny-API/FTP, analog zum Ansatz in `.github/workflows/deploy.yml`-Vorlage, aber lokal statt als GitHub Action) - **Deploy**: manuell per `deploy.bat` oder `deploy.py` → `hugo --gc --minify` + Upload nach Bunny.net Storage Zone (per Bunny-API/FTP, analog zum Ansatz in `.github/workflows/deploy.yml`-Vorlage, aber lokal statt als GitHub Action)
- **Nutzungsanalyse**: Server-Log-Auswertung mit GoAccess (kein JS-Tracking, keine Cookies, kein Consent-Banner). Skript `analyze.bat` / `analyze.py` lädt die Access-Logs per SFTP und erzeugt einen HTML-Report. Siehe `doc/analyse.md`.
## Hosting-Roadmap (Stand aktuell)
- **Jetzt**: Deploy läuft auf **Bunny.net** – für das erste Ausrollen der Instanz zur Kontrolle und Absprache. Die vorhandene Bunny-Konfiguration (Storage Zone, Pull Zone, `.env`) ist in Ordnung und bleibt vorerst so. `deploy.bat` / `deploy.py` deployen weiterhin nach Bunny.
- **Später**: Die Instanz wird auf **IONOS-Webspace** ausgerollt (Deploy-Ziel wird dann umgestellt).
- **Nutzungsanalyse jetzt schon erprobbar**: Die Log-Analyse (`analyze.py`) wird bereits mit den **IONOS-Access-Logs** getestet (SFTP-Zugang zum IONOS `/logs`-Ordner), unabhängig davon, dass das Deploy-Ziel noch Bunny ist. Nach dem IONOS-Umzug bleibt der Analyse-Workflow gleich.
## Projektstruktur ## Projektstruktur
``` ```
iwz/ iwz/
├── hugo.toml # Konfiguration: baseURL, Menü, Kontaktdaten (Params) ├── hugo.toml # Konfiguration: baseURL, Menü, Kontaktdaten (Params)
├── deploy.bat # Windows: hugo build + wrangler deploy ├── requirements.txt # Python-Abhängigkeiten (paramiko für analyze)
├── deploy.py # Python-Alternative zum deploy.bat ├── bin/ # Alle .bat-Skripte (Einstiegspunkte)
│ ├── setenv.bat # setzt IWZ, IWZ_BIN, IWZ_LIB, IWZ_ENV ... + PYTHONPATH
│ ├── install_py.bat # legt .venv an + pip install -r requirements.txt
│ ├── get_cmd.bat # öffnet Shell mit gesetzter Umgebung
│ ├── deploy.bat # → lib/deploy.py (Vollupload nach Bunny)
│ ├── deploy-lastdiff.bat# → lib/deploy_lastdiff.py (nur Änderungen)
│ ├── analyze.bat # → lib/analyze.py (IONOS-Logs + GoAccess)
│ ├── provide_previewlink.bat # → lib/preview_link.py
│ ├── .env # Secrets (NICHT im Git; von setenv als IWZ_ENV gesetzt)
│ └── .env.example # Vorlage zum Kopieren nach bin/.env
├── lib/ # Alle .py-Skripte (von den .bat aufgerufen)
│ ├── deploy.py # Hugo-Build + FTP-Upload nach Bunny Storage Zone
│ ├── deploy_lastdiff.py # nur geänderte Dateien (Hash-Manifest)
│ ├── analyze.py # SFTP-Download der Logs + GoAccess-Report
│ └── preview_link.py # signierter Bunny-Vorschau-Link
├── content/ ├── content/
│ ├── _index.md # Startseite │ ├── _index.md # Startseite
│ ├── preise.md # Preistabelle + Buchungsbedingungen │ ├── preise.md # Preistabelle + Buchungsbedingungen
@@ -94,8 +115,15 @@ hugo --gc --minify
# danach: Upload von ./public zur Bunny.net Storage Zone # danach: Upload von ./public zur Bunny.net Storage Zone
# (Storage Zone Name + API-Key/Storage-Passwort als Secrets/env, nicht im Git) # (Storage Zone Name + API-Key/Storage-Passwort als Secrets/env, nicht im Git)
# Bei jeder Änderung: # Bei jeder Änderung (Skripte liegen jetzt in bin/, Python in lib/):
deploy.bat bin\deploy.bat # Vollupload
# oder: # oder nur geänderte Dateien:
python deploy.py bin\deploy-lastdiff.bat
# oder direkt:
python lib\deploy.py
``` ```
Die `.bat`-Skripte in `bin/` rufen `setenv.bat` auf (setzt `IWZ_*` und
`PYTHONPATH`), wechseln ins Projekt-Root und starten das zugehörige Skript in
`lib/`. Die Secrets liegen in `bin/.env` (von `setenv.bat` als `IWZ_ENV`
gesetzt; die Python-Skripte lesen diesen Pfad, sonst Fallback auf `bin/.env`).
+38
View File
@@ -0,0 +1,38 @@
# Kopieren nach .env und mit echten Werten befuellen.
# .env ist in .gitignore eingetragen und wird NICHT eingecheckt.
# Storage Zone -> Access-Tab im Bunny.net Dashboard
BUNNY_STORAGE_ZONE=iwz-ferienwohnungen
BUNNY_STORAGE_HOST=storage.bunnycdn.com
BUNNY_STORAGE_PASSWORD=
# Pull Zone -> CDN -> Security-Tab im Bunny.net Dashboard (Token Authentication)
BUNNY_PULL_ZONE_URL=https://iwz-ferienwohnungen.b-cdn.net
BUNNY_AUTH_KEY=
# Account-API-Key fuer den CDN-Cache-Purge nach dem Deploy.
# Dashboard -> oben rechts auf den Namen -> Account -> API -> "API Key".
# ACHTUNG: nicht der BUNNY_AUTH_KEY oben, sondern der Account-weite API-Key!
BUNNY_API_KEY=
# --- Nutzungsanalyse (analyze.py) ------------------------------------------
# SFTP-Zugang zu IONOS (gleiche Zugangsdaten wie im IONOS-Kundenbereich / SFTP).
IONOS_SFTP_HOST=
IONOS_SFTP_PORT=22
IONOS_SFTP_USER=
IONOS_SFTP_PASSWORD=
# Ordner mit den Access-Logs auf dem Server (im Screenshot: /logs).
IONOS_LOGS_DIR=/logs
# GoAccess-Aufruf:
# GOACCESS_MODE=wsl -> GoAccess laeuft in der WSL (empfohlen, Standard).
# Installation: bin\install_py.bat prueft WSL+GoAccess
# und zeigt sonst die Installationsbefehle.
# GOACCESS_MODE=exe -> nativer Windows-Build; dann GOACCESS_EXE auf die
# goaccess.exe setzen (Download: https://goaccess.io/download).
GOACCESS_MODE=wsl
GOACCESS_EXE=goaccess
# Apache-Logformat. IONOS nutzt i.d.R. das Combined Log Format.
# Falls der Report leer bleibt -> siehe doc/analyse.md (Format anpassen).
LOG_FORMAT=COMBINED
+20
View File
@@ -0,0 +1,20 @@
@echo off
REM ================================================================
REM SPS_SKEL - Python Virtual Environment aktivieren
REM ================================================================
call "%~dp0setenv.bat"
if not exist "%IWZ%\.venv" (
echo FEHLER: Virtual environment nicht gefunden.
echo Bitte zuerst bin\install_py.bat ausfuehren.
exit /b 1
)
call "%IWZ%\.venv\Scripts\activate.bat"
echo Virtuelle Umgebung aktiviert.
echo Python-Version:
python --version
echo.
echo Installierte Pakete:
pip list
+26
View File
@@ -0,0 +1,26 @@
@echo off
REM ================================================================
REM IWZ Ferienwohnungen - Nutzungsanalyse (IONOS-Logs + GoAccess)
REM ================================================================
REM Aufruf:
REM bin\analyze.bat Logs laden + Report bauen + oeffnen
REM bin\analyze.bat --no-download nur neu auswerten (schon geladene Logs)
REM ================================================================
setlocal
call "%~dp0setenv.bat" >nul
REM Virtuelle Umgebung nutzen, falls vorhanden (paramiko liegt dort)
if exist "%IWZ%\.venv\Scripts\activate.bat" (
call "%IWZ%\.venv\Scripts\activate.bat"
)
set "PY=py"
py --version >nul 2>&1 || set "PY=python"
cd /d "%IWZ%"
"%PY%" "%IWZ_LIB%\analyze.py" %*
set "RC=%ERRORLEVEL%"
if not "%RC%"=="0" pause
endlocal & exit /b %RC%
+26
View File
@@ -0,0 +1,26 @@
@echo off
REM ================================================================
REM IWZ Ferienwohnungen - Deploy (nur geaenderte Dateien)
REM ================================================================
setlocal
call "%~dp0setenv.bat" >nul
REM Virtuelle Umgebung nutzen, falls vorhanden (paramiko etc. liegen dort)
if exist "%IWZ%\.venv\Scripts\activate.bat" (
call "%IWZ%\.venv\Scripts\activate.bat"
)
set "PY=py"
py --version >nul 2>&1 || set "PY=python"
echo.
echo === IWZ Ferienwohnungen - Deploy (nur Aenderungen) ===
echo.
cd /d "%IWZ%"
"%PY%" "%IWZ_LIB%\deploy_lastdiff.py" %*
set "RC=%ERRORLEVEL%"
if not "%RC%"=="0" pause
endlocal & exit /b %RC%
+29
View File
@@ -0,0 +1,29 @@
@echo off
REM ================================================================
REM IWZ Ferienwohnungen - Deploy (Vollupload nach Bunny.net)
REM ================================================================
setlocal
REM Umgebung setzen (IWZ, IWZ_LIB, IWZ_ENV, PYTHONPATH ...)
call "%~dp0setenv.bat" >nul
REM Virtuelle Umgebung nutzen, falls vorhanden (paramiko etc. liegen dort)
if exist "%IWZ%\.venv\Scripts\activate.bat" (
call "%IWZ%\.venv\Scripts\activate.bat"
)
REM Python-Interpreter bestimmen (py-Launcher bevorzugt, sonst python)
set "PY=py"
py --version >nul 2>&1 || set "PY=python"
echo.
echo === IWZ Ferienwohnungen - Deploy ===
echo.
REM Aus dem Projekt-Root heraus bauen (hugo + public liegen dort)
cd /d "%IWZ%"
"%PY%" "%IWZ_LIB%\deploy.py" %*
set "RC=%ERRORLEVEL%"
if not "%RC%"=="0" pause
endlocal & exit /b %RC%
+7
View File
@@ -0,0 +1,7 @@
@echo off
REM ================================================================
REM IWZ Ferienwohnungen - Shell mit gesetzten Umgebungsvariablen oeffnen
REM ================================================================
call "%~dp0setenv.bat"
start cmd /k "echo IWZ Umgebung aktiv. && echo IWZ=%IWZ%"
+54
View File
@@ -0,0 +1,54 @@
@echo off
REM ================================================================
REM IWZ - Python Virtual Environment einrichten
REM ================================================================
call "%~dp0setenv.bat"
if not exist "%IWZ%\.venv" (
echo Initialisiere Python virtual environment...
py -m venv "%IWZ%\.venv" --upgrade-deps
echo Erfolgreich.
call "%IWZ%\.venv\Scripts\activate.bat"
echo Installiere erforderliche Python Packages...
pip install -r "%IWZ%\requirements.txt" -q
echo Erfolgreich.
deactivate
) else (
echo Erforderliche Python Packages bereits installiert!
)
echo.
echo ================================================================
echo Pruefe Nutzungsanalyse-Voraussetzungen (WSL + GoAccess)
echo ================================================================
REM 1) Ist WSL verfuegbar?
where wsl >nul 2>&1
if errorlevel 1 (
echo [FEHLT] WSL ist nicht installiert.
echo So einrichten ^(einmalig, PowerShell als Administrator^):
echo wsl --install
echo Danach Windows neu starten und die Ubuntu-Ersteinrichtung
echo ^(Benutzername/Passwort^) durchlaufen. Anschliessend GoAccess
echo wie unten installieren.
goto :wsl_done
)
echo [OK] WSL gefunden.
REM 2) Ist GoAccess in der WSL installiert?
wsl -e bash -lc "command -v goaccess >/dev/null 2>&1"
if errorlevel 1 (
echo [FEHLT] GoAccess ist in der WSL nicht installiert.
echo So installieren:
echo wsl -e sudo apt update
echo wsl -e sudo apt install -y goaccess
echo ^(fragt einmalig nach dem WSL-sudo-Passwort^)
) else (
for /f "delims=" %%V in ('wsl -e bash -lc "goaccess --version ^| head -1"') do set "GA_VER=%%V"
echo [OK] GoAccess in der WSL: %GA_VER%
)
:wsl_done
echo ================================================================
+27
View File
@@ -0,0 +1,27 @@
@echo off
REM ================================================================
REM Ferienwohnungen Wolframm - Vorschau-Link erzeugen
REM ================================================================
setlocal
call "%~dp0setenv.bat" >nul
REM Virtuelle Umgebung nutzen, falls vorhanden
if exist "%IWZ%\.venv\Scripts\activate.bat" (
call "%IWZ%\.venv\Scripts\activate.bat"
)
set "PY=py"
py --version >nul 2>&1 || set "PY=python"
echo.
echo === Ferienwohnungen Wolframm - Vorschau-Link erzeugen ===
cd /d "%IWZ%"
"%PY%" "%IWZ_LIB%\preview_link.py" %*
echo.
echo Link oben markieren und mit Strg+C kopieren.
echo.
endlocal
cmd /k
+77
View File
@@ -0,0 +1,77 @@
@echo off
REM ================================================================
REM IWZ Ferienwohnungen - Umgebungsvariablen Setup
REM ================================================================
if defined IWZ_BIN (
echo IWZ Umgebung bereits gesetzt.
goto :show
)
echo Setting up environment variables for IWZ ...
REM Basis-Projektpfad (Ordner ueber bin\)
set "IWZ=%~dp0.."
if "%IWZ:~-6%"=="bin\.." set "IWZ=%IWZ:~0,-6%"
if "%IWZ:~-1%"=="\" set "IWZ=%IWZ:~0,-1%"
REM Pfade fuer verschiedene Komponenten
set "IWZ_BIN=%IWZ%\bin"
set "IWZ_LIB=%IWZ%\lib"
set "IWZ_DOC=%IWZ%\doc"
set "IWZ_CONTENT=%IWZ%\content"
set "IWZ_LAYOUTS=%IWZ%\layouts"
set "IWZ_STATIC=%IWZ%\static"
set "IWZ_PUBLIC=%IWZ%\public"
set "IWZ_LOGS=%IWZ%\logs"
set "IWZ_RESULTS=%IWZ%\results"
REM Pfad zur .env (liegt jetzt in bin\). Die Python-Skripte lesen IWZ_ENV.
set "IWZ_ENV=%IWZ_BIN%\.env"
REM Python-Pfad erweitern (nur wenn noch nicht vorhanden)
echo %PYTHONPATH% | find /i "%IWZ_LIB%" >nul
if errorlevel 1 (
set "PYTHONPATH=%IWZ_LIB%;%PYTHONPATH%"
)
REM Ordner erstellen falls sie nicht existieren
if not exist "%IWZ_BIN%" mkdir "%IWZ_BIN%"
if not exist "%IWZ_LIB%" mkdir "%IWZ_LIB%"
if not exist "%IWZ_LOGS%" mkdir "%IWZ_LOGS%"
if not exist "%IWZ_RESULTS%" mkdir "%IWZ_RESULTS%"
REM Optional dauerhaft in der Registry setzen: bin\setenv.bat --persist
if /i "%1"=="--persist" (
echo Setze Umgebungsvariablen dauerhaft in der Registry ^(setx^)...
setx IWZ "%IWZ%" >nul
setx IWZ_BIN "%IWZ_BIN%" >nul
setx IWZ_LIB "%IWZ_LIB%" >nul
setx IWZ_ENV "%IWZ_ENV%" >nul
echo Dauerhaft gesetzt. Gilt fuer alle neuen Prozesse.
)
REM Umgebungsvariablen anzeigen
:show
echo.
echo ================================================================
echo IWZ ENVIRONMENT SETUP COMPLETE
echo ================================================================
echo IWZ = %IWZ%
echo IWZ_BIN = %IWZ_BIN%
echo IWZ_LIB = %IWZ_LIB%
echo IWZ_DOC = %IWZ_DOC%
echo IWZ_CONTENT = %IWZ_CONTENT%
echo IWZ_LAYOUTS = %IWZ_LAYOUTS%
echo IWZ_STATIC = %IWZ_STATIC%
echo IWZ_PUBLIC = %IWZ_PUBLIC%
echo IWZ_LOGS = %IWZ_LOGS%
echo IWZ_RESULTS = %IWZ_RESULTS%
echo IWZ_ENV = %IWZ_ENV%
echo PYTHONPATH = %PYTHONPATH%
echo ================================================================
echo.
REM Optionally keep window open
if "%1"=="--keep-open" pause
Binary file not shown.

After

Width:  |  Height:  |  Size: 536 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 632 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 640 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 691 KiB

+7
View File
@@ -0,0 +1,7 @@
---
title: "Blog & Veranstaltungen"
description: "Aktuelle Veranstaltungen, Tipps und Neuigkeiten aus Kaufbeuren und dem Allgäu"
---
Hier finden Sie aktuelle Veranstaltungstipps und Neuigkeiten aus Kaufbeuren und
der Region – ideal, um Ihren Aufenthalt bei uns zu planen.
Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

@@ -0,0 +1,113 @@
---
title: "Veranstaltungshighlights in Kaufbeuren – Herbst 2026"
date: 2026-08-28
# Einblenddatum: 2 Monate vor dem ersten Event (11.09.2026) -> ab hier sichtbar
publishDate: 2026-07-11
# Ablaufdatum: 1 Monat nach dem ersten Event -> danach ausgeblendet
expiryDate: 2026-10-11
description: "Von Filmsoiree über Genusstage bis zum Jazzkonzert – die schönsten Veranstaltungen rund um Kaufbeuren im Herbst 2026."
bild: "filmsoiree-kino.jpg"
bild_alt: "Kinosaal mit roten Sesseln"
bild_quelle: 'Foto: <a href="https://unsplash.com/@krields" target="_blank" rel="noopener">Krists Luhaers</a> auf <a href="https://unsplash.com" target="_blank" rel="noopener">Unsplash</a>'
---
Der Herbst hält in Kaufbeuren und im Ostallgäu ein buntes Programm bereit. Ob
Kultur, Kulinarik oder ein stimmungsvoller Bummel durch die Altstadt – hier ist
für jeden etwas dabei. Während Ihres Aufenthalts bei uns in Kaufbeuren-Hirschzell
erreichen Sie alle Veranstaltungsorte bequem in wenigen Minuten.
## Filmsoiree: „Zwischen zwei Heimaten"
{{< bild
src="filmsoiree-kino.jpg"
alt="Kinosaal mit roten Sesseln"
quelle=`Foto: <a href="https://unsplash.com/@krields" target="_blank" rel="noopener">Krists Luhaers</a> auf <a href="https://unsplash.com" target="_blank" rel="noopener">Unsplash</a>` >}}
**11. September 2026 · Gablonzer Haus (Großer Saal), Kaufbeuren-Neugablonz**
Anlässlich von „80 Jahre Neugablonz – 80 Jahre Vertreibung" zeigt das
Isergebirgs-Museum in Zusammenarbeit mit dem Kulturreferat eine besondere
Filmsoiree. Der Eintritt ist frei (Zählkarte vorab erforderlich).
## Geführte Pilgerwanderung: Etappe Mindelheim nach Kaufbeuren
{{< bild
src="pilgerwanderung-natur.jpg"
alt="Wanderweg durch grüne Voralpenlandschaft"
quelle=`Foto: <a href="https://unsplash.com/@fab_fotografie" target="_blank" rel="noopener">Fabian Kühne</a> auf <a href="https://unsplash.com" target="_blank" rel="noopener">Unsplash</a>` >}}
**19. September 2026 · Start in Mindelheim, Ziel Crescentiakloster Kaufbeuren**
Eine begleitete Pilgerwanderung auf der rund 28 Kilometer langen 4. Etappe des
Crescentia-Pilgerwegs mit Impulsen und geistlicher Begleitung durch
Pilgerwegbegleiter.
## Allgäuer Genusstage 2026
{{< bild
src="genusstage-kulinarik.jpg"
alt="Regionale Spezialitäten auf einem Tisch"
quelle=`Foto: <a href="https://unsplash.com/@brookelark" target="_blank" rel="noopener">Brooke Lark</a> auf <a href="https://unsplash.com" target="_blank" rel="noopener">Unsplash</a>` >}}
**19. September – 04. Oktober 2026 · Kaufbeuren, Ostallgäu & Region**
Kulinarische Aktionswochen im Landkreis Ostallgäu und der Stadt Kaufbeuren.
Regionale Erzeuger, Gastronomen und Direktvermarkter präsentieren Spezialitäten
aus der Heimat.
## Jubiläumskonzert: 60 Jahre Kulturring Kaufbeuren
{{< bild
src="konzert-orchester.jpg"
alt="Orchester bei einem Konzert"
quelle=`Foto: <a href="https://unsplash.com/@manuelsardo" target="_blank" rel="noopener">Manuel Nägeli</a> auf <a href="https://unsplash.com" target="_blank" rel="noopener">Unsplash</a>` >}}
**26. September 2026 · Stadtsaal Kaufbeuren**
Der Kulturring Kaufbeuren feiert sein 60-jähriges Bestehen mit einer sinfonischen
Märchenstunde, gespielt vom renommierten ODEON Jugendsymphonieorchester München.
## Theater & Performance: „Poseidon und die Plastiksee"
{{< bild
src="theater-buehne.jpg"
alt="Theaterbühne mit Vorhang und Beleuchtung"
quelle=`Foto: <a href="https://unsplash.com/@barnaba" target="_blank" rel="noopener">Barna Bartis</a> auf <a href="https://unsplash.com" target="_blank" rel="noopener">Unsplash</a>` >}}
**07. Oktober 2026 · Stadttheater Kaufbeuren**
Ein Gastspiel der Saison des Kulturrings Kaufbeuren e.V., das sich auf
künstlerische Weise mit Umwelt- und Meeres-Themen im historischen Rahmen des
Stadttheaters auseinandersetzt.
## Jazzkonzert: Yakou Tribe – „Out of Sight"
{{< bild
src="jazz-konzert.jpg"
alt="Musiker bei einem Jazzkonzert"
quelle=`Foto: <a href="https://unsplash.com/@mariusmasalar" target="_blank" rel="noopener">Marius Masalar</a> auf <a href="https://unsplash.com" target="_blank" rel="noopener">Unsplash</a>` >}}
**30. Oktober 2026 · Stadttheater Kaufbeuren**
Das renommierte Berliner Jazz-Ensemble Yakou Tribe gastiert im Stadttheater
Kaufbeuren und präsentiert modernen, mitreißenden Instrumental-Jazz.
## Altstadtfunkeln (Candle Light Shopping & Kaufbeuren Leuchtet)
{{< bild
src="altstadtfunkeln-nacht.jpg"
alt="Illuminierte Altstadtgasse bei Nacht"
quelle=`Foto: <a href="https://unsplash.com/@agk42" target="_blank" rel="noopener">Alex Knight</a> auf <a href="https://unsplash.com" target="_blank" rel="noopener">Unsplash</a>` >}}
**13. und 14. November 2026 · Altstadt Kaufbeuren**
Die Zusammenführung von „Candle Light Shopping" und „Kaufbeuren leuchtet".
Besucher erwarten stimmungsvolle Videomappings an historischen Fassaden,
illuminierte Gassen und verlängerte Einkaufszeiten im lokalen Einzelhandel.
---
*Die verwendeten Fotos sind Symbolbilder (Unsplash-Lizenz, kostenlos auch für
kommerzielle Nutzung). Alle Veranstaltungsangaben ohne Gewähr – bitte informieren
Sie sich vor Ihrem Besuch bei den jeweiligen Veranstaltern über eventuelle
Änderungen.*
Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

+58 -44
View File
@@ -1,32 +1,38 @@
--- ---
title: "Unsere Ferienwohnungen" title: "Unsere Ferienwohnungen"
description: "Drei komfortable Ferienwohnungen in Kaufbeuren-Hirschzell" description: "Drei komfortable Ferienwohnungen in Kaufbeuren-Hirschzell"
allgemein: gemeinsame_ausstattung:
- titel: "Sicherheit" - titel: "Kochen und Essen"
punkte: punkte:
- "Rauchmelder" - "Backofen"
- "Verbandskasten" - "Dunstabzugshaube"
- titel: "Eigenschaften" - "Elektroherd"
- "Esstisch/Essecke"
- "Geschirr"
- "Geschirrhandtücher"
- "Kühlschrank"
- "Kaffeemaschine Filter"
- "Mikrowelle"
- "Mixer"
- "Toaster"
- "Wasserkocher"
- titel: "Haushaltshilfen"
punkte: punkte:
- "Seniorengerecht" - "Bügelbrett"
- "Nichtraucher" - "Bügeleisen"
- "Allergikergeeignet" - "Putzmittel"
- "bedingt barrierefrei" - "Staubsauger"
- "Haustiere nicht erlaubt" - "Wäscheständer"
- titel: "Für die Kleinen" - "Wäschetrockner"
- "Wäschewaschen möglich"
- titel: "Wohnen und Leben"
punkte: punkte:
- "Kinderhochstuhl" - "Flachbildfernseher"
- "Wickelkommode" - "Fliegenschutzgitter"
- "Wickelauflage" - "Heizung/Zentralheizung"
- "Babybett (Reisebett)" - "Internet WLAN"
- "Kindertritt" - "Kabel-TV"
- "Baby-Badewanne" - "Radio"
umfeld:
- titel: "Im Grünen"
punkte:
- "Terrasse"
- "Gartenmöbel"
- "Sonnenschirm"
- titel: "Entfernungen" - titel: "Entfernungen"
punkte: punkte:
- "PKW-Stellplatz: 10 m" - "PKW-Stellplatz: 10 m"
@@ -41,35 +47,43 @@ umfeld:
- "Apotheke: 3,0 km" - "Apotheke: 3,0 km"
- "Autobahn: 20 km" - "Autobahn: 20 km"
- "Flughafen: 45 km" - "Flughafen: 45 km"
- titel: "Spiel, Sport, Spaß" - titel: "Eigenschaften"
punkte: punkte:
- "Tischtennis" - "Allergikergeeignet"
- "Spielplatz" - "bedingt barrierefrei"
gemeinsam: - "Haustiere nicht erlaubt"
- titel: "Wohnen und Leben" - "Nichtraucher"
- "Seniorengerecht"
- titel: "Für die Kleinen"
punkte: punkte:
- "Heizung/Zentralheizung" - "Baby-Badewanne"
- "Fliegenschutzgitter" - "Babybett (Reisebett)"
- "Flachbildfernseher" - "Kinderhochstuhl"
- "Kabel-TV" - "Kindertritt"
- "Radio"
- "Internet WLAN"
- titel: "Bad und Sanitär" - titel: "Bad und Sanitär"
punkte: punkte:
- "Dusche" - "Dusche"
- "Waschbecken"
- "Spiegel"
- "Haartrockner" - "Haartrockner"
- "Spiegel"
- "Waschbecken"
- "WC" - "WC"
- titel: "Haushaltshilfen" - titel: "Spiel, Sport, Spaß"
punkte: punkte:
- "Bügelbrett" - "Spielplatz"
- "Bügeleisen" - "Tischtennis"
- "Staubsauger" - titel: "Im Grünen"
- "Wäschewaschen möglich" punkte:
- "Wäscheständer" - "Gartenmöbel"
- "Wäschetrockner" - "Sonnenschirm"
- "Putzmittel" - "Terrasse"
- titel: "Sicherheit"
punkte:
- "Rauchmelder"
- "Verbandskasten"
- titel: "Fuhrpark"
punkte:
- "PKW-Stellplatz"
--- ---
Alle drei Wohnungen sind vollständig ausgestattet, allergikerfreundlich und familientauglich. Alle drei Wohnungen sind vollständig ausgestattet, allergikerfreundlich und familientauglich.
+3 -12
View File
@@ -16,20 +16,11 @@ ausstattung:
- "1 Einzelbett (90×200 cm)" - "1 Einzelbett (90×200 cm)"
- titel: "Kochen und Essen" - titel: "Kochen und Essen"
punkte: punkte:
- "Elektroherd"
- "Backofen"
- "Dunstabzugshaube"
- "Geschirrspüler" - "Geschirrspüler"
- "Kühlschrank"
- "Gefriertruhe" - "Gefriertruhe"
- "Kaffeemaschine (Filter)" - titel: "Für die Kleinen"
- "Wasserkocher" punkte:
- "Toaster" - "Wickelkommode"
- "Mixer"
- "Mikrowelle"
- "Esstisch/Essecke"
- "Geschirr"
- "Geschirrhandtücher"
bilder: bilder:
- bild: "/images/fewo1/ferienwohnung-1-wolframm-kaufbeuren-Wohnzimmer.jpg" - bild: "/images/fewo1/ferienwohnung-1-wolframm-kaufbeuren-Wohnzimmer.jpg"
titel: "Ferienwohnung Kaufbeuren Wohnzimmer FeWo 1" titel: "Ferienwohnung Kaufbeuren Wohnzimmer FeWo 1"
+3 -12
View File
@@ -14,19 +14,10 @@ ausstattung:
- "1 Zustellbett (80×190 cm)" - "1 Zustellbett (80×190 cm)"
- titel: "Kochen und Essen" - titel: "Kochen und Essen"
punkte: punkte:
- "Elektroherd"
- "Backofen"
- "Dunstabzugshaube"
- "Mikrowelle"
- "Kühlschrank"
- "Gefrierfach" - "Gefrierfach"
- "Kaffeemaschine (Filter)" - titel: "Für die Kleinen"
- "Wasserkocher" punkte:
- "Toaster" - "Wickelunterlage"
- "Mixer"
- "Esstisch/Essecke"
- "Geschirr"
- "Geschirrhandtücher"
bilder: bilder:
- bild: "/images/fewo2/Wohn-esszimmer-Front-fewo-2.jpg" - bild: "/images/fewo2/Wohn-esszimmer-Front-fewo-2.jpg"
titel: "Ferienwohnung Kaufbeuren Wohn-Esszimmer FeWo 2" titel: "Ferienwohnung Kaufbeuren Wohn-Esszimmer FeWo 2"
+2 -14
View File
@@ -15,22 +15,10 @@ ausstattung:
- "1 Zustellbett (80×190 cm)" - "1 Zustellbett (80×190 cm)"
- titel: "Kochen und Essen" - titel: "Kochen und Essen"
punkte: punkte:
- "Elektroherd"
- "Backofen"
- "Dunstabzugshaube"
- "Kühlschrank"
- "Gefrierfach" - "Gefrierfach"
- "Kaffeemaschine (Filter)" - titel: "Für die Kleinen"
- "Wasserkocher"
- "Toaster"
- "Mixer"
- "Mikrowelle"
- "Esstisch/Essecke"
- "Geschirr"
- "Geschirrhandtücher"
- titel: "Besonderheit"
punkte: punkte:
- "Kinderfreundlich" - "Wickelkommode"
bilder: bilder:
- bild: "/images/fewo3/ferienwohnung-allgaeu-kaufbeuren-wohnung-3-Kueche-Sitzecke-fewo.jpg" - bild: "/images/fewo3/ferienwohnung-allgaeu-kaufbeuren-wohnung-3-Kueche-Sitzecke-fewo.jpg"
titel: "Ferienwohnung Kaufbeuren FeWo 3 – Sitzecke in der Küche" titel: "Ferienwohnung Kaufbeuren FeWo 3 – Sitzecke in der Küche"
-10
View File
@@ -1,10 +0,0 @@
@echo off
echo.
echo === IWZ Ferienwohnungen - Deploy (nur Änderungen) ===
echo.
python deploy_lastdiff.py
if %errorlevel% neq 0 (
pause
exit /b 1
)
pause
-10
View File
@@ -1,10 +0,0 @@
@echo off
echo.
echo === IWZ Ferienwohnungen - Deploy ===
echo.
python deploy.py
if %errorlevel% neq 0 (
pause
exit /b 1
)
pause
+146
View File
@@ -0,0 +1,146 @@
# Nutzungsanalyse der Website (Server-Logs + GoAccess)
Die neue Hugo-Seite verzichtet bewusst auf JavaScript-Tracking (kein Google
Analytics, keine Cookies, kein Consent-Banner nötig). Stattdessen werten wir
die **Apache-Access-Logs** aus, die IONOS ohnehin auf dem Server schreibt.
Ablauf des Skripts `analyze.py`:
1. Verbindet sich per **SFTP** mit IONOS und lädt alle `access.log*`-Dateien
aus dem `/logs`-Ordner nach `./logs/` herunter.
2. Entpackt die rotierten `.gz`-Logs und fügt alles zu einer Datei zusammen.
3. Lässt **GoAccess** darüberlaufen und erzeugt einen HTML-Report unter
`./results/report_JJJJ-MM-TT.html`, der automatisch im Browser geöffnet wird.
`logs/` und `results/` stehen in `.gitignore` und werden nicht eingecheckt.
---
## Einmalige Einrichtung
### 1. Python-Abhängigkeit (paramiko)
```powershell
pip install -r requirements.txt
```
### 2. GoAccess (über WSL – empfohlen)
GoAccess wird offiziell nur für Linux/macOS gebaut und ist in keinem
Windows-Paketmanager verfügbar. Unter Windows ist der zuverlässigste Weg die
**WSL** (Windows Subsystem for Linux). `bin\install_py.bat` prüft automatisch,
ob WSL und GoAccess vorhanden sind, und zeigt sonst die passenden Befehle.
**WSL installieren** (falls noch nicht vorhanden, PowerShell als Administrator):
```powershell
wsl --install
```
Danach Windows neu starten und die Ubuntu-Ersteinrichtung (Benutzer/Passwort)
durchlaufen.
**GoAccess in der WSL installieren:**
```powershell
wsl -e sudo apt update
wsl -e sudo apt install -y goaccess
```
In der `.env` steht dafür (Standard):
```text
GOACCESS_MODE=wsl
```
Das Skript ruft GoAccess dann per `wsl goaccess ...` auf und übersetzt die
Windows-Pfade automatisch nach `/mnt/c/...`.
> **Alternative – nativer Windows-Build:** Falls du kein WSL nutzen willst,
> lade den Cygwin-Build von <https://goaccess.io/download>, entpacke ihn (z. B.
> nach `C:\tools\goaccess\`) und setze in der `.env`:
>
> ```text
> GOACCESS_MODE=exe
> GOACCESS_EXE=C:\tools\goaccess\goaccess.exe
> ```
### 3. IONOS-SFTP-Zugang in `.env` eintragen
Die Zugangsdaten stehen im IONOS-Kundenbereich unter **Hosting → SFTP/SSH**.
Es sind dieselben, mit denen der `/logs`-Ordner erreichbar ist.
```text
IONOS_SFTP_HOST=<z.B. home123456789.1and1-data.host oder access-5000000000.webspace-host.com>
IONOS_SFTP_PORT=22
IONOS_SFTP_USER=<SFTP-Benutzer>
IONOS_SFTP_PASSWORD=<SFTP-Passwort>
IONOS_LOGS_DIR=/logs
```
> Die `.env` steht in `.gitignore` und wird **nicht** eingecheckt.
---
## Nutzung
```powershell
# Logs neu laden, Report bauen und im Browser öffnen:
analyze.bat
# oder:
python analyze.py
# Nur neu auswerten (ohne erneuten Download der schon geladenen Logs):
python analyze.py --no-download
```
Beim wiederholten Lauf werden bereits geladene, rotierte Logs übersprungen
(gleiche Dateigröße) – nur das aktuell wachsende Log wird neu geholt.
---
## Was im Report steht
GoAccess zeigt u. a.:
- **Eindeutige Besucher** und Seitenaufrufe pro Tag
- **Meistbesuchte Seiten** (welche Ferienwohnung interessiert am meisten?)
- **Referrer** (woher kommen die Besucher – Google, direkte Eingabe, Links)
- **Länder / Sprachen**, Browser, Betriebssysteme, Geräte
- **HTTP-Statuscodes** (404-Fehler = tote Links finden)
- **Traffic-Volumen**
Aufrufe von Suchmaschinen-Bots werden herausgefiltert (`--ignore-crawlers`),
IP-Adressen im Report anonymisiert (`--anonymize-ip`).
---
## DSGVO-Hinweis
Diese Auswertung passiert **nur lokal auf deinem Rechner** aus den ohnehin
anfallenden Server-Logs. Es wird nichts an Dritte übertragen und kein
Tracking-Code beim Besucher ausgeführt. Server-Logs mit IP-Adressen dürfen zu
Sicherheits-/Betriebszwecken vorübergehend gespeichert werden; im GoAccess-
Report sind die IPs zusätzlich anonymisiert. Ein Cookie-Consent-Banner ist für
diese Art der Auswertung **nicht** erforderlich.
---
## Fehlerbehebung
**Report bleibt leer / „No data".**
Dann passt das Log-Format nicht. IONOS nutzt meist das *Combined Log Format*
(`LOG_FORMAT=COMBINED`). Wirf einen Blick in die erste Zeile von
`logs/access.log.current`:
- Beginnt sie mit einer IP und enthält am Ende `"...Referrer" "...User-Agent"`
→ `COMBINED` ist korrekt.
- Fehlen Referrer/User-Agent → `LOG_FORMAT=COMMON`.
- Steht der Vhost-Name vor der IP → `LOG_FORMAT=VCOMBINED`.
Alternativ ein eigenes Format als GoAccess-`--log-format`-String in `.env`
hinterlegen (siehe <https://goaccess.io/man#custom-log>).
**`paramiko`-Authentifizierungsfehler.**
SFTP-Host/-User/-Passwort im IONOS-Kundenbereich gegenprüfen. Der SFTP-Benutzer
ist nicht zwingend identisch mit dem Login des Kundenbereichs.
+5 -1
View File
@@ -28,7 +28,11 @@ defaultContentLanguage = "de"
name = "Ausflugsziele" name = "Ausflugsziele"
url = "/ausflugsziele/" url = "/ausflugsziele/"
weight = 4 weight = 4
[[menu.main]]
name = "Blog"
url = "/blog/"
weight = 5
[[menu.main]] [[menu.main]]
name = "Kontakt" name = "Kontakt"
url = "/kontakt/" url = "/kontakt/"
weight = 5 weight = 6
+11 -49
View File
@@ -30,57 +30,19 @@
{{ end }} {{ end }}
</div> </div>
{{ if or .Params.allgemein .Params.umfeld .Params.gemeinsam }} {{ with .Params.gemeinsame_ausstattung }}
<div class="umfeld" id="umfeld"> <div class="umfeld" id="umfeld">
<p class="umfeld-intro">Diese Ausstattung und Umgebungsinfos gelten für alle drei Ferienwohnungen gleichermaßen.</p> <p class="umfeld-intro">Diese Ausstattung gilt für alle drei Ferienwohnungen gleichermaßen.</p>
<div class="spec-grid">
{{ with .Params.allgemein }} {{ range . }}
<section class="umfeld-gruppe"> <div class="spec-block">
<h2>Allgemein</h2> <h3>{{ .titel }}</h3>
<div class="spec-grid"> <ul>
{{ range . }} {{ range .punkte }}<li>{{ . }}</li>{{ end }}
<div class="spec-block"> </ul>
<h3>{{ .titel }}</h3>
<ul>
{{ range .punkte }}<li>{{ . }}</li>{{ end }}
</ul>
</div>
{{ end }}
</div> </div>
</section> {{ end }}
{{ end }} </div>
{{ with .Params.umfeld }}
<section class="umfeld-gruppe">
<h2>Umfeld</h2>
<div class="spec-grid">
{{ range . }}
<div class="spec-block">
<h3>{{ .titel }}</h3>
<ul>
{{ range .punkte }}<li>{{ . }}</li>{{ end }}
</ul>
</div>
{{ end }}
</div>
</section>
{{ end }}
{{ with .Params.gemeinsam }}
<section class="umfeld-gruppe">
<h2>Gemeinsame Ausstattung</h2>
<div class="spec-grid">
{{ range . }}
<div class="spec-block">
<h3>{{ .titel }}</h3>
<ul>
{{ range .punkte }}<li>{{ . }}</li>{{ end }}
</ul>
</div>
{{ end }}
</div>
</section>
{{ end }}
</div> </div>
{{ end }} {{ end }}
</div> </div>
+33
View File
@@ -0,0 +1,33 @@
{{ define "main" }}
<article class="page">
<div class="container">
<h1>{{ .Title }}</h1>
{{ .Content }}
<div class="fewo-grid blog-grid">
{{ range .Pages.ByPublishDate.Reverse }}
<div class="fewo-card">
{{ $post := . }}
{{ with .Params.bild }}
{{ $src := . }}
{{ with $post.Resources.GetMatch $src }}{{ $src = .RelPermalink }}{{ end }}
<a href="{{ $post.RelPermalink }}" class="fewo-card-imglink">
<img src="{{ $src }}" alt="{{ with $post.Params.bild_alt }}{{ . }}{{ else }}{{ $post.Title }}{{ end }}" class="fewo-card-img" loading="lazy">
</a>
{{ end }}
<div class="fewo-card-body">
<p class="blog-date">{{ .PublishDate.Format "02.01.2006" }}</p>
<h2><a href="{{ .RelPermalink }}">{{ .Title }}</a></h2>
{{ with .Params.description }}<p>{{ . }}</p>{{ end }}
<a href="{{ .RelPermalink }}" class="btn btn-primary">Weiterlesen</a>
</div>
</div>
{{ end }}
</div>
{{ if not .Pages }}
<p>Aktuell sind keine Beiträge veröffentlicht. Schauen Sie bald wieder vorbei!</p>
{{ end }}
</div>
</article>
{{ end }}
+18
View File
@@ -0,0 +1,18 @@
{{ define "main" }}
<article class="page blog-post">
<div class="container container--narrow">
<p class="blog-date">{{ .PublishDate.Format "02.01.2006" }}</p>
<h1>{{ .Title }}</h1>
{{ with .Params.bild }}
{{ $src := . }}
{{ with $.Resources.GetMatch $src }}{{ $src = .RelPermalink }}{{ end }}
<figure class="blog-hero">
<img src="{{ $src }}" alt="{{ with $.Params.bild_alt }}{{ . }}{{ else }}{{ $.Title }}{{ end }}" class="blog-hero-img">
{{ with $.Params.bild_quelle }}<figcaption class="bild-quelle">Bildquelle: {{ . | safeHTML }}</figcaption>{{ end }}
</figure>
{{ end }}
{{ .Content }}
<p class="blog-back"><a href="/blog/" class="btn btn-outline">&larr; Zurück zur Übersicht</a></p>
</div>
</article>
{{ end }}
+18 -2
View File
@@ -41,9 +41,26 @@
{{ .Content }} {{ .Content }}
</div> </div>
{{ $gemeinsam := (.Site.GetPage "/ferienwohnungen").Params.gemeinsame_ausstattung }}
{{ with .Params.ausstattung }} {{ with .Params.ausstattung }}
<section class="ausstattung"> <section class="ausstattung">
<h2>Ausstattung</h2> <h2>Ausstattung spezifisch</h2>
<div class="spec-grid">
{{ range . }}
<div class="spec-block">
<h3>{{ .titel }}</h3>
<ul>
{{ range .punkte }}<li>{{ . }}</li>{{ end }}
</ul>
</div>
{{ end }}
</div>
</section>
{{ end }}
{{ with $gemeinsam }}
<section class="ausstattung">
<h2>Ausstattung allgemein</h2>
<div class="spec-grid"> <div class="spec-grid">
{{ range . }} {{ range . }}
<div class="spec-block"> <div class="spec-block">
@@ -54,7 +71,6 @@
</div> </div>
{{ end }} {{ end }}
</div> </div>
<p class="ausstattung-hinweis">Allgemeine Ausstattung (Sicherheit, Eigenschaften, für die Kleinen), Umfeld (Garten, Freizeit, Entfernungen) und weitere gemeinsame Ausstattung finden Sie unter <a href="/ferienwohnungen/#umfeld">Allgemein, Umfeld &amp; gemeinsame Ausstattung</a> aller Ferienwohnungen.</p>
</section> </section>
{{ end }} {{ end }}
+15 -1
View File
@@ -17,6 +17,15 @@
</div> </div>
</section> </section>
<section class="belegungsplan-band">
<div class="container">
<button type="button" class="btn belegungsplan-band-btn" id="belegungsplan-btn" aria-expanded="false" aria-controls="belegungsplan-frame">
Belegungsplan – hier freie Kapazitäten einsehen
</button>
<div class="belegungsplan-frame" id="belegungsplan-frame" data-src="https://www.webplanner.de/tools/?76703072363837353064343069" hidden></div>
</div>
</section>
{{/* Highlights-Block vorerst ausgeblendet – siehe Wunsch, dass "Unsere Wohnungen" zuerst steht. {{/* Highlights-Block vorerst ausgeblendet – siehe Wunsch, dass "Unsere Wohnungen" zuerst steht.
<section class="highlights"> <section class="highlights">
<div class="container"> <div class="container">
@@ -70,7 +79,12 @@
{{ with $page.Params.personen }}<span>bis {{ . }} Personen</span>{{ end }} {{ with $page.Params.personen }}<span>bis {{ . }} Personen</span>{{ end }}
{{ with $page.Params.preis }}<span>ab {{ . }} € / Nacht</span>{{ end }} {{ with $page.Params.preis }}<span>ab {{ . }} € / Nacht</span>{{ end }}
</p> </p>
<a href="{{ $page.RelPermalink }}" class="btn btn-primary">Details &amp; Buchung</a> <div class="fewo-card-actions">
<a href="{{ $page.RelPermalink }}" class="btn btn-outline">Details</a>
{{ with $page.Params.belegungsplan }}
<a href="{{ . }}" class="btn btn-primary" target="_blank" rel="noopener">Buchung</a>
{{ end }}
</div>
</div> </div>
</div> </div>
{{ end }} {{ end }}
+11
View File
@@ -0,0 +1,11 @@
{{- $src := .Get "src" -}}
{{- $alt := .Get "alt" -}}
{{- $quelle := .Get "quelle" -}}
{{- /* Bild als Page-Resource aus dem Bundle auflösen; sonst src unverändert nutzen */ -}}
{{- with .Page.Resources.GetMatch $src }}{{ $src = .RelPermalink }}{{ end -}}
<figure class="blog-figure">
<img src="{{ $src }}" alt="{{ $alt }}" loading="lazy">
{{- with $quelle }}
<figcaption class="bild-quelle">Bildquelle: {{ . | safeHTML }}</figcaption>
{{- end }}
</figure>
+226
View File
@@ -0,0 +1,226 @@
#!/usr/bin/env python3
"""
IWZ Ferienwohnungen - Nutzungsanalyse
Laedt die Apache-Access-Logs per SFTP von IONOS herunter, entpackt die
rotierten .gz-Dateien und erzeugt mit GoAccess einen HTML-Report.
Kein JavaScript-Tracking, keine Cookies beim Besucher -> DSGVO-freundlich.
Die Auswertung passiert rein aus den Server-Logs.
Voraussetzungen:
- pip install -r requirements.txt (paramiko)
- GoAccess Windows-Build (Pfad in .env: GOACCESS_EXE)
- .env mit IONOS_SFTP_* befuellt (siehe .env.example)
Aufruf:
python analyze.py # Logs laden + Report bauen + oeffnen
python analyze.py --no-download # nur neu auswerten (schon geladene Logs)
"""
import gzip
import os
import shutil
import stat
import subprocess
import sys
import webbrowser
from datetime import date
from pathlib import Path
try:
import paramiko
except ImportError:
print("FEHLER: paramiko fehlt. Bitte 'pip install -r requirements.txt' ausfuehren.")
sys.exit(1)
PROJECT_ROOT = Path(__file__).resolve().parent.parent # Skript liegt in lib/
LOGS_DIR = PROJECT_ROOT / "logs" # heruntergeladene Rohlogs (in .gitignore)
RESULTS_DIR = PROJECT_ROOT / "results" # erzeugte Reports (in .gitignore)
SFTP_PORT = 22
def env_file():
"""Pfad zur .env: bevorzugt IWZ_ENV (von setenv.bat), sonst bin/.env."""
from_var = os.environ.get("IWZ_ENV")
if from_var:
return Path(from_var)
return PROJECT_ROOT / "bin" / ".env"
# Nur diese Dateien aus dem IONOS /logs-Ordner interessieren uns.
# Alles andere (sftp.log, traffic.db, forensic/, info.*) wird ignoriert.
ACCESS_LOG_PREFIX = "access.log"
def load_env(path):
env = {}
if not path.exists():
return env
for line in path.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, value = line.split("=", 1)
env[key.strip()] = value.strip()
return env
def download_access_logs(host, port, user, password, remote_dir):
"""Laedt alle access.log*-Dateien aus dem IONOS /logs-Ordner nach LOGS_DIR."""
print(f"\n-> Verbinde per SFTP mit {host}:{port} ...")
transport = paramiko.Transport((host, port))
try:
transport.connect(username=user, password=password)
sftp = paramiko.SFTPClient.from_transport(transport)
sftp.chdir(remote_dir)
LOGS_DIR.mkdir(exist_ok=True)
count = 0
for attr in sftp.listdir_attr("."):
name = attr.filename
if not name.startswith(ACCESS_LOG_PREFIX):
continue
if stat.S_ISDIR(attr.st_mode):
continue
local_path = LOGS_DIR / name
# Schon vorhanden und gleich gross? -> ueberspringen (rotierte Logs
# aendern sich nicht mehr; nur .current / aktueller Tag waechst).
if (local_path.exists()
and local_path.stat().st_size == attr.st_size
and not name.endswith(".current")):
continue
print(f" lade {name} ({attr.st_size} Bytes)")
sftp.get(name, str(local_path))
count += 1
sftp.close()
print(f" {count} Datei(en) geladen/aktualisiert.")
finally:
transport.close()
def collect_log_lines():
"""Fuehrt alle access.log*-Dateien (auch .gz) zu einem kombinierten Log zusammen.
Gibt den Pfad zur kombinierten Klartext-Logdatei zurueck.
"""
combined = LOGS_DIR / "_combined.log"
sources = sorted(
p for p in LOGS_DIR.iterdir()
if p.is_file()
and p.name.startswith(ACCESS_LOG_PREFIX)
and p.name != combined.name
)
if not sources:
print("\nFEHLER: keine access.log*-Dateien in ./logs/ gefunden.")
print("Erst 'python analyze.py' (ohne --no-download) ausfuehren.")
sys.exit(1)
print(f"\n-> Fuehre {len(sources)} Logdatei(en) zusammen ...")
with open(combined, "wb") as out:
for src in sources:
if src.suffix == ".gz":
with gzip.open(src, "rb") as f:
shutil.copyfileobj(f, out)
else:
with open(src, "rb") as f:
shutil.copyfileobj(f, out)
return combined
def to_wsl_path(win_path):
"""Uebersetzt einen Windows-Pfad (C:\\...) in einen WSL-Pfad (/mnt/c/...)."""
p = Path(win_path).resolve()
drive = p.drive.rstrip(":").lower() # "C:" -> "c"
rest = p.as_posix()[len(p.drive):] # Rest ohne Laufwerk, mit /
return f"/mnt/{drive}{rest}"
def goaccess_args(log_format):
"""Gemeinsame GoAccess-Optionen (ohne Ein-/Ausgabedateien)."""
return [
"--log-format=" + log_format,
"--anonymize-ip", # DSGVO: IPs im Report anonymisieren
"--ignore-crawlers", # Bots aus den Besucherzahlen filtern
"--html-report-title=IWZ Ferienwohnungen - Zugriffsstatistik",
]
def run_goaccess(mode, goaccess_exe, log_file, report_file, log_format):
"""Ruft GoAccess auf und erzeugt einen eigenstaendigen HTML-Report.
mode="wsl": GoAccess laeuft in der WSL (Standard), Pfade werden nach
/mnt/c/... uebersetzt.
mode="exe": nativer Windows-Build unter GOACCESS_EXE.
"""
print(f"\n-> GoAccess ({mode}) -> {report_file.name}")
if mode == "wsl":
cmd = [
"wsl", "goaccess",
to_wsl_path(log_file),
"-o", to_wsl_path(report_file),
] + goaccess_args(log_format)
not_found_hint = (
"GoAccess ist in der WSL nicht installiert (oder WSL fehlt).\n"
" In der WSL installieren: wsl -e sudo apt update ^&^& "
"sudo apt install -y goaccess\n"
"Siehe auch bin/install_py.bat (prueft WSL + GoAccess) und doc/analyse.md."
)
else:
cmd = [
goaccess_exe,
str(log_file),
"-o", str(report_file),
] + goaccess_args(log_format)
not_found_hint = (
f"GoAccess-EXE nicht gefunden ('{goaccess_exe}').\n"
" Windows-Build laden: https://goaccess.io/download, entpacken,\n"
" dann GOACCESS_EXE in bin/.env auf die goaccess.exe setzen."
)
try:
result = subprocess.run(cmd)
except FileNotFoundError:
print("\nFEHLER: GoAccess konnte nicht gestartet werden.")
print(not_found_hint)
sys.exit(1)
if result.returncode != 0:
print("\nFEHLER: GoAccess fehlgeschlagen.")
print("Pruefe die GoAccess-Installation und das Log-Format (LOG_FORMAT).")
sys.exit(1)
if __name__ == "__main__":
print("=== IWZ Nutzungsanalyse ===")
no_download = "--no-download" in sys.argv
env = {**load_env(env_file()), **os.environ}
host = env.get("IONOS_SFTP_HOST")
port = int(env.get("IONOS_SFTP_PORT", SFTP_PORT))
user = env.get("IONOS_SFTP_USER")
password = env.get("IONOS_SFTP_PASSWORD")
remote_dir = env.get("IONOS_LOGS_DIR", "/logs")
# GoAccess-Aufruf: "wsl" (Standard) oder "exe" (nativer Windows-Build).
goaccess_mode = env.get("GOACCESS_MODE", "wsl").strip().lower()
goaccess_exe = env.get("GOACCESS_EXE", "goaccess")
# IONOS liefert i.d.R. Apache "Combined Log Format". Bei anderen
# Formaten hier anpassen (siehe doc/analyse.md).
log_format = env.get("LOG_FORMAT", "COMBINED")
if not no_download:
if not host or not user or not password:
print("\nFEHLER: IONOS_SFTP_HOST / _USER / _PASSWORD fehlen in bin/.env.")
print("Siehe bin/.env.example. Oder mit --no-download nur neu auswerten.")
sys.exit(1)
download_access_logs(host, port, user, password, remote_dir)
else:
print("\n-> Download uebersprungen (--no-download).")
combined = collect_log_lines()
RESULTS_DIR.mkdir(exist_ok=True)
report_file = RESULTS_DIR / f"report_{date.today().isoformat()}.html"
run_goaccess(goaccess_mode, goaccess_exe, combined, report_file, log_format)
print(f"\nFertig! Report: {report_file}")
webbrowser.open(report_file.as_uri())
+11 -3
View File
@@ -13,11 +13,19 @@ import urllib.parse
import urllib.request import urllib.request
from pathlib import Path from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parent PROJECT_ROOT = Path(__file__).resolve().parent.parent # Skript liegt in lib/
PUBLIC_DIR = PROJECT_ROOT / "public" PUBLIC_DIR = PROJECT_ROOT / "public"
FTP_PORT = 21 FTP_PORT = 21
def env_file():
"""Pfad zur .env: bevorzugt IWZ_ENV (von setenv.bat), sonst bin/.env."""
from_var = os.environ.get("IWZ_ENV")
if from_var:
return Path(from_var)
return PROJECT_ROOT / "bin" / ".env"
def load_env(path): def load_env(path):
env = {} env = {}
if not path.exists(): if not path.exists():
@@ -99,7 +107,7 @@ def purge_cdn_cache(pull_zone_url, api_key):
if __name__ == "__main__": if __name__ == "__main__":
print("=== IWZ Deploy ===") print("=== IWZ Deploy ===")
env = {**load_env(PROJECT_ROOT / ".env"), **os.environ} env = {**load_env(env_file()), **os.environ}
zone = env.get("BUNNY_STORAGE_ZONE") zone = env.get("BUNNY_STORAGE_ZONE")
host = env.get("BUNNY_STORAGE_HOST", "storage.bunnycdn.com") host = env.get("BUNNY_STORAGE_HOST", "storage.bunnycdn.com")
password = env.get("BUNNY_STORAGE_PASSWORD") password = env.get("BUNNY_STORAGE_PASSWORD")
@@ -108,7 +116,7 @@ if __name__ == "__main__":
if not zone or not password: if not zone or not password:
print("\nFEHLER: BUNNY_STORAGE_ZONE / BUNNY_STORAGE_PASSWORD fehlen.") print("\nFEHLER: BUNNY_STORAGE_ZONE / BUNNY_STORAGE_PASSWORD fehlen.")
print("Siehe .env.example -> .env kopieren und befuellen.") print("Siehe bin/.env.example -> bin/.env kopieren und befuellen.")
sys.exit(1) sys.exit(1)
hugo_build() hugo_build()
+11 -3
View File
@@ -14,12 +14,20 @@ import subprocess
import sys import sys
from pathlib import Path from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parent PROJECT_ROOT = Path(__file__).resolve().parent.parent # Skript liegt in lib/
PUBLIC_DIR = PROJECT_ROOT / "public" PUBLIC_DIR = PROJECT_ROOT / "public"
MANIFEST_PATH = PROJECT_ROOT / ".deploy_manifest.json" MANIFEST_PATH = PROJECT_ROOT / ".deploy_manifest.json"
FTP_PORT = 21 FTP_PORT = 21
def env_file():
"""Pfad zur .env: bevorzugt IWZ_ENV (von setenv.bat), sonst bin/.env."""
from_var = os.environ.get("IWZ_ENV")
if from_var:
return Path(from_var)
return PROJECT_ROOT / "bin" / ".env"
def load_env(path): def load_env(path):
env = {} env = {}
if not path.exists(): if not path.exists():
@@ -124,14 +132,14 @@ def deploy_to_bunny(host, zone, password, changed, deleted):
if __name__ == "__main__": if __name__ == "__main__":
print("=== IWZ Deploy (nur Änderungen) ===") print("=== IWZ Deploy (nur Änderungen) ===")
env = {**load_env(PROJECT_ROOT / ".env"), **os.environ} env = {**load_env(env_file()), **os.environ}
zone = env.get("BUNNY_STORAGE_ZONE") zone = env.get("BUNNY_STORAGE_ZONE")
host = env.get("BUNNY_STORAGE_HOST", "storage.bunnycdn.com") host = env.get("BUNNY_STORAGE_HOST", "storage.bunnycdn.com")
password = env.get("BUNNY_STORAGE_PASSWORD") password = env.get("BUNNY_STORAGE_PASSWORD")
if not zone or not password: if not zone or not password:
print("\nFEHLER: BUNNY_STORAGE_ZONE / BUNNY_STORAGE_PASSWORD fehlen.") print("\nFEHLER: BUNNY_STORAGE_ZONE / BUNNY_STORAGE_PASSWORD fehlen.")
print("Siehe .env.example -> .env kopieren und befuellen.") print("Siehe bin/.env.example -> bin/.env kopieren und befuellen.")
sys.exit(1) sys.exit(1)
hugo_build() hugo_build()
+12 -3
View File
@@ -6,15 +6,24 @@ fuer die per Bunny Token Authentication geschuetzte Pull Zone.
import base64 import base64
import hashlib import hashlib
import os
import sys import sys
import time import time
from pathlib import Path from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parent PROJECT_ROOT = Path(__file__).resolve().parent.parent # Skript liegt in lib/
GUELTIGKEIT_SEKUNDEN = 7 * 24 * 60 * 60 # 7 Tage GUELTIGKEIT_SEKUNDEN = 7 * 24 * 60 * 60 # 7 Tage
URL_PATH = "/" URL_PATH = "/"
def env_file():
"""Pfad zur .env: bevorzugt IWZ_ENV (von setenv.bat), sonst bin/.env."""
from_var = os.environ.get("IWZ_ENV")
if from_var:
return Path(from_var)
return PROJECT_ROOT / "bin" / ".env"
def load_env(path): def load_env(path):
env = {} env = {}
if not path.exists(): if not path.exists():
@@ -36,12 +45,12 @@ def build_signed_url(pull_zone_url, auth_key, url_path, expires):
if __name__ == "__main__": if __name__ == "__main__":
env = load_env(PROJECT_ROOT / ".env") env = load_env(env_file())
pull_zone_url = env.get("BUNNY_PULL_ZONE_URL", "").rstrip("/") pull_zone_url = env.get("BUNNY_PULL_ZONE_URL", "").rstrip("/")
auth_key = env.get("BUNNY_AUTH_KEY") auth_key = env.get("BUNNY_AUTH_KEY")
if not pull_zone_url or not auth_key: if not pull_zone_url or not auth_key:
print("\nFEHLER: BUNNY_PULL_ZONE_URL / BUNNY_AUTH_KEY fehlen in .env.") print("\nFEHLER: BUNNY_PULL_ZONE_URL / BUNNY_AUTH_KEY fehlen in bin/.env.")
print("Siehe .env.example. Auth Key: Pull Zone -> Security -> Token Authentication.") print("Siehe .env.example. Auth Key: Pull Zone -> Security -> Token Authentication.")
sys.exit(1) sys.exit(1)
-8
View File
@@ -1,8 +0,0 @@
@echo off
echo.
echo === Ferienwohnungen Wolframm - Vorschau-Link erzeugen ===
python preview_link.py
echo.
echo Link oben markieren und mit Strg+C kopieren.
echo.
cmd /k
+9
View File
@@ -0,0 +1,9 @@
# Python-Abhaengigkeiten fuer die lib/-Skripte.
#
# deploy.py / deploy_lastdiff.py / preview_link.py:
# nutzen ausschliesslich die Standardbibliothek (ftplib, urllib, hashlib,
# json, subprocess ...) -> keine externen Pakete noetig.
#
# analyze.py:
# paramiko fuer den SFTP-Download der IONOS-Access-Logs.
paramiko
+62 -5
View File
@@ -257,9 +257,34 @@ img { max-width: 100%; height: auto; display: block; }
.highlight-card h3 { margin-bottom: 0.5rem; font-size: 1.05rem; } .highlight-card h3 { margin-bottom: 0.5rem; font-size: 1.05rem; }
.highlight-card p { color: var(--text-muted); font-size: 0.9rem; } .highlight-card p { color: var(--text-muted); font-size: 0.9rem; }
/* ===== Belegungsplan-Band (auffälliger Verweis auf der Startseite) ===== */
.belegungsplan-band {
background: #c0392b;
padding: 1.6rem 0;
text-align: center;
}
.belegungsplan-band-btn {
background: #e67e22;
color: #fff;
border: none;
font-size: 1.15rem;
font-weight: 700;
padding: 0.9rem 2rem;
box-shadow: 0 2px 8px rgba(0,0,0,0.25);
}
.belegungsplan-band-btn:hover {
background: #d35400;
color: #fff;
}
/* ===== Belegungsplan (Übersicht ein-/ausblenden) ===== */ /* ===== Belegungsplan (Übersicht ein-/ausblenden) ===== */
.belegungsplan-toggle { margin: 1rem 0 2.5rem; } .belegungsplan-toggle { margin: 1rem 0 2.5rem; }
.belegungsplan-frame { margin-top: 1.5rem; } .belegungsplan-frame {
margin-top: 1.5rem;
background: #fff;
padding: 1rem;
border-radius: var(--radius);
}
.belegungsplan-frame[hidden] { display: none; } .belegungsplan-frame[hidden] { display: none; }
/* ===== FeWo Grid ===== */ /* ===== FeWo Grid ===== */
@@ -281,6 +306,8 @@ img { max-width: 100%; height: auto; display: block; }
.fewo-card-img { width: 100%; height: 200px; object-fit: cover; transition: opacity 0.2s; } .fewo-card-img { width: 100%; height: 200px; object-fit: cover; transition: opacity 0.2s; }
.fewo-card-imglink:hover .fewo-card-img { opacity: 0.9; } .fewo-card-imglink:hover .fewo-card-img { opacity: 0.9; }
.fewo-card-body { padding: 1.5rem; } .fewo-card-body { padding: 1.5rem; }
.fewo-card-actions { display: flex; gap: 0.5rem; flex-wrap: wrap; }
.fewo-card-actions .btn { margin-right: 0; }
.fewo-card-body h2, .fewo-card-body h3 { margin-bottom: 0.5rem; font-size: 1.2rem; } .fewo-card-body h2, .fewo-card-body h3 { margin-bottom: 0.5rem; font-size: 1.2rem; }
.fewo-card-body h2 a, .fewo-card-body h3 a { color: var(--text); } .fewo-card-body h2 a, .fewo-card-body h3 a { color: var(--text); }
.fewo-meta { .fewo-meta {
@@ -311,10 +338,13 @@ img { max-width: 100%; height: auto; display: block; }
/* ===== Karten-Grid (wiederverwendet für Umfeld + Ausstattung) ===== */ /* ===== Karten-Grid (wiederverwendet für Umfeld + Ausstattung) ===== */
.spec-grid { .spec-grid {
display: grid; display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 1.25rem; gap: 1.25rem;
} }
@media (max-width: 760px) { @media (max-width: 980px) {
.spec-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 560px) {
.spec-grid { grid-template-columns: 1fr; } .spec-grid { grid-template-columns: 1fr; }
} }
.spec-block { .spec-block {
@@ -337,7 +367,6 @@ img { max-width: 100%; height: auto; display: block; }
/* ===== Ausstattung (einzelne Ferienwohnung) ===== */ /* ===== Ausstattung (einzelne Ferienwohnung) ===== */
.ausstattung { margin-top: 2rem; } .ausstattung { margin-top: 2rem; }
.ausstattung h2 { margin: 1.5rem 0 1rem; font-size: 1.2rem; border-bottom: 1px solid var(--border); padding-bottom: 0.25rem; } .ausstattung h2 { margin: 1.5rem 0 1rem; font-size: 1.2rem; border-bottom: 1px solid var(--border); padding-bottom: 0.25rem; }
.ausstattung-hinweis { margin-top: 1.5rem; font-size: 0.95rem; color: var(--text-muted); }
/* ===== Ausflugsziele-Gruppen ===== */ /* ===== Ausflugsziele-Gruppen ===== */
.ausflug-gruppe { margin-top: 3rem; } .ausflug-gruppe { margin-top: 3rem; }
@@ -548,7 +577,7 @@ img { max-width: 100%; height: auto; display: block; }
content: ""; content: "";
position: absolute; position: absolute;
inset: 0; inset: 0;
background: rgba(255,255,255,0.6); background: transparent;
} }
.zitat-box-inner { .zitat-box-inner {
position: relative; position: relative;
@@ -610,3 +639,31 @@ img { max-width: 100%; height: auto; display: block; }
/* ===== Main ===== */ /* ===== Main ===== */
.main { min-height: calc(100vh - var(--nav-height) - 200px); } .main { min-height: calc(100vh - var(--nav-height) - 200px); }
/* ===== Blog ===== */
.blog-grid { margin-top: 2rem; }
.blog-date { color: var(--muted, #777); font-size: 0.85rem; margin-bottom: 0.4rem; }
.container--narrow { max-width: 760px; }
.blog-hero-img {
width: 100%;
max-height: 420px;
object-fit: cover;
border-radius: var(--radius, 6px);
}
.blog-post h2 { margin-top: 2.5rem; }
.blog-back { margin-top: 2.5rem; }
.blog-figure { margin: 1rem 0 1.5rem; }
.blog-figure img {
width: 100%;
max-height: 380px;
object-fit: cover;
border-radius: var(--radius, 6px);
}
.blog-hero { margin: 0 0 2rem; }
.bild-quelle {
font-size: 0.78rem;
color: var(--muted, #777);
margin-top: 0.4rem;
line-height: 1.4;
}
.bild-quelle a { color: var(--muted, #777); text-decoration: underline; }