From f3701c5893eb9dbcc05a4f35f1809dad011a7932 Mon Sep 17 00:00:00 2001 From: "soroush.asadi" Date: Fri, 26 Jun 2026 00:46:08 +0330 Subject: [PATCH] perf(seo): response compression, security headers, immutable upload cache MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- DrSousan.Api/Program.cs | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/DrSousan.Api/Program.cs b/DrSousan.Api/Program.cs index 81f143f..27a86cd 100644 --- a/DrSousan.Api/Program.cs +++ b/DrSousan.Api/Program.cs @@ -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(); + opts.Providers.Add(); + opts.MimeTypes = Microsoft.AspNetCore.ResponseCompression.ResponseCompressionDefaults.MimeTypes + .Concat(new[] { "application/ld+json", "image/svg+xml" }); +}); +builder.Services.Configure( + o => o.Level = System.IO.Compression.CompressionLevel.Fastest); +builder.Services.Configure( + 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();