fix(spa): serve index.html for SvelteKit client-side routes
Release / release (push) Successful in 1m7s

Deep-linking to non-root URLs like /settings or /notification-trackers
returned {"detail":"Not Found"} because StaticFiles(html=True) only
serves index.html for directory roots, not for arbitrary SPA routes.

Subclass StaticFiles with an SPA fallback: any 404 on a non-/api path
serves the root index.html, letting the SvelteKit router hydrate the
correct view on the client. Real /api/* 404s still bubble up as JSON
from FastAPI.
This commit is contained in:
2026-04-21 20:57:39 +03:00
parent 2eccbc7279
commit 28465f56f9
@@ -158,7 +158,27 @@ async def health():
from pathlib import Path
if _cfg.static_dir and Path(_cfg.static_dir).is_dir():
from fastapi.staticfiles import StaticFiles
app.mount("/", StaticFiles(directory=_cfg.static_dir, html=True), name="frontend")
from starlette.responses import FileResponse
from starlette.exceptions import HTTPException as StarletteHTTPException
_static_dir = Path(_cfg.static_dir)
class SPAStaticFiles(StaticFiles):
"""StaticFiles that falls back to index.html for SvelteKit client-side routes.
Unknown paths return index.html so that deep links like /settings
hydrate the SPA, while /api/* and real asset 404s behave normally.
"""
async def get_response(self, path: str, scope):
try:
return await super().get_response(path, scope)
except StarletteHTTPException as exc:
if exc.status_code == 404 and not path.startswith("api/"):
return FileResponse(_static_dir / "index.html")
raise
app.mount("/", SPAStaticFiles(directory=_cfg.static_dir, html=True), name="frontend")
def run():