#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) <noreply@anthropic.com>
This commit is contained in:
Dotty Dotter 2026-04-28 02:20:26 +02:00
parent e1f6f18524
commit 6f53f35c09
2 changed files with 45 additions and 5 deletions

View File

@ -703,6 +703,25 @@ def startup():
if os.path.isdir(AUDIO_DIR): if os.path.isdir(AUDIO_DIR):
app.mount("/audio", StaticFiles(directory=AUDIO_DIR), name="audio") app.mount("/audio", StaticFiles(directory=AUDIO_DIR), name="audio")
# Serve webapp as static files (fallback)
if os.path.isdir(STATIC_DIR): # SPA-Routing: erst statische Files versuchen, sonst index.html zurueckliefern.
app.mount("/", StaticFiles(directory=STATIC_DIR, html=True), name="static") # 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)

View File

@ -1685,7 +1685,13 @@ async function loadApp() {
const resp = await fetch(`${API_BASE}/api/podcasts`); const resp = await fetch(`${API_BASE}/api/podcasts`);
if (resp.ok) { if (resp.ok) {
const podcasts = await resp.json(); const podcasts = await resp.json();
if (podcasts.length === 1) { // URL-Routing: /<podcast-id> 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 // Single podcast → load directly
await selectPodcast(podcasts[0].id); await selectPodcast(podcasts[0].id);
} else if (podcasts.length > 1) { } else if (podcasts.length > 1) {
@ -1694,6 +1700,15 @@ async function loadApp() {
} else { } else {
throw new Error('No podcasts found'); 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; return;
} }
} catch (e) { } catch (e) {
@ -1712,11 +1727,14 @@ async function loadApp() {
} }
} }
async function selectPodcast(podcastId) { async function selectPodcast(podcastId, fromUrl = false) {
try { try {
const resp = await fetch(`${API_BASE}/api/podcasts/${podcastId}`); const resp = await fetch(`${API_BASE}/api/podcasts/${podcastId}`);
DATA = await resp.json(); DATA = await resp.json();
CURRENT_PODCAST = podcastId; 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) // Reset mindmap area (might have been used for selector)
const mindmap = document.getElementById('mindmap'); const mindmap = document.getElementById('mindmap');
mindmap.style.overflow = 'hidden'; mindmap.style.overflow = 'hidden';
@ -1914,6 +1932,9 @@ function showPodcastList() {
CURRENT_PODCAST = null; CURRENT_PODCAST = null;
document.getElementById('svg').innerHTML = ''; document.getElementById('svg').innerHTML = '';
document.getElementById('staffel-filters').innerHTML = ''; document.getElementById('staffel-filters').innerHTML = '';
if (window.history && window.location.pathname !== '/') {
window.history.pushState({}, '', '/');
}
showPodcastSelector(ALL_PODCASTS); showPodcastSelector(ALL_PODCASTS);
} }