Interaktive Open-Data-Kartenanwendung für Badestellen und wassernahe POIs in Schleswig-Holstein.
Open Bath Map besteht aus einem Nuxt-3-Frontend und einem FastAPI-Backend. Das Backend lädt offene Badegewässerdaten des Landes Schleswig-Holstein, ergänzt sie um wassernahe touristische POIs, normalisiert beide Quellen und stellt sie über eine gemeinsame Karten-API bereit. Das Frontend rendert darauf eine Leaflet-Karte mit SSR-fähigen Detailseiten, Suchfunktion, Filtern, JSON+LD und PWA-Bausteinen.
Der aktuelle Fokus des Repos liegt auf einer kartenzentrierten API. Die frühere fachliche /api/bathing-sites-Schiene ist entfernt. Das Frontend nutzt ausschließlich /api/map/v1/* sowie /api/health.
- Interaktive Karte für Badestellen und zusätzliche wassernahe POIs
- Bounds-basierte Marker-Abfrage für den sichtbaren Kartenausschnitt
- Radius-Suche auf Basis des Browser-Standorts
- Volltextsuche mit Typ-, Kategorie- und Infrastrukturfiltern
- SEO-fähige Detailseiten unter sprechenden Slugs
- JSON+LD und Open-Graph-Metadaten pro Detailseite
- Bild-Fallbacks für nicht erreichbare Bilder
- Desktop-Sidebar und mobiles Bottom Sheet mit tab-basierter Navigation
- PWA-Grundausstattung mit Manifest und Service Worker
- Optionale PostgreSQL-/PostGIS-Persistenz mit Volltext- und Trigramm-Suche
Das Frontend liegt unter frontend und ist ein Nuxt-3-Projekt mit Vue 3, TypeScript, Tailwind CSS, Leaflet und leaflet.markercluster.
Wichtige Bausteine:
frontend/pages/[[slug]].vue: SSR-Einstiegspunkt für Karten- und Detailseiten, inklusive SEO-Metadaten und JSON+LDfrontend/components/map/MapExperience.vue: zentrale Orchestrierung von Karte, Sidebar, Bottom Sheet, Suche und Auswahlzustandfrontend/components/map/MapView.vue: Leaflet-Karte mit Marker-Rendering und Interaktionenfrontend/components/map/MapSidebar.vue: Desktop-Navigation für Info, Suche/Filter und Markerdetailsfrontend/components/map/MapBottomSheet.vue: mobile Entsprechung zur Sidebarfrontend/composables/useMapData.ts: API-Zugriffe auf Bounds, Radius, Detaildaten und Suchefrontend/composables/useMapSelection.ts: Synchronisierung von Marker-Auswahl und slug-basierter Routefrontend/composables/useMapState.ts: globaler Karten- und UI-Zustandfrontend/composables/useJsonLd.ts: Einbindung strukturierter Datenfrontend/composables/useImageFallback.ts: Fallback bei defekten Bild-URLs
Das Backend liegt unter backend und ist ein FastAPI-Dienst mit SQLModel/SQLAlchemy, optionaler PostgreSQL-Persistenz und PostGIS-Unterstützung.
Wichtige Bausteine:
backend/app/main.py: FastAPI-App und Router-Registrierungbackend/app/api/routes/map.py: Karten-Endpunktebackend/app/api/routes/health.py: Health-Endpunktbackend/app/services/opendata/service.py: Discovery, Download, Normalisierung und Mapping der Open-Data-Quellenbackend/app/services/postgres_store.py: Persistenz, Suche und Kartenabfragen auf PostgreSQL/PostGISbackend/app/db/models.py: SQLModel-Tabellen für Datensatzstatus, Badestellen und Kartenobjektebackend/app/db/session.py: Engine-, Session- und Support-Objekt-Verwaltung für PostgreSQLbackend/app/services/opendata/source_queries.toml: konfigurierbare CKAN-/Fallback-Quellen
Die Badestellen werden aus mehreren CSV-Quellen des Landes Schleswig-Holstein zusammengesetzt:
- Stammdaten
- Einstufung
- Infrastruktur
- Saisondauer
- Messungen
Zusätzlich werden wassernahe touristische POIs aus der touristischen Landesdatenbank eingebunden.
Die Quellkonfiguration liegt in backend/app/services/opendata/source_queries.toml. Die CKAN-Basis-URL wird ebenfalls daraus gelesen. Falls die CKAN-Discovery keine passende Ressource liefert, verwendet der Service direkte EFI-Fallback-URLs.
- Das Frontend lädt Marker über Bounds-, Radius- oder Suchanfragen.
- Das Backend liest Daten entweder direkt aus dem Cache/den Open-Data-Quellen oder aus PostgreSQL, wenn
DATABASE_URLgesetzt ist. - Die Open-Data-Schicht normalisiert Badegewässerdaten und leitet daraus
MapItem-Objekte ab. - Wassernahe POIs werden ergänzt und mit den Badestellen in einer gemeinsamen Kartenrepräsentation zusammengeführt.
- Detailseiten werden unter
/<slug>serverseitig vorbereitet, damit SEO-Metadaten und JSON+LD bereits im ersten HTML enthalten sind.
Liefert Status, Cache-Alter, Sync-Zeitpunkt, Quell-URLs und die Anzahl der aktuell verfügbaren Datensätze.
Liefert Marker für einen Kartenausschnitt.
Query-Parameter:
xminyminxmaxymaxtype:badestelleoderpoicategoryinfrastructure
Liefert Marker im Umkreis eines Punktes.
Query-Parameter:
latlngradius_kmtypecategoryinfrastructure
Liefert ein einzelnes Kartenobjekt.
Query-Parameter:
idoderslug
Volltextsuche über Kartenobjekte mit optionalen Filtern.
Query-Parameter:
qtypecategoryinfrastructurelimit
.
├── backend
│ ├── alembic
│ │ └── versions
│ ├── app
│ │ ├── api/routes
│ │ ├── db
│ │ ├── models
│ │ └── services
│ │ └── opendata
│ ├── pyproject.toml
│ └── requirements.txt
├── frontend
│ ├── assets/css
│ ├── components
│ │ ├── map
│ │ └── site
│ ├── composables
│ ├── pages
│ ├── public
│ ├── types
│ ├── utils
│ ├── app.vue
│ ├── nuxt.config.ts
│ └── package.json
├── scripts
│ ├── generate_sitemap.py
│ └── sync_postgres.py
├── .env.example
├── package.json
└── pnpm-workspace.yaml
- Node.js 20+
pnpm- Python 3.12+
- Optional für Persistenz und Suche: PostgreSQL mit PostGIS
cp .env.example .envpnpm --dir frontend installcd backend
python3.12 -m venv .venv
source .venv/bin/activate
pip install -e .cd backend
source .venv/bin/activate
uvicorn app.main:app --reload --host 127.0.0.1 --port 8000pnpm dev:frontendLokale Standard-URLs:
- Frontend:
http://127.0.0.1:3000 - Backend:
http://127.0.0.1:8000
Wenn DATABASE_URL nicht gesetzt ist, lädt das Backend Daten direkt aus den Open-Data-Quellen und verwendet den lokalen JSON-Cache unter backend/cache.
Das ist für Entwicklung und schnelles Testen ausreichend.
Wenn DATABASE_URL gesetzt ist, verwendet die API den PostgreSQL-Store. Dabei werden PostGIS-, unaccent- und pg_trgm-Supportobjekte automatisch angelegt. Suche und Kartenabfragen laufen dann gegen die Datenbank.
Den Datenbestand kannst du mit folgendem Script synchronisieren:
pnpm sync:postgresDas Script lädt die Open-Data-Quellen, normalisiert sie und schreibt Badestellen sowie Kartenobjekte nach PostgreSQL.
Das Repo enthält Alembic-Migrationen unter backend/alembic/versions.
Migrationen anwenden:
cd backend
source .venv/bin/activate
alembic upgrade headWichtig: Für Migrations- und Sync-Betrieb muss DATABASE_URL gesetzt sein.
Frontend starten:
pnpm dev:frontendFrontend-Produktionsbuild:
pnpm build:frontendFrontend-Vorschau:
pnpm preview:frontendNuxt-Typecheck:
pnpm --dir frontend typecheckPostgreSQL-Sync:
pnpm sync:postgresSitemap generieren:
pnpm generate:sitemapDie Sitemap wird nach frontend/public/sitemap.xml geschrieben.
| Variable | Bedeutung |
|---|---|
NUXT_PUBLIC_API_BASE |
Basis-URL des FastAPI-Backends |
NUXT_PUBLIC_SITE_URL |
Öffentliche Basis-URL des Frontends für Canonicals, Sitemap und SEO |
NUXT_PUBLIC_CONTACT_MAIL |
Kontakt-E-Mail für Impressum und Datenschutz |
NUXT_PUBLIC_CONTACT_PHONE |
Telefonnummer für das Impressum |
NUXT_PUBLIC_PRIVACY_CONTACT_PERSON |
Verantwortliche Person für Datenschutzangaben |
NUXT_PUBLIC_ADDRESS_NAME |
Name der Organisation |
NUXT_PUBLIC_ADDRESS_STREET |
Straße |
NUXT_PUBLIC_ADDRESS_HOUSE_NUMBER |
Hausnummer |
NUXT_PUBLIC_ADDRESS_POSTAL_CODE |
Postleitzahl |
NUXT_PUBLIC_ADDRESS_CITY |
Ort |
| Variable | Bedeutung |
|---|---|
BACKEND_HOST |
Host für den FastAPI-Server |
BACKEND_PORT |
Port für den FastAPI-Server |
BACKEND_CORS_ORIGINS |
Kommaseparierte Liste erlaubter Origins |
CACHE_TTL_MINUTES |
Gültigkeit des Datei-Caches in Minuten |
REQUEST_TIMEOUT_SECONDS |
Timeout für Requests auf externe Datenquellen |
DATABASE_URL |
Optionale PostgreSQL-Verbindung; wenn gesetzt, liest die API primär aus der Datenbank |
- Detailseiten werden serverseitig über
frontend/pages/[[slug]].vuevorbereitet. useSeoMetasetzt Titel, Beschreibung und Open-Graph-Bilder abhängig vom gewählten Kartenobjekt.- JSON+LD wird pro Route erzeugt.
- Die Sitemap wird per
scripts/generate_sitemap.pygeneriert. - Das Frontend ist als Nuxt-Anwendung mit PWA-Manifest und Service Worker konfiguriert.
Die Anwendung enthält zusätzlich indexierbare SEO-Landingpages:
/regionen/regionen/[slug]/sammlungen/sammlungen/[slug]
Wichtige Dateien:
frontend/content/landingPages.ts: Content-/SEO-Konfiguration (Slug, Titel, Intro, Meta, Filter, Related Links)frontend/utils/landingSelectors.ts: zentrale Auswahl- und Ableitungslogik für Badestellenfrontend/utils/landingSeo.ts: Meta-/JSON+LD-Helfer für Landingpagesfrontend/utils/landingValidation.ts: strukturelle Guards (Slug-Format, Eindeutigkeit, Related-Link-Referenzen)
- In
regionLandingPageseinen neuen Eintrag mitslug,h1, Meta-Texten undfilteranlegen. - Optional
relatedRegionsundrelatedCollectionssetzen. - Falls die URL in die Sitemap soll, den Slug in
scripts/generate_sitemap.pyunterREGION_SLUGSergänzen.
- In
collectionLandingPageseinen neuen Eintrag mitslug,h1, Meta-Texten undfilteranlegen. - Auswahlkriterien in der
filter-Definition dokumentieren (selectionLogicText). - Optional
relatedRegionsundrelatedCollectionssetzen. - Falls die URL in die Sitemap soll, den Slug in
COLLECTION_SLUGSergänzen.
- Die Landingpages arbeiten datenbasiert auf
MapItem-Feldern (city,district,tags,category,amenities,accessibility, freie Textfelder). - Die komplette Matching-Logik ist zentral in
landingSelectors.tsgekapselt. - Komponenten enthalten keine fachliche Filterlogik, sondern rendern nur die übergebenen Ergebnisse.
- Die fachliche Quellkonfiguration liegt in
backend/app/services/opendata/source_queries.toml, nicht mehr hartkodiert in Python. - Bilder für Badestellen werden aus Kennzeichen/Datensatzlogik serverseitig erzeugt; im Frontend existiert zusätzlich ein Dummy-Fallback.
- Das Repo enthält generierte Artefakte wie
frontend/.nuxt,frontend/.outputund lokale Virtualenv-/Node-Module-Verzeichnisse. Diese sind keine sinnvolle Quelle für Architekturentscheidungen; maßgeblich sind die Dateien unterfrontend/,backend/undscripts/.
