fix(spa): serve index.html for SvelteKit client-side routes
Release / release (push) Successful in 1m7s
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:
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user