diff --git a/CHANGELOG.md b/CHANGELOG.md index 443dcc3..a249395 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Frontend uses `@cryptgeon/shared` for crypto + payload codec (replacing the previous local `cryptgeon/shared`); heavy pack/unpack runs in a web worker. - CLI rebuilt with `vite-plus` (bundles all deps) and imports from `@cryptgeon/shared`. +### Fixed + +- SPA fallback now serves the app (200) for client-side routes such as `/about` and `/note/`, while unmatched `/api/*` paths still return 404. + ### Breaking changes - Endpoints moved to `/api/v3/notes/` and `/api/v3/status`; health check to `/healthz`. diff --git a/packages/backend/src/main.rs b/packages/backend/src/main.rs index ff27313..bbab6ec 100644 --- a/packages/backend/src/main.rs +++ b/packages/backend/src/main.rs @@ -20,6 +20,10 @@ mod note; mod status; mod store; +async fn api_not_found() -> axum::http::StatusCode { + axum::http::StatusCode::NOT_FOUND +} + #[tokio::main] async fn main() { dotenv().ok(); @@ -39,11 +43,13 @@ async fn main() { .nest("/notes", notes_routes) .merge(status_routes); - let api_routes = Router::new().nest("/v3", v3_routes); + let api_routes = Router::new() + .nest("/v3", v3_routes) + .fallback(api_not_found); let index = format!("{}{}", config::FRONTEND_PATH.to_string(), "/index.html"); let serve_dir = - ServeDir::new(config::FRONTEND_PATH.to_string()).not_found_service(ServeFile::new(index)); + ServeDir::new(config::FRONTEND_PATH.to_string()).fallback(ServeFile::new(index)); let app = Router::new() .nest("/api", api_routes) .merge(health_routes) @@ -66,4 +72,4 @@ async fn main() { axum::serve(listener, ServiceExt::::into_make_service(app)) .await .unwrap(); -} \ No newline at end of file +} diff --git a/test/web/spa-fallback.spec.ts b/test/web/spa-fallback.spec.ts new file mode 100644 index 0000000..28a2c51 --- /dev/null +++ b/test/web/spa-fallback.spec.ts @@ -0,0 +1,15 @@ +import { expect, test } from "@playwright/test"; + +test.describe("@web", () => { + for (const path of ["/", "/about", "/note/does-not-exist"]) { + test(`serves ${path} with status 200`, async ({ request }) => { + const response = await request.get(path); + expect(response.status()).toBe(200); + }); + } + + test("api still reports a missing note as 404", async ({ request }) => { + const response = await request.get("/api/notes/does-not-exist"); + expect(response.status()).toBe(404); + }); +});