commit 7f820a64784d6a9e860a54ce4f64d713970fe1ed Author: Burkhard Naumann Date: Sat May 30 00:09:33 2026 +0200 Reiseblog mit CMS diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..de35ccf --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +.astro/ +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..2828fd8 --- /dev/null +++ b/README.md @@ -0,0 +1,81 @@ +# đŸšČ Radweltreise-Blog + +Statische Website (kein PHP, keine Datenbank) fĂŒr ein Reisetagebuch mit +Gesamtroute auf einer Karte. Gebaut mit [Astro](https://astro.build) + +[Leaflet](https://leafletjs.com). + +- **Karte oben**, klein – Klick aufs Symbol (‹) macht sie groß (Vollbild). +- **Route** entsteht automatisch aus deinen **GPX-Dateien** (z. B. Komoot-Export). +- **Foto-Marker** entstehen automatisch aus den **GPS-Daten (EXIF)** deiner Fotos. + +## Lokal starten + +```bash +npm install +npm run dev +``` + +Dann im Browser http://localhost:4321 öffnen. + +Produktions-Build (erzeugt den Ordner `dist/` mit fertigem HTML): + +```bash +npm run build +npm run preview # zum lokalen Anschauen des Builds +``` + +## Neuen Tagebucheintrag schreiben + +Lege eine Markdown-Datei in `src/pages/posts/` an, z. B. +`src/pages/posts/2026-06-05-andorra.md`: + +```markdown +--- +layout: ../../layouts/Base.astro +title: Ankunft in Andorra +date: 2026-06-05 +--- + +# Ankunft in Andorra + +Text, Text, Text 
 + +![Bildunterschrift](/photos/IMG_2042.jpg) +``` + +## GPX-Strecke "hochladen" (Komoot & Co.) + +1. In der App die Etappe als **GPX exportieren**. +2. Die Datei in den Ordner **`src/data/tracks/`** legen. + Tipp: Dateinamen mit Datum beginnen (`2026-06-05-andorra.gpx`), + dann ist die Reihenfolge automatisch korrekt. +3. Neu bauen / pushen (siehe unten). Fertig – die Linie hĂ€ngt sich an die Route an. + +Es ist kein Upload-Formular nötig: "Hochladen" = Datei in den Ordner legen. +Vom Handy unterwegs am einfachsten mit **Working Copy** (iOS) bzw. +**Termux/MGit** (Android) per Git, oder mit einem **Syncthing/Nextcloud**-Ordner. + +## Fotos mit Karten-Marker + +Original-Fotos (mit GPS im EXIF) in **`public/photos/`** ablegen. Beim Bauen +werden Koordinaten + Aufnahmezeit ausgelesen und als Marker gesetzt. + +⚠ Nicht ĂŒber Dienste schicken, die EXIF entfernen (z. B. WhatsApp). Die +Originaldatei verwenden. + +## Auf den eigenen Server bringen (Kurzfassung) + +Statische Dateien aus `dist/` werden von einem Webserver ohne PHP ausgeliefert. +Empfehlung **Caddy** (automatisches HTTPS): + +``` +radweltreise.example.com { + root * /var/www/radweltreise/dist + file_server + encode gzip +} +``` + +Automatischer Rebuild bei Git-Push: einen `post-receive`-Hook im Bare-Repo +auf dem Server `npm ci && npm run build` ausfĂŒhren lassen. Sag Bescheid, +dann richten wir das Schritt fĂŒr Schritt ein. diff --git a/astro.config.mjs b/astro.config.mjs new file mode 100644 index 0000000..6b88742 --- /dev/null +++ b/astro.config.mjs @@ -0,0 +1,5 @@ +import { defineConfig } from 'astro/config'; + +export default defineConfig({ + site: 'https://www.sattelfest.org', +}); diff --git a/deploy/Caddyfile b/deploy/Caddyfile new file mode 100644 index 0000000..748b7ad --- /dev/null +++ b/deploy/Caddyfile @@ -0,0 +1,17 @@ +# /etc/caddy/Caddyfile +# Statische Auslieferung mit automatischem HTTPS (Let's Encrypt). + +www.sattelfest.org { + root * /srv/sattelfest/dist + file_server + encode gzip zstd + + # Fotos dĂŒrfen lange im Browser-Cache bleiben. + @photos path /photos/* + header @photos Cache-Control "public, max-age=2592000" +} + +# Ohne "www" auf "www" weiterleiten. +sattelfest.org { + redir https://www.sattelfest.org{uri} permanent +} diff --git a/deploy/apache-git-sattelfest.conf b/deploy/apache-git-sattelfest.conf new file mode 100644 index 0000000..985d423 --- /dev/null +++ b/deploy/apache-git-sattelfest.conf @@ -0,0 +1,15 @@ +# Nur der ZUSÄTZLICHE VirtualHost fĂŒr den Git-Host (Forgejo). +# Deine bestehenden vHosts fĂŒr www/apex bleiben unangetastet. +# +# Ablegen als: /etc/apache2/sites-available/git-sattelfest.conf +# Aktivieren: sudo a2ensite git-sattelfest && sudo systemctl reload apache2 +# HTTPS danach mit certbot ergĂ€nzen (siehe Anleitung). + + + ServerName git.sattelfest.org + + ProxyPreserveHost On + ProxyRequests Off + ProxyPass / http://localhost:3000/ + ProxyPassReverse / http://localhost:3000/ + diff --git a/deploy/apache-sattelfest.conf b/deploy/apache-sattelfest.conf new file mode 100644 index 0000000..96bdbb0 --- /dev/null +++ b/deploy/apache-sattelfest.conf @@ -0,0 +1,43 @@ +# Apache-Vorlage – Alternative zur Caddyfile. +# Ablegen als: /etc/apache2/sites-available/sattelfest.conf +# Aktivieren: sudo a2ensite sattelfest && sudo systemctl reload apache2 +# +# HTTPS fĂŒgt certbot automatisch hinzu (siehe Anleitung) – hier stehen +# zunĂ€chst nur die HTTP-(Port-80-)VirtualHosts. + +# --- Statische Website --------------------------------------------------- + + ServerName www.sattelfest.org + DocumentRoot /srv/sattelfest/dist + + + Require all granted + Options -Indexes + AllowOverride None + + + # Fotos lange im Browser-Cache halten + + Header set Cache-Control "public, max-age=2592000" + + + # Komprimierung + + AddOutputFilterByType DEFLATE text/html text/css application/javascript application/json image/svg+xml + + + +# --- Ohne "www" auf "www" weiterleiten ----------------------------------- + + ServerName sattelfest.org + Redirect permanent / https://www.sattelfest.org/ + + +# --- Git-Host (Forgejo) – Reverse Proxy ---------------------------------- + + ServerName git.sattelfest.org + ProxyPreserveHost On + ProxyRequests Off + ProxyPass / http://localhost:3000/ + ProxyPassReverse / http://localhost:3000/ + diff --git a/deploy/forgejo-post-receive.sh b/deploy/forgejo-post-receive.sh new file mode 100644 index 0000000..0477610 --- /dev/null +++ b/deploy/forgejo-post-receive.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# Forgejo Build-Hook fĂŒr die Umgebung mit Apache. +# Eintragen unter: Repo -> Einstellungen -> Git-Hooks -> post-receive. +# (Voraussetzung: Admin hat Git-Hooks aktiviert, siehe Anleitung.) +# +# Ablauf: Code auschecken -> bauen -> Ergebnis in den Apache-DocumentRoot kopieren. + +set -euo pipefail + +WORKTREE=/srv/sattelfest # Arbeits-/Build-Verzeichnis (Quellcode) +PUBLISH=/var/www/sattelfest # Apache DocumentRoot (fertige Website) + +export PATH="/usr/local/bin:/usr/bin:/bin:$PATH" + +git --work-tree="$WORKTREE" checkout -f main + +cd "$WORKTREE" +npm ci +npm run build + +# Fertige Dateien in den DocumentRoot spiegeln (--delete entfernt Altes). +rsync -a --delete "$WORKTREE/dist/" "$PUBLISH/" + +echo "✅ Deploy fertig: $(date)" diff --git a/deploy/post-receive b/deploy/post-receive new file mode 100644 index 0000000..3d840f9 --- /dev/null +++ b/deploy/post-receive @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# Git-Hook: lĂ€uft auf dem Server nach jedem "git push". +# Checkt den Code aus und baut die statische Seite neu. +# +# Installation: nach /srv/git/sattelfest.git/hooks/post-receive kopieren +# und ausfĂŒhrbar machen (chmod +x). + +set -euo pipefail + +GIT_DIR_PATH=/srv/git/sattelfest.git +WORKTREE=/srv/sattelfest +BRANCH=main + +# Node/npm (NodeSource installiert nach /usr/bin) auffindbar machen. +export PATH="/usr/local/bin:/usr/bin:/bin:$PATH" + +echo "→ Code auschecken 
" +git --git-dir="$GIT_DIR_PATH" --work-tree="$WORKTREE" checkout -f "$BRANCH" + +cd "$WORKTREE" + +echo "→ AbhĂ€ngigkeiten installieren 
" +npm ci + +echo "→ Seite bauen 
" +npm run build + +echo "✅ Deploy fertig: $(date)" diff --git a/package.json b/package.json new file mode 100644 index 0000000..5bcbab2 --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "name": "radweltreise-blog", + "type": "module", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "astro dev", + "build": "astro build", + "preview": "astro preview" + }, + "dependencies": { + "astro": "^5.0.0", + "leaflet": "^1.9.4", + "@tmcw/togeojson": "^5.8.1", + "@xmldom/xmldom": "^0.9.6", + "exifr": "^7.1.3" + } +} diff --git a/public/admin/config.yml b/public/admin/config.yml new file mode 100644 index 0000000..ce4a55d --- /dev/null +++ b/public/admin/config.yml @@ -0,0 +1,45 @@ +# Decap CMS – Konfiguration +# --------------------------------------------------------------- +# Vor dem ersten Login zwei Platzhalter ersetzen: +# 1. repo: /sattelfest (Besitzer/Repo in Forgejo) +# 2. app_id: (aus der OAuth-App in Forgejo) +# base_url ggf. an deine Forgejo-Adresse anpassen. +# --------------------------------------------------------------- + +backend: + name: gitea + repo: FORGEJO_USER/sattelfest + base_url: https://git.sattelfest.org + branch: main + app_id: OAUTH_CLIENT_ID + +# Wohin Fotos aus dem Editor gespeichert werden. +# Genau dieser Ordner wird beim Bauen nach GPS (EXIF) durchsucht -> +# jedes hochgeladene Foto wird automatisch zu einem Marker auf der Karte. +media_folder: public/photos +public_folder: /photos + +# Sprache der OberflĂ€che +locale: de + +collections: + - name: posts + label: Tagebuch + label_singular: Eintrag + folder: src/pages/posts + create: true + slug: "{{year}}-{{month}}-{{day}}-{{slug}}" + extension: md + format: frontmatter + fields: + - { label: Layout, name: layout, widget: hidden, default: "../../layouts/Base.astro" } + - { label: Titel, name: title, widget: string } + - { + label: Datum, + name: date, + widget: datetime, + format: "YYYY-MM-DD", + date_format: "YYYY-MM-DD", + time_format: false, + } + - { label: Text, name: body, widget: markdown } diff --git a/public/admin/index.html b/public/admin/index.html new file mode 100644 index 0000000..2968743 --- /dev/null +++ b/public/admin/index.html @@ -0,0 +1,17 @@ + + + + + + Inhalte verwalten · Sattelfest + + + + + + diff --git a/public/photos/.gitkeep b/public/photos/.gitkeep new file mode 100644 index 0000000..ed5aeee --- /dev/null +++ b/public/photos/.gitkeep @@ -0,0 +1,2 @@ +# Lege hier deine Original-Fotos ab (mit GPS im EXIF). +# Aus jedem Foto wird automatisch ein Marker auf der Karte. diff --git a/src/components/RouteMap.astro b/src/components/RouteMap.astro new file mode 100644 index 0000000..1fb211b --- /dev/null +++ b/src/components/RouteMap.astro @@ -0,0 +1,114 @@ +--- +import { loadRoute } from '../lib/gpx.js'; +import { loadPhotoMarkers } from '../lib/photos.js'; + +// Beides lĂ€uft beim BAUEN (nicht im Browser): GPX -> Linie, Fotos -> Marker. +const route = await loadRoute(); +const markers = await loadPhotoMarkers(); +const data = JSON.stringify({ route, markers }); +--- + + + + + + + + diff --git a/src/data/tracks/2026-06-01-pyrenaeen.gpx b/src/data/tracks/2026-06-01-pyrenaeen.gpx new file mode 100644 index 0000000..ed916a3 --- /dev/null +++ b/src/data/tracks/2026-06-01-pyrenaeen.gpx @@ -0,0 +1,14 @@ + + + + Beispiel-Etappe PyrenĂ€en + + 1500 + 1620 + 1810 + 2050 + 1740 + 1480 + + + diff --git a/src/layouts/Base.astro b/src/layouts/Base.astro new file mode 100644 index 0000000..9147e6b --- /dev/null +++ b/src/layouts/Base.astro @@ -0,0 +1,54 @@ +--- +const { title, frontmatter } = Astro.props; +const pageTitle = frontmatter?.title ?? title ?? 'Radweltreise'; +--- + + + + + + + + {pageTitle} + + +
+

đŸšČ Radweltreise

+
+
+ +
+ + + + diff --git a/src/lib/gpx.js b/src/lib/gpx.js new file mode 100644 index 0000000..60fbcfd --- /dev/null +++ b/src/lib/gpx.js @@ -0,0 +1,35 @@ +import { readdir, readFile } from 'node:fs/promises'; +import path from 'node:path'; +import { DOMParser } from '@xmldom/xmldom'; +import { gpx } from '@tmcw/togeojson'; + +// Hier landen deine Komoot-/App-Exporte. Dateinamen am besten mit Datum +// beginnen lassen (z. B. 2026-06-01-pyrenaeen.gpx), dann stimmt die Reihenfolge. +const TRACKS_DIR = path.join(process.cwd(), 'src/data/tracks'); + +// Liest alle .gpx-Dateien und fĂŒgt ihre Streckenlinien zu einer +// GeoJSON-FeatureCollection zusammen (= deine Gesamtroute). +export async function loadRoute() { + let files = []; + try { + files = (await readdir(TRACKS_DIR)).filter((f) => f.toLowerCase().endsWith('.gpx')); + } catch { + return { type: 'FeatureCollection', features: [] }; + } + files.sort(); + + const features = []; + for (const file of files) { + const xml = await readFile(path.join(TRACKS_DIR, file), 'utf-8'); + const doc = new DOMParser().parseFromString(xml, 'text/xml'); + const geo = gpx(doc); + for (const feature of geo.features) { + const type = feature.geometry?.type; + if (type === 'LineString' || type === 'MultiLineString') { + feature.properties = { ...feature.properties, source: file }; + features.push(feature); + } + } + } + return { type: 'FeatureCollection', features }; +} diff --git a/src/lib/photos.js b/src/lib/photos.js new file mode 100644 index 0000000..6b662c3 --- /dev/null +++ b/src/lib/photos.js @@ -0,0 +1,43 @@ +import { readdir, readFile } from 'node:fs/promises'; +import path from 'node:path'; +import exifr from 'exifr'; + +// Fotos hier ablegen; sie werden 1:1 ausgeliefert (URL /photos/). +// Wichtig: Original-Dateien verwenden – manche Cloud-/Chat-Dienste löschen die GPS-Daten. +const PHOTOS_DIR = path.join(process.cwd(), 'public/photos'); + +// Liest aus jedem Foto GPS + Aufnahmezeitpunkt und liefert Karten-Marker. +export async function loadPhotoMarkers() { + let files = []; + try { + files = (await readdir(PHOTOS_DIR)).filter((f) => /\.(jpe?g|tiff?|heic|heif)$/i.test(f)); + } catch { + return []; + } + + const markers = []; + for (const file of files) { + const buffer = await readFile(path.join(PHOTOS_DIR, file)); + + let gps = null; + try { + gps = await exifr.gps(buffer); + } catch { + gps = null; + } + if (!gps || gps.latitude == null || gps.longitude == null) continue; + + let date = null; + try { + const meta = await exifr.parse(buffer, ['DateTimeOriginal']); + if (meta?.DateTimeOriginal) date = new Date(meta.DateTimeOriginal).toISOString(); + } catch { + date = null; + } + + markers.push({ file, url: `/photos/${file}`, lat: gps.latitude, lng: gps.longitude, date }); + } + + markers.sort((a, b) => (a.date ?? '').localeCompare(b.date ?? '')); + return markers; +} diff --git a/src/pages/index.astro b/src/pages/index.astro new file mode 100644 index 0000000..05c2b68 --- /dev/null +++ b/src/pages/index.astro @@ -0,0 +1,26 @@ +--- +import Base from '../layouts/Base.astro'; +import RouteMap from '../components/RouteMap.astro'; + +// Alle Tagebuch-EintrĂ€ge einsammeln (Markdown-Dateien im Ordner posts/). +const modules = import.meta.glob('./posts/*.md', { eager: true }); +const posts = Object.values(modules) + .map((m) => ({ url: m.url, ...m.frontmatter })) + .sort((a, b) => new Date(b.date) - new Date(a.date)); +--- + + + + +

Tagebuch

+ {posts.length === 0 &&

Noch keine EintrĂ€ge – der erste kommt bald!

} + + diff --git a/src/pages/posts/2026-06-01-pyrenaeen.md b/src/pages/posts/2026-06-01-pyrenaeen.md new file mode 100644 index 0000000..ca558a3 --- /dev/null +++ b/src/pages/posts/2026-06-01-pyrenaeen.md @@ -0,0 +1,15 @@ +--- +layout: ../../layouts/Base.astro +title: Über die PyrenĂ€en +date: 2026-06-01 +--- + +# Über die PyrenĂ€en + +Heute 1400 Höhenmeter, die Beine sind tot, aber die Aussicht war unbezahlbar. +Oben am Pass gab es KĂ€se aus dem Rucksack und einen Espresso, der nach Sieg schmeckte. + + + + +[← zurĂŒck zur Übersicht](/)