perf(seo): response compression, security headers, immutable upload cache
CI/CD / CI · dotnet build (push) Successful in 3m13s
CI/CD / Deploy · drsousan (push) Successful in 34s

Now that the origin serves directly (no CDN compressing for us):
- Add gzip + brotli response compression — homepage HTML 84KB -> ~25KB
  (~71% smaller), big Core Web Vitals / crawl-budget win
- Baseline security headers on every response: X-Content-Type-Options,
  X-Frame-Options, Referrer-Policy (no HSTS yet — cert just stabilised)
- Long-cache immutable GUID uploads (Cache-Control max-age=30d,immutable)

Verified locally: gzip+br negotiated, headers present, uploads cached.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-26 00:46:08 +03:30
parent 00a138fe46
commit f3701c5893
+38 -1
View File
@@ -60,6 +60,21 @@ builder.Services.AddSingleton(HtmlEncoder.Create(UnicodeRanges.All));
builder.Services.ConfigureHttpJsonOptions(opts =>
opts.SerializerOptions.ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.IgnoreCycles);
// Response compression (gzip + brotli) — origin nginx no longer sits behind a
// compressing CDN, so HTML/CSS/JS were served uncompressed (~80KB). Cuts payload ~75%.
builder.Services.AddResponseCompression(opts =>
{
opts.EnableForHttps = true;
opts.Providers.Add<Microsoft.AspNetCore.ResponseCompression.BrotliCompressionProvider>();
opts.Providers.Add<Microsoft.AspNetCore.ResponseCompression.GzipCompressionProvider>();
opts.MimeTypes = Microsoft.AspNetCore.ResponseCompression.ResponseCompressionDefaults.MimeTypes
.Concat(new[] { "application/ld+json", "image/svg+xml" });
});
builder.Services.Configure<Microsoft.AspNetCore.ResponseCompression.BrotliCompressionProviderOptions>(
o => o.Level = System.IO.Compression.CompressionLevel.Fastest);
builder.Services.Configure<Microsoft.AspNetCore.ResponseCompression.GzipCompressionProviderOptions>(
o => o.Level = System.IO.Compression.CompressionLevel.Fastest);
// ── Build ─────────────────────────────────────────────────────────────────────
var app = builder.Build();
@@ -68,9 +83,31 @@ var app = builder.Build();
if (!app.Environment.IsDevelopment())
app.UseExceptionHandler("/error");
app.UseResponseCompression();
// Baseline security headers on every response (safe defaults; no HSTS yet — the
// cert was just stabilised, so we avoid forcing HTTPS pinning until it's proven).
app.Use(async (ctx, next) =>
{
var h = ctx.Response.Headers;
h["X-Content-Type-Options"] = "nosniff";
h["X-Frame-Options"] = "SAMEORIGIN";
h["Referrer-Policy"] = "strict-origin-when-cross-origin";
await next();
});
app.UseCors();
app.UseDefaultFiles(); // serves /admin/index.html for /admin/ (wwwroot/index.html deleted → no conflict with Razor Pages)
app.UseStaticFiles();
app.UseStaticFiles(new StaticFileOptions
{
// Uploaded files use immutable GUID names → safe to cache aggressively.
OnPrepareResponse = ctx =>
{
var p = ctx.Context.Request.Path.Value ?? "";
if (p.StartsWith("/uploads/", StringComparison.OrdinalIgnoreCase))
ctx.Context.Response.Headers["Cache-Control"] = "public,max-age=2592000,immutable";
}
});
app.UseAuthentication();
app.UseAuthorization();