From 6f53f35c0901d79f0704d1da423a729a204ba15d Mon Sep 17 00:00:00 2001 From: Dotty Dotter Date: Tue, 28 Apr 2026 02:20:26 +0200 Subject: [PATCH] #10 URL-Routing fuer Podcast-Tiefenlinks Backend: - SPA-Fallback: catch-all-Route liefert index.html, falls keine statische Datei matcht (mit Ausnahme von /api/* und /audio/*). Dadurch funktionieren Tiefen- Links wie /ldn oder /neu-denken direkt. Frontend: - loadApp() liest pathname und laedt den passenden Podcast direkt, falls die ID in /api/podcasts vorkommt; sonst klassischer Selector. - selectPodcast() updated den Pfad per history.pushState, damit Bookmarks und Sharing funktionieren. - popstate-Handler reagiert auf Browser-Back/Forward. - showPodcastList() setzt den Pfad auf '/'. Co-Authored-By: Claude Opus 4.7 (1M context) --- backend/app.py | 25 ++++++++++++++++++++++--- webapp/index.html | 25 +++++++++++++++++++++++-- 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/backend/app.py b/backend/app.py index 5a322a9..448f2da 100644 --- a/backend/app.py +++ b/backend/app.py @@ -703,6 +703,25 @@ def startup(): if os.path.isdir(AUDIO_DIR): app.mount("/audio", StaticFiles(directory=AUDIO_DIR), name="audio") -# Serve webapp as static files (fallback) -if os.path.isdir(STATIC_DIR): - app.mount("/", StaticFiles(directory=STATIC_DIR, html=True), name="static") + +# SPA-Routing: erst statische Files versuchen, sonst index.html zurueckliefern. +# Damit funktionieren Tiefen-Links wie /ldn oder /neu-denken (Issue #10). +@app.get("/") +def spa_root(): + index = Path(STATIC_DIR) / "index.html" + if index.is_file(): + return FileResponse(str(index)) + raise HTTPException(404) + + +@app.get("/{path:path}") +def spa_fallback(path: str): + if path.startswith("api/") or path.startswith("audio/"): + raise HTTPException(404) + static_path = Path(STATIC_DIR) / path + if static_path.is_file(): + return FileResponse(str(static_path)) + index = Path(STATIC_DIR) / "index.html" + if index.is_file(): + return FileResponse(str(index)) + raise HTTPException(404) diff --git a/webapp/index.html b/webapp/index.html index c0c46e0..c9d428c 100644 --- a/webapp/index.html +++ b/webapp/index.html @@ -1685,7 +1685,13 @@ async function loadApp() { const resp = await fetch(`${API_BASE}/api/podcasts`); if (resp.ok) { const podcasts = await resp.json(); - if (podcasts.length === 1) { + // URL-Routing: / oeffnet direkt diesen Podcast + const pathPodcast = window.location.pathname.replace(/^\//, '').replace(/\/$/, '').split('/')[0]; + const fromUrl = pathPodcast && podcasts.find(p => p.id === pathPodcast); + if (fromUrl) { + ALL_PODCASTS = podcasts; + await selectPodcast(fromUrl.id, /*fromUrl*/ true); + } else if (podcasts.length === 1) { // Single podcast → load directly await selectPodcast(podcasts[0].id); } else if (podcasts.length > 1) { @@ -1694,6 +1700,15 @@ async function loadApp() { } else { throw new Error('No podcasts found'); } + // Browser-Back/Forward + window.addEventListener('popstate', async () => { + const p = window.location.pathname.replace(/^\//, '').replace(/\/$/, '').split('/')[0]; + if (!p) { + showPodcastSelector(podcasts); + } else if (podcasts.find(x => x.id === p) && p !== CURRENT_PODCAST) { + await selectPodcast(p, true); + } + }); return; } } catch (e) { @@ -1712,11 +1727,14 @@ async function loadApp() { } } -async function selectPodcast(podcastId) { +async function selectPodcast(podcastId, fromUrl = false) { try { const resp = await fetch(`${API_BASE}/api/podcasts/${podcastId}`); DATA = await resp.json(); CURRENT_PODCAST = podcastId; + if (!fromUrl && window.history && window.location.pathname !== `/${podcastId}`) { + window.history.pushState({ podcast: podcastId }, '', `/${podcastId}`); + } // Reset mindmap area (might have been used for selector) const mindmap = document.getElementById('mindmap'); mindmap.style.overflow = 'hidden'; @@ -1914,6 +1932,9 @@ function showPodcastList() { CURRENT_PODCAST = null; document.getElementById('svg').innerHTML = ''; document.getElementById('staffel-filters').innerHTML = ''; + if (window.history && window.location.pathname !== '/') { + window.history.pushState({}, '', '/'); + } showPodcastSelector(ALL_PODCASTS); }