Skip to content

Commit a35a452

Browse files
authored
Merge pull request #556 from Chris0Jeky/feat/pkg-01-spa-static-serving
feat: serve Vue SPA as static files from .NET API (PKG-01 #533)
2 parents f38ba4b + 5200b6b commit a35a452

File tree

1 file changed

+38
-0
lines changed

1 file changed

+38
-0
lines changed

backend/src/Taskdeck.Api/Extensions/PipelineConfiguration.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)