|
1 | 1 | using System.Text.Json; |
2 | 2 | using Docs.Web; |
| 3 | +using Docs.Web.Middleware; |
3 | 4 |
|
4 | 5 | var builder = WebApplication.CreateBuilder(args); |
5 | 6 |
|
|
8 | 9 | // Add response compression |
9 | 10 | builder.Services.AddResponseCompression(); |
10 | 11 |
|
11 | | -// Content negotiation middleware |
12 | | -builder.Services.AddTransient<ContentNegotiationMiddleware>(); |
13 | | - |
14 | | -var app = builder.Build(); |
15 | | - |
16 | | -app.MapDefaultEndpoints(); |
| 12 | +// Custom middlewares |
| 13 | +builder.Services.AddTransient<MarkdownContentNegotationMiddleware>(); |
| 14 | +builder.Services.AddTransient<TrailingSlashMiddleware>(); |
| 15 | +builder.Services.AddTransient<NotFoundMiddleware>(); |
17 | 16 |
|
18 | 17 | // Load redirect map from Astro-generated redirects.json |
19 | 18 | var redirectMap = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); |
20 | | -var redirectsPath = Path.Combine(app.Environment.WebRootPath, "redirects.json"); |
| 19 | +var redirectsPath = Path.Combine(builder.Environment.WebRootPath, "redirects.json"); |
21 | 20 | if (File.Exists(redirectsPath)) |
22 | 21 | { |
23 | 22 | var json = File.ReadAllText(redirectsPath); |
|
29 | 28 | redirectMap[key] = value; |
30 | 29 | } |
31 | 30 | } |
| 31 | +} |
| 32 | +builder.Services.AddSingleton<IReadOnlyDictionary<string, string>>(redirectMap); |
| 33 | +builder.Services.AddTransient<RedirectMiddleware>(); |
| 34 | + |
| 35 | +var app = builder.Build(); |
| 36 | + |
| 37 | +if (redirectMap.Count > 0) |
| 38 | +{ |
32 | 39 | app.Logger.LogInformation("Loaded {Count} redirects from redirects.json", redirectMap.Count); |
33 | 40 | } |
34 | 41 | else |
35 | 42 | { |
36 | | - app.Logger.LogWarning("redirects.json not found at {Path}, no redirects will be applied", redirectsPath); |
| 43 | + app.Logger.LogWarning("No redirects loaded (redirects.json missing or empty at {Path})", redirectsPath); |
37 | 44 | } |
38 | 45 |
|
39 | | -// Redirect middleware — match old URLs to new destinations (301 permanent) |
40 | | -app.Use(async (context, next) => |
41 | | -{ |
42 | | - var path = context.Request.Path.Value?.TrimEnd('/') ?? ""; |
43 | | - |
44 | | - if (redirectMap.TryGetValue(path, out var destination)) |
45 | | - { |
46 | | - var queryString = context.Request.QueryString.HasValue ? context.Request.QueryString.Value : ""; |
47 | | - context.Response.StatusCode = 301; |
48 | | - context.Response.Headers.Location = $"{destination}{queryString}"; |
49 | | - return; |
50 | | - } |
| 46 | +app.MapDefaultEndpoints(); |
51 | 47 |
|
52 | | - await next(); |
53 | | -}); |
| 48 | +// Redirect middleware — match old URLs to new destinations (301 permanent) |
| 49 | +app.UseMiddleware<RedirectMiddleware>(); |
54 | 50 |
|
55 | 51 | // Enable response compression |
56 | 52 | app.UseResponseCompression(); |
57 | 53 |
|
58 | | -// Content negotiation: serve .md file when Accept: text/markdown |
59 | | -app.UseMiddleware<ContentNegotiationMiddleware>(); |
| 54 | +// Serve .md file when Accept: text/markdown |
| 55 | +app.UseMiddleware<MarkdownContentNegotationMiddleware>(); |
60 | 56 |
|
61 | 57 | // Add trailing slash redirect middleware (replicate nginx behavior) |
62 | | -app.Use(async (context, next) => |
63 | | -{ |
64 | | - var path = context.Request.Path.Value; |
65 | | - |
66 | | - // If path doesn't end with slash and doesn't have a file extension, redirect with trailing slash |
67 | | - if (!string.IsNullOrEmpty(path) && |
68 | | - !path.EndsWith("/") && |
69 | | - !Path.HasExtension(path) && |
70 | | - !path.StartsWith("/health") && |
71 | | - !path.StartsWith("/alive")) |
72 | | - { |
73 | | - var queryString = context.Request.QueryString.HasValue ? context.Request.QueryString.Value : ""; |
74 | | - context.Response.StatusCode = 301; |
75 | | - context.Response.Headers.Location = $"{path}/{queryString}"; |
76 | | - return; |
77 | | - } |
78 | | - |
79 | | - await next(); |
80 | | -}); |
| 58 | +app.UseMiddleware<TrailingSlashMiddleware>(); |
81 | 59 |
|
82 | 60 | // Add version header if APPLICATION_VERSION is set |
83 | 61 | var applicationVersion = Environment.GetEnvironmentVariable("APPLICATION_VERSION"); |
|
164 | 142 | }); |
165 | 143 |
|
166 | 144 | // Handle 404 with custom page |
167 | | -app.Use(async (context, next) => |
168 | | -{ |
169 | | - await next(); |
170 | | - |
171 | | - if (context.Response.StatusCode == 404 && !context.Response.HasStarted) |
172 | | - { |
173 | | - var webHostEnvironment = context.RequestServices.GetRequiredService<IWebHostEnvironment>(); |
174 | | - var notFoundPath = Path.Combine(webHostEnvironment.WebRootPath, "404.html"); |
175 | | - |
176 | | - if (File.Exists(notFoundPath)) |
177 | | - { |
178 | | - context.Response.ContentType = "text/html"; |
179 | | - await context.Response.SendFileAsync(notFoundPath); |
180 | | - } |
181 | | - } |
182 | | -}); |
| 145 | +app.UseMiddleware<NotFoundMiddleware>(); |
183 | 146 |
|
184 | 147 | // Fallback to index.html for directory requests |
185 | 148 | app.MapFallback(async context => |
|
0 commit comments