#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:
parent
e1f6f18524
commit
6f53f35c09
@ -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)
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user