diff --git a/.gitignore b/.gitignore index 1c3fa73..fe504e0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ public/ resources/ .hugo_build.lock +.env diff --git a/CLAUDE.md b/CLAUDE.md index 9ec4444..21b43bd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,12 +8,12 @@ Drei Ferienwohnungen in Kaufbeuren-Hirschzell / Allgäu. ## Stack - **Static Site Generator**: Hugo (keine externe Abhängigkeit, einzelne .exe) -- **Hosting**: Cloudflare Pages (kostenlos, automatisches HTTPS) -- **DNS**: Cloudflare (Nameserver-Wechsel von IONOS zu Cloudflare) -- **Domain-Registrar**: IONOS (bleibt so, nur DNS wandert zu Cloudflare) +- **Hosting**: Bunny.net (Bunny CDN / Storage Zone, kostenpflichtig nach Traffic) +- **DNS**: Bunny.net DNS oder weiterhin IONOS (siehe Umzug-Notizen, ggf. anzupassen) +- **Domain-Registrar**: IONOS (bleibt so) - **Buchungskalender**: webplanner.de (externer Dienst, per Link eingebunden) -- **Git**: eigene Gitea-Instanz (kein GitHub/GitLab) -- **Deploy**: manuell per `deploy.bat` oder `deploy.py` → `wrangler pages deploy` +- **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) ## Projektstruktur @@ -72,8 +72,11 @@ bilder: - [x] Telefon, E-Mail, Adresse in `hugo.toml` unter `[params]` eintragen - [x] `content/impressum.md` mit echten Pflichtangaben füllen - [x] `content/datenschutz.md` durch rechtskonforme Version ersetzen -- [ ] Cloudflare-Account anlegen + Wrangler einrichten (`wrangler login`) -- [ ] Nameserver bei IONOS auf Cloudflare umstellen +- [x] Bunny.net-Account anlegen + Storage Zone für die Seite erstellen +- [x] Bunny Storage-Passwort besorgen, sicher hinterlegen (`.env`, nicht ins Git) +- [x] `deploy.bat` / `deploy.py` um Upload zur Bunny Storage Zone erweitern (FTP, Port 21, passiv) +- [ ] Bunny Pull Zone / CDN vor die Storage Zone schalten (Auslieferung + HTTPS) +- [ ] Domain-Anbindung an Bunny klären (DNS bei IONOS auf Bunny zeigen lassen, oder DNS zu Bunny umziehen) ## Lokaler Dev-Server @@ -84,9 +87,12 @@ hugo server ## Deploy +Deploy läuft lokal per Skript (kein CI/CD, da Repo bei Gitea liegt). Referenz für den Bunny-Upload-Schritt ist die Vorlage `.github/workflows/deploy.yml` (GitHub-Actions-Syntax), deren Build- und Deploy-Logik 1:1 in `deploy.bat`/`deploy.py` nachgebaut wird: + ```powershell -# Einmalig: -wrangler login +hugo --gc --minify +# danach: Upload von ./public zur Bunny.net Storage Zone +# (Storage Zone Name + API-Key/Storage-Passwort als Secrets/env, nicht im Git) # Bei jeder Änderung: deploy.bat diff --git a/Umzug.md b/Umzug.md index 983be28..9443b76 100644 --- a/Umzug.md +++ b/Umzug.md @@ -1,16 +1,166 @@ -# SSL/HTTPS auf Cloudflare Pages +# Bunny.net – Ersteinrichtung und Vorab-Veröffentlichung + +Ziel: `iwz-ferienwohnungen` bei Bunny.net anlegen, eine erste Fassung der Hugo-Seite +online stellen und den Zugriff darauf auf dich + den Kunden beschränken (Vorschau, +bevor die Seite öffentlich unter der echten Domain läuft). + +> Hinweis: Dieser Weg ersetzt den früher geplanten Cloudflare-Pages-Ansatz. +> Details zum alten Cloudflare/IONOS-Plan siehe weiter unten im Abschnitt +> "Alt: Cloudflare-Variante (nicht mehr aktuell)". + +## 1. Bunny.net-Account anlegen + +1. Auf [bunny.net](https://bunny.net) registrieren (E-Mail + Passwort, keine + Kreditkarte für den Start nötig, Abrechnung erfolgt nach Traffic/Storage). +2. E-Mail-Adresse bestätigen und einloggen ins Dashboard. + +## 2. Storage Zone anlegen (Ablage für die gebauten Hugo-Dateien) + +1. Im Dashboard-Menü **Storage** → **Add Storage Zone**. +2. Name vergeben, z. B. `iwz-ferienwohnungen`. +3. **Main Region**: Region wählen, die am nächsten an euch/den Besuchern liegt + (z. B. Frankfurt/Falkenstein, Europa). +4. Replikations-Regionen optional dazuwählen, falls später Besucher aus anderen + Kontinenten erwartet werden (für die Vorschau-Phase nicht nötig). +5. Zone erstellen. + +## 3. Zugangsdaten der Storage Zone notieren + +1. Storage Zone öffnen → Reiter **FTP & API Access**. +2. **Hostname**, **Username** und **Password** (nicht das Read-only-Passwort!) + notieren – diese werden später für den Upload per `deploy.bat`/`deploy.py` + gebraucht (als `BUNNY_STORAGE_ZONE` und `BUNNY_API_KEY`, siehe CLAUDE.md). +3. Zugangsdaten **nicht** ins Git-Repo einchecken (z. B. in `.env` oder als + Windows-Umgebungsvariable ablegen). + +## 4. Erste Fassung hochladen + +Lokal die Seite bauen und die `public/`-Inhalte in die Storage Zone hochladen: + +```powershell +hugo --gc --minify +``` + +Für den allerersten Test kann der Upload manuell per **File Manager** im Bunny- +Dashboard (Storage Zone → Files → Drag & Drop von `public/`) erfolgen. Sobald +`deploy.bat`/`deploy.py` entsprechend erweitert ist, übernimmt das Skript diesen +Schritt automatisch (Ordnerinhalt von `public/` 1:1 in die Storage Zone spiegeln). + +## 5. Pull Zone anlegen (macht die Storage Zone über HTTPS erreichbar) + +1. In der Storage Zone → **Connected Pull Zones** → **Connect Pull Zone** + (oder im Menü **CDN → Add Pull Zone** und als Origin die Storage Zone wählen). +2. Namen vergeben, z. B. `iwz-ferienwohnungen`. +3. Zonen für die Auslieferung wählen (Europa reicht für die Vorschau). +4. Pull Zone erstellen → Bunny vergibt automatisch eine Test-URL: + `https://iwz-ferienwohnungen.b-cdn.net` +5. HTTPS ist auf dieser `b-cdn.net`-URL automatisch aktiv, kein eigenes + Zertifikat nötig. + +Damit ist die Seite technisch live – als nächstes wird sie geschützt, bevor der +Link an den Kunden geht. + +## 6. Zugriff beschränken: nur du + der Kunde + +Bunny.net Pull Zones bieten dafür **Token Authentication**: Requests an die +Pull Zone werden nur ausgeliefert, wenn die URL einen gültigen, signierten Token +enthält. Es gibt keinen klassischen Basic-Auth-Login-Dialog im Browser – +stattdessen bekommt der Kunde einen fertigen Link mit Token, der bis zu einem +festgelegten Ablaufzeitpunkt gültig ist. + +### 6.1 Token Authentication aktivieren + +1. Pull Zone öffnen → Reiter **Security**. +2. **Token Authentication** aktivieren. +3. Den angezeigten **Authentication Key** kopieren und sicher aufbewahren + (nicht ins Git-Repo, nicht per unverschlüsselter Mail verschicken) – wer + diesen Key kennt, kann selbst gültige Links erzeugen. + +Ab jetzt liefert die Pull Zone **nichts mehr aus**, ohne gültigen Token in der +URL – das ist der eigentliche Zugriffsschutz. + +### 6.2 Signierten Link erzeugen + +Ein signierter Link besteht aus einem Hash über `Authentication Key + Pfad + +Ablaufzeitpunkt`. Das lässt sich einfach per kleinem Python-Snippet erzeugen, +z. B. lokal ausführen, wenn ein neuer Vorschau-Link für den Kunden gebraucht +wird: + +```python +import hashlib +import base64 +import time + +security_key = "DEIN_AUTHENTICATION_KEY" +url_path = "/index.html" # z.B. Startseite, oder "/" je nach Setup +expires = int(time.time()) + 7 * 24 * 60 * 60 # 7 Tage gültig + +hashable = f"{security_key}{url_path}{expires}" +token_hash = hashlib.sha256(hashable.encode()).digest() +token = base64.urlsafe_b64encode(token_hash).decode().rstrip("=") + +print(f"https://iwz-ferienwohnungen.b-cdn.net{url_path}?token={token}&expires={expires}") +``` + +Den ausgegebenen Link schickst du dem Kunden. Läuft die Gültigkeit ab, einfach +ein neues Snippet mit neuem `expires`-Wert ausführen und den frischen Link +verschicken. + +### 6.3 Praktisch für Vorschau-Zwecke + +- Länge der Gültigkeit (`expires`) nach Bedarf wählen – z. B. 7 Tage für eine + kurze Abstimmungsrunde, oder wenige Stunden für einen einzelnen Termin. +- Ohne gültigen Token bekommt jeder andere Besucher einen Fehler von Bunny – + die Seite ist damit faktisch nur über den geteilten Link erreichbar. +- Sobald die Seite live gehen soll (echte Domain, öffentlich), wird Token + Authentication in der Pull Zone einfach wieder deaktiviert. + +## 7. Domain-Anbindung (später, für den echten Umzug) + +Noch offen und erst relevant, wenn die Seite final ist: + +- Eigene Domain (`iwz-ferienwohnungen.de`) als Custom Hostname in der Pull Zone + hinterlegen. +- Entweder DNS bei IONOS per CNAME auf die Bunny-Pull-Zone zeigen lassen, oder + DNS komplett zu Bunny.net DNS umziehen (Nameserver-Wechsel bei IONOS, + ähnlich wie ursprünglich für Cloudflare geplant). +- SSL-Zertifikat für die eigene Domain wird von Bunny automatisch ausgestellt, + sobald der Hostname verifiziert ist. +- Domain-Registrar bleibt in jedem Fall IONOS. + +## Zusammenfassung des Stacks (aktuell) + +``` +Domain-Registrar: IONOS (bleibt so) +DNS: noch offen (IONOS bleibt vorerst / später ggf. Bunny.net DNS) +Hosting: Bunny.net (Storage Zone + Pull Zone/CDN) +Vorschau-Schutz: Bunny Token Authentication (signierte, befristete Links) +Code/Content: Gitea (eigene Instanz, kein CI/CD-Workflow) +Deploy: manuell per deploy.bat / deploy.py (hugo build + Upload zur Storage Zone) +Buchungskalender: webplanner.de (bleibt so, extern) +``` + +--- + +# Alt: Cloudflare-Variante (nicht mehr aktuell) + +Der folgende Abschnitt beschreibt den ursprünglich geplanten Umzug auf +Cloudflare Pages und wird nicht mehr weiterverfolgt (Entscheidung: Umstieg auf +Bunny.net, siehe oben). Als Referenz stehen gelassen. + +## SSL/HTTPS auf Cloudflare Pages Ja, vollautomatisch. Es gibt keine Tokens oder Zertifikate, die du selbst beantragen oder verlängern musst. Das läuft komplett im Hintergrund über Let's Encrypt, gesteuert von Cloudflare. Du siehst davon nichts. -# DNS und IP-Adresse — das ist der entscheidende Teil +## DNS und IP-Adresse — das ist der entscheidende Teil Hier gibt es zwei verschiedene Szenarien: -## Szenario A: Domain bei einem anderen Registrar (z.B. IONOS, Strato, Hetzner) +### Szenario A: Domain bei einem anderen Registrar (z.B. IONOS, Strato, Hetzner) Du hast zwei Möglichkeiten: -### Option 1 — Nameserver umziehen (empfohlen): +#### Option 1 — Nameserver umziehen (empfohlen): Beim Registrar trägst du statt dessen Nameserver die Cloudflare-Nameserver ein: aria.ns.cloudflare.com @@ -20,10 +170,10 @@ Danach verwaltest du alle DNS-Einträge bei Cloudflare. Cloudflare kennt dann de www → dein-projekt.pages.dev -### Option 2 — Nur einen CNAME/A-Record setzen (ohne Nameserver-Wechsel): +#### Option 2 — Nur einen CNAME/A-Record setzen (ohne Nameserver-Wechsel): Beim Registrar trägst du einen CNAME auf dein-projekt.pages.dev ein. Cloudflare Pages erkennt die Domain und stellt trotzdem ein Zertifikat aus — dauert aber etwas länger zur Verifizierung. -## Szenario B: Domain direkt bei Cloudflare registriert +### Szenario B: Domain direkt bei Cloudflare registriert Dann ist alles von Anfang an in einem Dashboard, kein Umzug nötig. @@ -45,9 +195,7 @@ Danach läuft alles von selbst — das Zertifikat verlängert sich alle 90 Tage # Wo ist die Domain iwz-ferienwohnungen.de aktuell registriert? - - -## Ablauf IONOS +## Ablauf IONOS Nameserver-Wechsel bei IONOS zu Cloudflare ### Schritt 1: Cloudflare-Account anlegen und Domain hinzufügen @@ -76,11 +224,11 @@ IONOS bleibt weiterhin dein Domain-Registrar — das ist wichtig zu verstehen. D Falls über IONOS auch E-Mail-Adressen laufen (z.B. info@iwz-ferienwohnungen.de), dann müssen die MX-Einträge beim Import in Cloudflare korrekt übernommen werden. Cloudflare importiert die normalerweise automatisch, aber das solltest du einmal kontrollieren bevor du den Nameserver-Wechsel machst. Die E-Mail funktioniert dann wie bisher — Cloudflare leitet den Mail-Verkehr nur weiter, hostet ihn nicht selbst. -# Zusammenfassung des gesamten Stacks +# Zusammenfassung des gesamten Stacks (alt, Cloudflare) Domain-Registrar: IONOS (bleibt so) DNS + SSL: Cloudflare (kostenlos) Hosting: Cloudflare Pages (kostenlos) Code/Content: GitHub Repository (kostenlos) Buchungskalender: webplanner.de (bleibt so, extern) -Laufende Kosten danach: *nur noch die Domain-Verlängerung bei IONOS* (~10–15 € pro Jahr). \ No newline at end of file +Laufende Kosten danach: *nur noch die Domain-Verlängerung bei IONOS* (~10–15 € pro Jahr). diff --git a/deploy.bat b/deploy.bat index e0f7a5e..c02cfe4 100644 --- a/deploy.bat +++ b/deploy.bat @@ -1,22 +1,10 @@ @echo off echo. -echo === IWZ Ferienwohnungen – Deploy === +echo === IWZ Ferienwohnungen - Deploy === echo. -echo Hugo Build... -hugo +python deploy.py if %errorlevel% neq 0 ( - echo FEHLER: Hugo Build fehlgeschlagen! pause exit /b 1 ) -echo. -echo Deploy zu Cloudflare Pages... -wrangler pages deploy public/ --project-name iwz-ferienwohnungen -if %errorlevel% neq 0 ( - echo FEHLER: Deploy fehlgeschlagen! - pause - exit /b 1 -) -echo. -echo Fertig! Seite ist live. pause diff --git a/deploy.py b/deploy.py index df8a385..fd2be3b 100644 --- a/deploy.py +++ b/deploy.py @@ -1,25 +1,79 @@ #!/usr/bin/env python3 """ -IWZ Ferienwohnungen – Deploy Script -Baut die Hugo-Seite und deployt sie zu Cloudflare Pages. +IWZ Ferienwohnungen - Deploy Script +Baut die Hugo-Seite und deployt sie per FTP zur Bunny.net Storage Zone. """ +import ftplib +import os import subprocess import sys +from pathlib import Path -PROJECT = "iwz-ferienwohnungen" +PROJECT_ROOT = Path(__file__).resolve().parent +PUBLIC_DIR = PROJECT_ROOT / "public" +FTP_PORT = 21 -def run(cmd, desc): - print(f"\n→ {desc}...") - result = subprocess.run(cmd, shell=True) +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 hugo_build(): + print("\n-> Hugo Build...") + result = subprocess.run("hugo --gc --minify", shell=True) if result.returncode != 0: - print(f"\nFEHLER bei: {desc}") + print("\nFEHLER: Hugo Build fehlgeschlagen!") sys.exit(1) +def ftp_upload_dir(ftp, local_dir, remote_dir=""): + for entry in sorted(local_dir.iterdir()): + remote_path = f"{remote_dir}/{entry.name}" if remote_dir else entry.name + if entry.is_dir(): + try: + ftp.mkd(remote_path) + except ftplib.error_perm: + pass # existiert bereits + ftp_upload_dir(ftp, entry, remote_path) + else: + with open(entry, "rb") as f: + ftp.storbinary(f"STOR {remote_path}", f) + print(f" {remote_path}") + + +def deploy_to_bunny(host, zone, password): + print(f"\n-> Deploy zu Bunny.net Storage Zone '{zone}'...") + with ftplib.FTP() as ftp: + ftp.connect(host, FTP_PORT) + ftp.login(zone, password) + ftp.set_pasv(True) + ftp_upload_dir(ftp, PUBLIC_DIR) + + if __name__ == "__main__": print("=== IWZ Deploy ===") - run("hugo", "Hugo Build") - run(f'wrangler pages deploy public/ --project-name {PROJECT}', "Cloudflare Pages Deploy") - print("\n✓ Fertig! Seite ist live.") + + env = {**load_env(PROJECT_ROOT / ".env"), **os.environ} + zone = env.get("BUNNY_STORAGE_ZONE") + host = env.get("BUNNY_STORAGE_HOST", "storage.bunnycdn.com") + password = env.get("BUNNY_STORAGE_PASSWORD") + + if not zone or not password: + print("\nFEHLER: BUNNY_STORAGE_ZONE / BUNNY_STORAGE_PASSWORD fehlen.") + print("Siehe .env.example -> .env kopieren und befuellen.") + sys.exit(1) + + hugo_build() + deploy_to_bunny(host, zone, password) + + print("\nFertig! Seite ist live.") diff --git a/preview_link.py b/preview_link.py new file mode 100644 index 0000000..6969bf0 --- /dev/null +++ b/preview_link.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +""" +IWZ Ferienwohnungen - erzeugt einen befristeten, signierten Vorschau-Link +fuer die per Bunny Token Authentication geschuetzte Pull Zone. +""" + +import base64 +import hashlib +import sys +import time +from pathlib import Path + +PROJECT_ROOT = Path(__file__).resolve().parent +GUELTIGKEIT_SEKUNDEN = 7 * 24 * 60 * 60 # 7 Tage +URL_PATH = "/" + + +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 build_signed_url(pull_zone_url, auth_key, url_path, expires): + hashable = f"{auth_key}{url_path}{expires}" + token_hash = hashlib.sha256(hashable.encode()).digest() + token = base64.urlsafe_b64encode(token_hash).decode().rstrip("=") + return f"{pull_zone_url}{url_path}?token={token}&expires={expires}" + + +if __name__ == "__main__": + env = load_env(PROJECT_ROOT / ".env") + pull_zone_url = env.get("BUNNY_PULL_ZONE_URL", "").rstrip("/") + auth_key = env.get("BUNNY_AUTH_KEY") + + if not pull_zone_url or not auth_key: + print("\nFEHLER: BUNNY_PULL_ZONE_URL / BUNNY_AUTH_KEY fehlen in .env.") + print("Siehe .env.example. Auth Key: Pull Zone -> Security -> Token Authentication.") + sys.exit(1) + + expires = int(time.time()) + GUELTIGKEIT_SEKUNDEN + link = build_signed_url(pull_zone_url, auth_key, URL_PATH, expires) + + gueltig_bis = time.strftime("%d.%m.%Y %H:%M", time.localtime(expires)) + print("\n=== Vorschau-Link (Bunny Token Authentication) ===\n") + print(link) + print(f"\nGueltig bis: {gueltig_bis}\n") diff --git a/provide_previewlink.bat b/provide_previewlink.bat new file mode 100644 index 0000000..9db71ac --- /dev/null +++ b/provide_previewlink.bat @@ -0,0 +1,8 @@ +@echo off +echo. +echo === IWZ Ferienwohnungen - Vorschau-Link erzeugen === +python preview_link.py +echo. +echo Link oben markieren und mit Strg+C kopieren. +echo. +cmd /k