@@ -49,6 +49,39 @@ public static WebApplication ConfigureTaskdeckPipeline(
4949 app . UseMiddleware < UnhandledExceptionMiddleware > ( ) ;
5050 app . UseMiddleware < SecurityHeadersMiddleware > ( ) ;
5151
52+ // SPA static file serving: serve Vue build output from wwwroot/.
53+ // Placed after security-headers middleware so that the OnStarting callback registered by
54+ // SecurityHeadersMiddleware is already in scope when UseStaticFiles short-circuits the pipeline,
55+ // ensuring CSP, X-Frame-Options, and other headers are applied to SPA assets including index.html.
56+ // UseDefaultFiles must precede UseStaticFiles so that requests to "/" map to index.html.
57+ // Directory listing is disabled by default (no DirectoryBrowser registered).
58+ //
59+ // Cache-control strategy (PKG-01 AC: "SPA assets served with appropriate cache headers"):
60+ // - Versioned/hashed assets (Vite output under /assets/): max-age=1 year + immutable.
61+ // Safe because Vite appends a content hash to each filename — stale content is impossible.
62+ // - All other files (including index.html): no-cache so the browser always revalidates,
63+ // ensuring users pick up new deployments immediately.
64+ app . UseDefaultFiles ( ) ;
65+ app . UseStaticFiles ( new StaticFileOptions
66+ {
67+ OnPrepareResponse = ctx =>
68+ {
69+ var path = ctx . Context . Request . Path . Value ?? string . Empty ;
70+ var headers = ctx . Context . Response . Headers ;
71+
72+ if ( path . StartsWith ( "/assets/" , StringComparison . OrdinalIgnoreCase ) )
73+ {
74+ // Vite hashes these filenames — safe to cache indefinitely.
75+ headers [ "Cache-Control" ] = "public, max-age=31536000, immutable" ;
76+ }
77+ else
78+ {
79+ // index.html and other non-versioned files: revalidate on every request.
80+ headers [ "Cache-Control" ] = "no-cache" ;
81+ }
82+ }
83+ } ) ;
84+
5285 app . UseAuthentication ( ) ;
5386 if ( rateLimitingSettings . Enabled )
5487 {
@@ -59,6 +92,11 @@ public static WebApplication ConfigureTaskdeckPipeline(
5992 app . MapControllers ( ) ;
6093 app . MapHub < BoardsHub > ( "/hubs/boards" ) ;
6194
95+ // SPA fallback: any route not matched by a controller or hub endpoint returns index.html,
96+ // enabling Vue Router's client-side navigation. API (/api/*) and hub (/hubs/*) routes
97+ // are matched above and never reach this fallback.
98+ app . MapFallbackToFile ( "index.html" ) ;
99+
62100 return app ;
63101 }
64102
0 commit comments