|
19 | 19 | // Add route configuration to enforce lowercase URLs for better SEO |
20 | 20 | builder.Services.Configure<RouteOptions>(options => |
21 | 21 | { |
22 | | - options.LowercaseUrls = true; |
23 | | - options.LowercaseQueryStrings = true; |
24 | | - options.AppendTrailingSlash = false; |
| 22 | + options.LowercaseUrls = true; |
| 23 | + options.LowercaseQueryStrings = true; |
| 24 | + options.AppendTrailingSlash = false; |
25 | 25 | }); |
26 | 26 |
|
27 | 27 | builder.AddAzureTableClient("tables"); |
|
34 | 34 | // Configure Redis key prefixes for better organization |
35 | 35 | builder.Services.PostConfigure<Microsoft.Extensions.Caching.StackExchangeRedis.RedisCacheOptions>(options => |
36 | 36 | { |
37 | | - options.InstanceName = "CopilotThatJawn:"; |
| 37 | + options.InstanceName = "CopilotThatJawn:"; |
38 | 38 | }); |
39 | 39 |
|
40 | 40 | // Add WebOptimizer services |
41 | 41 | builder.Services.AddWebOptimizer(pipeline => |
42 | 42 | { |
43 | | - if (!builder.Environment.IsDevelopment()) |
44 | | - { |
45 | | - // Bundle and minify CSS files in production only |
46 | | - pipeline.AddCssBundle("/css/bundle.min.css", |
47 | | - "css/site.css", |
48 | | - "css/layout.css"); |
49 | | - |
50 | | - // Bundle and minify JavaScript files in production only |
51 | | - pipeline.AddJavaScriptBundle("/js/bundle.min.js", |
52 | | - "js/site.js", |
53 | | - "js/analytics.js", |
54 | | - "js/theme-switcher.js"); |
55 | | - |
56 | | - // Enable minification for all CSS files |
57 | | - pipeline.MinifyCssFiles(); |
58 | | - |
59 | | - // Enable minification for all JavaScript files |
60 | | - pipeline.MinifyJsFiles(); |
61 | | - } |
62 | | - else |
63 | | - { |
64 | | - // In development, still enable basic minification for testing |
65 | | - pipeline.MinifyCssFiles("css/*.css"); |
66 | | - pipeline.MinifyJsFiles("js/*.js"); |
67 | | - } |
| 43 | + if (!builder.Environment.IsDevelopment()) |
| 44 | + { |
| 45 | + // Bundle and minify CSS files in production only |
| 46 | + pipeline.AddCssBundle("/css/bundle.min.css", |
| 47 | + "css/site.css", |
| 48 | + "css/layout.css"); |
| 49 | + |
| 50 | + // Bundle and minify JavaScript files in production only |
| 51 | + pipeline.AddJavaScriptBundle("/js/bundle.min.js", |
| 52 | + "js/site.js", |
| 53 | + "js/analytics.js", |
| 54 | + "js/theme-switcher.js"); |
| 55 | + |
| 56 | + // Enable minification for all CSS files |
| 57 | + pipeline.MinifyCssFiles(); |
| 58 | + |
| 59 | + // Enable minification for all JavaScript files |
| 60 | + pipeline.MinifyJsFiles(); |
| 61 | + } |
| 62 | + else |
| 63 | + { |
| 64 | + // In development, still enable basic minification for testing |
| 65 | + pipeline.MinifyCssFiles("css/*.css"); |
| 66 | + pipeline.MinifyJsFiles("js/*.js"); |
| 67 | + } |
68 | 68 | }); |
69 | 69 |
|
70 | 70 | // Add services to the container. |
71 | 71 | builder.Services.AddRazorPages() |
72 | | - .AddRazorPagesOptions(options => |
73 | | - { |
74 | | - options.RootDirectory = "/Pages"; |
75 | | - }); |
| 72 | + .AddRazorPagesOptions(options => |
| 73 | + { |
| 74 | + options.RootDirectory = "/Pages"; |
| 75 | + }); |
76 | 76 |
|
77 | 77 | builder.Services.AddControllers(); // Add controller support for API endpoints |
78 | 78 | builder.Services.AddMvc().AddViewComponentsAsServices(); // Register view components |
|
84 | 84 | // Configure output cache policies (Redis is already configured above via AddRedisOutputCache) |
85 | 85 | builder.Services.Configure<Microsoft.AspNetCore.OutputCaching.OutputCacheOptions>(options => |
86 | 86 | { |
87 | | - // Default site-wide caching policy - extended to 6 hours for better performance |
88 | | - options.AddBasePolicy(builder => |
89 | | - builder.Cache() |
90 | | - .SetVaryByHost(true) |
91 | | - .SetVaryByQuery("*") |
92 | | - .SetVaryByHeader("Accept-Language") // Vary by language |
93 | | - .Expire(TimeSpan.FromHours(6)) // Cache for 6 hours by default |
94 | | - .Tag("outputcache", "site")); // Add tags for better organization |
95 | | - |
96 | | - // Special policy for static content pages - extended to 3 days |
97 | | - options.AddPolicy("StaticContent", builder => |
98 | | - builder.Cache() |
99 | | - .SetVaryByHost(true) |
100 | | - .Expire(TimeSpan.FromDays(3)) // Cache static content for 3 days |
101 | | - .Tag("outputcache", "static")); // Add tags for better organization |
102 | | - |
103 | | - // Special policy for tips and content pages - extended to 3 days since they're static |
104 | | - options.AddPolicy("TipsContent", builder => |
105 | | - builder.Cache() |
106 | | - .SetVaryByHost(true) |
107 | | - .SetVaryByRouteValue("slug") // Vary by tip slug |
108 | | - .Expire(TimeSpan.FromDays(3)) // Cache tips for 3 days |
109 | | - .Tag("outputcache", "tips", "content")); // Add tags for better organization |
110 | | - |
111 | | - // Policy for frequently updated content - extended to 6 hours minimum |
112 | | - options.AddPolicy("DynamicContent", builder => |
113 | | - builder.Cache() |
114 | | - .SetVaryByHost(true) |
115 | | - .SetVaryByQuery("*") |
116 | | - .Expire(TimeSpan.FromHours(6)) // Cache dynamic content for 6 hours |
117 | | - .Tag("outputcache", "dynamic")); // Add tags for better organization |
| 87 | + // Default site-wide caching policy - extended to 6 hours for better performance |
| 88 | + options.AddBasePolicy(builder => |
| 89 | + builder.Cache() |
| 90 | + .SetVaryByHost(true) |
| 91 | + .SetVaryByQuery("*") |
| 92 | + .SetVaryByHeader("Accept-Language") // Vary by language |
| 93 | + .Expire(TimeSpan.FromHours(6)) // Cache for 6 hours by default |
| 94 | + .Tag("outputcache", "site")); // Add tags for better organization |
| 95 | + |
| 96 | + // Special policy for static content pages - extended to 3 days |
| 97 | + options.AddPolicy("StaticContent", builder => |
| 98 | + builder.Cache() |
| 99 | + .SetVaryByHost(true) |
| 100 | + .Expire(TimeSpan.FromDays(3)) // Cache static content for 3 days |
| 101 | + .Tag("outputcache", "static")); // Add tags for better organization |
| 102 | + |
| 103 | + // Special policy for tips and content pages - extended to 3 days since they're static |
| 104 | + options.AddPolicy("TipsContent", builder => |
| 105 | + builder.Cache() |
| 106 | + .SetVaryByHost(true) |
| 107 | + .SetVaryByRouteValue("slug") // Vary by tip slug |
| 108 | + .Expire(TimeSpan.FromDays(3)) // Cache tips for 3 days |
| 109 | + .Tag("outputcache", "tips", "content")); // Add tags for better organization |
| 110 | + |
| 111 | + // Policy for frequently updated content - extended to 6 hours minimum |
| 112 | + options.AddPolicy("DynamicContent", builder => |
| 113 | + builder.Cache() |
| 114 | + .SetVaryByHost(true) |
| 115 | + .SetVaryByQuery("*") |
| 116 | + .Expire(TimeSpan.FromHours(6)) // Cache dynamic content for 6 hours |
| 117 | + .Tag("outputcache", "dynamic")); // Add tags for better organization |
118 | 118 | }); |
119 | 119 |
|
120 | 120 | // Add response compression |
121 | 121 | builder.Services.AddResponseCompression(options => |
122 | 122 | { |
123 | | - options.EnableForHttps = true; |
124 | | - options.Providers.Add<BrotliCompressionProvider>(); |
125 | | - options.Providers.Add<GzipCompressionProvider>(); |
126 | | - options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat( |
127 | | - new[] { "image/svg+xml", "application/javascript", "text/css" }); |
| 123 | + options.EnableForHttps = true; |
| 124 | + options.Providers.Add<BrotliCompressionProvider>(); |
| 125 | + options.Providers.Add<GzipCompressionProvider>(); |
| 126 | + options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat( |
| 127 | + new[] { "image/svg+xml", "application/javascript", "text/css" }); |
128 | 128 | }); |
129 | 129 |
|
130 | 130 | builder.Services.Configure<BrotliCompressionProviderOptions>(options => |
131 | 131 | { |
132 | | - options.Level = CompressionLevel.Optimal; |
| 132 | + options.Level = CompressionLevel.Optimal; |
133 | 133 | }); |
134 | 134 |
|
135 | 135 | builder.Services.Configure<GzipCompressionProviderOptions>(options => |
136 | 136 | { |
137 | | - options.Level = CompressionLevel.Optimal; |
| 137 | + options.Level = CompressionLevel.Optimal; |
138 | 138 | }); |
139 | 139 |
|
140 | 140 | // Add Azure Blob Storage |
141 | | -builder.Services.AddSingleton(x => |
| 141 | +builder.Services.AddSingleton(x => |
142 | 142 | { |
143 | | - var connectionString = builder.Configuration.GetConnectionString("blobs"); |
144 | | - return new BlobServiceClient(connectionString); |
| 143 | + var connectionString = builder.Configuration.GetConnectionString("blobs"); |
| 144 | + if (Uri.IsWellFormedUriString(connectionString, UriKind.Absolute)) |
| 145 | + { |
| 146 | + // If the connection string is a URI, use it directly |
| 147 | + return new BlobServiceClient(new Uri(connectionString)); |
| 148 | + } |
| 149 | + else |
| 150 | + { |
| 151 | + // Otherwise, treat it as a standard connection string |
| 152 | + if (string.IsNullOrWhiteSpace(connectionString)) |
| 153 | + { |
| 154 | + throw new ArgumentException("Invalid Azure Blob Storage connection string.", nameof(connectionString)); |
| 155 | + } |
| 156 | + return new BlobServiceClient(connectionString); |
| 157 | + } |
145 | 158 | }); |
146 | 159 |
|
147 | 160 | // Add Content Service with image handling |
|
152 | 165 | var options = new RewriteOptions() |
153 | 166 | .AddRedirectToNonWwwPermanent() |
154 | 167 | .AddRedirect("^tips/tag$", "tips", 301); // Redirect /tips/tag to /tips with 301 (permanent) redirect |
155 | | - //.AddRedirectToHttpsPermanent(); |
| 168 | + //.AddRedirectToHttpsPermanent(); |
156 | 169 | app.UseRewriter(options); |
157 | 170 |
|
158 | 171 | // Configure the HTTP request pipeline. |
|
179 | 192 |
|
180 | 193 | if (!app.Environment.IsDevelopment()) |
181 | 194 | { |
182 | | - app.UseWebOptimizer(); // Use WebOptimizer in production |
183 | | - |
184 | | - // Add middleware to handle cache headers for WebOptimizer files |
185 | | - app.Use(async (context, next) => |
186 | | - { |
187 | | - if (context.Request.Path.StartsWithSegments("/css/bundle.min.css") || |
188 | | - context.Request.Path.StartsWithSegments("/js/bundle.min.js")) |
189 | | - { |
190 | | - // Set headers to ensure proper cache behavior for bundled files |
191 | | - context.Response.OnStarting(() => |
192 | | - { |
193 | | - context.Response.Headers.CacheControl = "public,max-age=31536000,immutable"; |
194 | | - context.Response.Headers.Vary = "Accept-Encoding"; |
195 | | - return Task.CompletedTask; |
196 | | - }); |
197 | | - } |
198 | | - await next(); |
199 | | - }); |
| 195 | + app.UseWebOptimizer(); // Use WebOptimizer in production |
| 196 | + |
| 197 | + // Add middleware to handle cache headers for WebOptimizer files |
| 198 | + app.Use(async (context, next) => |
| 199 | + { |
| 200 | + if (context.Request.Path.StartsWithSegments("/css/bundle.min.css") || |
| 201 | + context.Request.Path.StartsWithSegments("/js/bundle.min.js")) |
| 202 | + { |
| 203 | + // Set headers to ensure proper cache behavior for bundled files |
| 204 | + context.Response.OnStarting(() => |
| 205 | + { |
| 206 | + context.Response.Headers.CacheControl = "public,max-age=31536000,immutable"; |
| 207 | + context.Response.Headers.Vary = "Accept-Encoding"; |
| 208 | + return Task.CompletedTask; |
| 209 | + }); |
| 210 | + } |
| 211 | + await next(); |
| 212 | + }); |
200 | 213 | } |
201 | 214 |
|
202 | 215 | app.UseStaticFiles(new StaticFileOptions |
203 | 216 | { |
204 | | - OnPrepareResponse = ctx => |
205 | | - { |
206 | | - if (!app.Environment.IsDevelopment()) |
207 | | - { |
208 | | - var path = ctx.Context.Request.Path.Value?.ToLowerInvariant(); |
209 | | - |
210 | | - // Different caching strategies based on file type and path |
211 | | - if (path != null) |
212 | | - { |
213 | | - // WebOptimizer bundles and files with version query strings - cache aggressively |
214 | | - if (path.Contains("bundle.min.") || ctx.Context.Request.Query.ContainsKey("v")) |
215 | | - { |
216 | | - ctx.Context.Response.Headers.CacheControl = "public,max-age=31536000,immutable"; // 1 year |
217 | | - } |
218 | | - // Regular CSS/JS files - shorter cache with validation |
219 | | - else if (path.EndsWith(".css") || path.EndsWith(".js")) |
220 | | - { |
221 | | - ctx.Context.Response.Headers.CacheControl = "public,max-age=3600,must-revalidate"; // 1 hour |
222 | | - } |
223 | | - // Images and fonts - medium cache |
224 | | - else if (path.EndsWith(".png") || path.EndsWith(".jpg") || path.EndsWith(".jpeg") || |
225 | | - path.EndsWith(".gif") || path.EndsWith(".svg") || path.EndsWith(".webp") || |
226 | | - path.EndsWith(".woff") || path.EndsWith(".woff2") || path.EndsWith(".ttf")) |
227 | | - { |
228 | | - ctx.Context.Response.Headers.CacheControl = "public,max-age=2592000"; // 30 days |
229 | | - } |
230 | | - // Other static files - short cache |
231 | | - else |
232 | | - { |
233 | | - ctx.Context.Response.Headers.CacheControl = "public,max-age=3600"; // 1 hour |
234 | | - } |
235 | | - } |
236 | | - |
237 | | - ctx.Context.Response.Headers.Vary = "Accept-Encoding"; |
238 | | - } |
239 | | - else |
240 | | - { |
241 | | - // Disable caching in development |
242 | | - ctx.Context.Response.Headers.CacheControl = "no-cache, no-store"; |
243 | | - ctx.Context.Response.Headers.Pragma = "no-cache"; |
244 | | - ctx.Context.Response.Headers.Expires = "-1"; |
245 | | - } |
246 | | - } |
| 217 | + OnPrepareResponse = ctx => |
| 218 | + { |
| 219 | + if (!app.Environment.IsDevelopment()) |
| 220 | + { |
| 221 | + var path = ctx.Context.Request.Path.Value?.ToLowerInvariant(); |
| 222 | + |
| 223 | + // Different caching strategies based on file type and path |
| 224 | + if (path != null) |
| 225 | + { |
| 226 | + // WebOptimizer bundles and files with version query strings - cache aggressively |
| 227 | + if (path.Contains("bundle.min.") || ctx.Context.Request.Query.ContainsKey("v")) |
| 228 | + { |
| 229 | + ctx.Context.Response.Headers.CacheControl = "public,max-age=31536000,immutable"; // 1 year |
| 230 | + } |
| 231 | + // Regular CSS/JS files - shorter cache with validation |
| 232 | + else if (path.EndsWith(".css") || path.EndsWith(".js")) |
| 233 | + { |
| 234 | + ctx.Context.Response.Headers.CacheControl = "public,max-age=3600,must-revalidate"; // 1 hour |
| 235 | + } |
| 236 | + // Images and fonts - medium cache |
| 237 | + else if (path.EndsWith(".png") || path.EndsWith(".jpg") || path.EndsWith(".jpeg") || |
| 238 | + path.EndsWith(".gif") || path.EndsWith(".svg") || path.EndsWith(".webp") || |
| 239 | + path.EndsWith(".woff") || path.EndsWith(".woff2") || path.EndsWith(".ttf")) |
| 240 | + { |
| 241 | + ctx.Context.Response.Headers.CacheControl = "public,max-age=2592000"; // 30 days |
| 242 | + } |
| 243 | + // Other static files - short cache |
| 244 | + else |
| 245 | + { |
| 246 | + ctx.Context.Response.Headers.CacheControl = "public,max-age=3600"; // 1 hour |
| 247 | + } |
| 248 | + } |
| 249 | + |
| 250 | + ctx.Context.Response.Headers.Vary = "Accept-Encoding"; |
| 251 | + } |
| 252 | + else |
| 253 | + { |
| 254 | + // Disable caching in development |
| 255 | + ctx.Context.Response.Headers.CacheControl = "no-cache, no-store"; |
| 256 | + ctx.Context.Response.Headers.Pragma = "no-cache"; |
| 257 | + ctx.Context.Response.Headers.Expires = "-1"; |
| 258 | + } |
| 259 | + } |
247 | 260 | }); |
248 | 261 |
|
249 | 262 | app.UseRouting(); |
|
0 commit comments