Skip to content

Commit d0e1672

Browse files
fix: serve sitemap.xml in-memory via controller endpoint
Replace startup file-write of sitemap.xml with a dynamic /sitemap.xml controller action. This eliminates the UnauthorizedAccessException caused by the non-root app user being unable to write to /app/wwwroot/sitemap.xml. - Add SitemapXml() action to HomeController using SitemapProvider.CreateSitemap - Inject IOptions<SiteSettings> for base URL, IRouteConfigurationService via [FromServices] - Remove GenerateAndSerializeSitemapXml (file-write) from SitemapXmlHelpers - Remove unused DirectoryInfo parameter from GenerateSitemapXml - Remove startup sitemap file-write from Program.cs; keep EnsureSitemapHealthy for fail-fast validation - Update tests to match new GenerateSitemapXml signature - Inline BuildKit secret in CI workflow (no temp PAT file) Serving from memory is also more secure: no write permissions needed, no stale file on disk, and the content is always fresh.
1 parent 21d199a commit d0e1672

4 files changed

Lines changed: 16 additions & 38 deletions

File tree

EssentialCSharp.Web.Tests/SitemapXmlHelpersTests.cs

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
using System.IO;
21
using System.Globalization;
32
using DotnetSitemapGenerator;
43
using EssentialCSharp.Web.Helpers;
@@ -70,14 +69,12 @@ await Assert.That(() => SitemapXmlHelpers.EnsureSitemapHealthy(siteMappings))
7069
public async Task GenerateSitemapXml_DoesNotIncludeIdentityRoutes()
7170
{
7271
// Arrange
73-
var tempDir = Directory.CreateTempSubdirectory("SitemapTest_");
7472
var siteMappings = new List<SiteMapping> { CreateSiteMapping(1, 1, true) };
7573
var baseUrl = "https://test.example.com/";
7674

7775
// Act & Assert
7876
var routeConfigurationService = _Factory.Services.GetRequiredService<IRouteConfigurationService>();
7977
SitemapXmlHelpers.GenerateSitemapXml(
80-
tempDir,
8178
siteMappings,
8279
routeConfigurationService,
8380
baseUrl,
@@ -98,14 +95,12 @@ public async Task GenerateSitemapXml_DoesNotIncludeIdentityRoutes()
9895
public async Task GenerateSitemapXml_IncludesBaseUrl()
9996
{
10097
// Arrange
101-
var tempDir = Directory.CreateTempSubdirectory("SitemapTest_");
10298
var siteMappings = new List<SiteMapping>();
10399
var baseUrl = "https://test.example.com/";
104100

105101
// Act & Assert
106102
var routeConfigurationService = _Factory.Services.GetRequiredService<IRouteConfigurationService>();
107103
SitemapXmlHelpers.GenerateSitemapXml(
108-
tempDir,
109104
siteMappings,
110105
routeConfigurationService,
111106
baseUrl,
@@ -126,7 +121,6 @@ public async Task GenerateSitemapXml_IncludesBaseUrl()
126121
public async Task GenerateSitemapXml_IncludesSiteMappingsMarkedForXml()
127122
{
128123
// Arrange
129-
var tempDir = Directory.CreateTempSubdirectory("SitemapTest_");
130124
var baseUrl = "https://test.example.com/";
131125

132126
var siteMappings = new List<SiteMapping>
@@ -139,7 +133,6 @@ public async Task GenerateSitemapXml_IncludesSiteMappingsMarkedForXml()
139133
// Act & Assert
140134
var routeConfigurationService = _Factory.Services.GetRequiredService<IRouteConfigurationService>();
141135
SitemapXmlHelpers.GenerateSitemapXml(
142-
tempDir,
143136
siteMappings,
144137
routeConfigurationService,
145138
baseUrl,
@@ -156,14 +149,12 @@ public async Task GenerateSitemapXml_IncludesSiteMappingsMarkedForXml()
156149
public async Task GenerateSitemapXml_DoesNotIncludeIndexRoutes()
157150
{
158151
// Arrange
159-
var tempDir = Directory.CreateTempSubdirectory("SitemapTest_");
160152
var siteMappings = new List<SiteMapping>();
161153
var baseUrl = "https://test.example.com/";
162154

163155
// Act & Assert
164156
var routeConfigurationService = _Factory.Services.GetRequiredService<IRouteConfigurationService>();
165157
SitemapXmlHelpers.GenerateSitemapXml(
166-
tempDir,
167158
siteMappings,
168159
routeConfigurationService,
169160
baseUrl,
@@ -179,14 +170,12 @@ public async Task GenerateSitemapXml_DoesNotIncludeIndexRoutes()
179170
public async Task GenerateSitemapXml_DoesNotIncludeErrorRoutes()
180171
{
181172
// Arrange
182-
var tempDir = Directory.CreateTempSubdirectory("SitemapTest_");
183173
var siteMappings = new List<SiteMapping>();
184174
var baseUrl = "https://test.example.com/";
185175

186176
// Act & Assert
187177
var routeConfigurationService = _Factory.Services.GetRequiredService<IRouteConfigurationService>();
188178
SitemapXmlHelpers.GenerateSitemapXml(
189-
tempDir,
190179
siteMappings,
191180
routeConfigurationService,
192181
baseUrl,
@@ -202,7 +191,6 @@ public async Task GenerateSitemapXml_DoesNotIncludeErrorRoutes()
202191
public async Task GenerateSitemapXml_UsesLastModifiedDateFromSiteMapping()
203192
{
204193
// Arrange
205-
var tempDir = Directory.CreateTempSubdirectory("SitemapTest_");
206194
var baseUrl = "https://test.example.com/";
207195
var specificLastModified = new DateTime(2023, 5, 15, 10, 30, 0, DateTimeKind.Utc);
208196

@@ -214,7 +202,6 @@ public async Task GenerateSitemapXml_UsesLastModifiedDateFromSiteMapping()
214202
// Act
215203
var routeConfigurationService = _Factory.Services.GetRequiredService<IRouteConfigurationService>();
216204
SitemapXmlHelpers.GenerateSitemapXml(
217-
tempDir,
218205
siteMappings,
219206
routeConfigurationService,
220207
baseUrl,

EssentialCSharp.Web/Controllers/HomeController.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
1+
using DotnetSitemapGenerator;
12
using EssentialCSharp.Web.Extensions;
3+
using EssentialCSharp.Web.Helpers;
24
using EssentialCSharp.Web.Models;
35
using EssentialCSharp.Web.Services;
46
using HtmlAgilityPack;
57
using Microsoft.AspNetCore.Mvc;
68
using Microsoft.AspNetCore.RateLimiting;
79
using Microsoft.CodeAnalysis;
10+
using Microsoft.Extensions.Options;
811

912
namespace EssentialCSharp.Web.Controllers;
1013

11-
public class HomeController(ILogger<HomeController> logger, IWebHostEnvironment hostingEnvironment, ISiteMappingService siteMappingService, IHttpContextAccessor httpContextAccessor, IRouteConfigurationService routeConfigurationService) : BaseController(routeConfigurationService, httpContextAccessor)
14+
public class HomeController(ILogger<HomeController> logger, IWebHostEnvironment hostingEnvironment, ISiteMappingService siteMappingService, IHttpContextAccessor httpContextAccessor, IRouteConfigurationService routeConfigurationService, IOptions<SiteSettings> siteSettings) : BaseController(routeConfigurationService, httpContextAccessor)
1215
{
1316
[EnableRateLimiting("content")]
1417
public IActionResult Index()
@@ -86,6 +89,14 @@ public IActionResult Guidelines()
8689
return View();
8790
}
8891

92+
[Route("/sitemap.xml")]
93+
[ResponseCache(Duration = 3600)]
94+
public IActionResult SitemapXml([FromServices] IRouteConfigurationService sitemapRouteConfig)
95+
{
96+
SitemapXmlHelpers.GenerateSitemapXml(siteMappingService.SiteMappings.ToList(), sitemapRouteConfig, siteSettings.Value.BaseUrl, out var nodes);
97+
return new SitemapProvider().CreateSitemap(new SitemapModel(nodes));
98+
}
99+
89100
private string FlipPage(int currentChapter, int currentPage, bool next)
90101
{
91102
if (siteMappingService.SiteMappings.Count == 0)

EssentialCSharp.Web/Helpers/SitemapXmlHelpers.cs

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
using DotnetSitemapGenerator;
2-
using DotnetSitemapGenerator.Serialization;
32
using EssentialCSharp.Web.Services;
43

54
namespace EssentialCSharp.Web.Helpers;
65

7-
public static partial class SitemapXmlHelpers
6+
public static class SitemapXmlHelpers
87
{
98
public static void EnsureSitemapHealthy(List<SiteMapping> siteMappings)
109
{
@@ -19,16 +18,7 @@ public static void EnsureSitemapHealthy(List<SiteMapping> siteMappings)
1918
}
2019
}
2120

22-
public static void GenerateAndSerializeSitemapXml(DirectoryInfo wwwrootDirectory, List<SiteMapping> siteMappings, ILogger logger, IRouteConfigurationService routeConfigurationService, string baseUrl)
23-
{
24-
GenerateSitemapXml(wwwrootDirectory, siteMappings, routeConfigurationService, baseUrl, out List<SitemapNode> nodes);
25-
XmlSerializer sitemapProvider = new();
26-
var xmlPath = Path.Join(wwwrootDirectory.FullName, "sitemap.xml");
27-
sitemapProvider.Serialize(new SitemapModel(nodes), xmlPath, true);
28-
LogSitemapWritten(logger, xmlPath);
29-
}
30-
31-
public static void GenerateSitemapXml(DirectoryInfo wwwrootDirectory, List<SiteMapping> siteMappings, IRouteConfigurationService routeConfigurationService, string baseUrl, out List<SitemapNode> nodes)
21+
public static void GenerateSitemapXml(List<SiteMapping> siteMappings, IRouteConfigurationService routeConfigurationService, string baseUrl, out List<SitemapNode> nodes)
3222
{
3323
DateTime newDateTime = DateTime.UtcNow;
3424

@@ -98,6 +88,4 @@ private static decimal GetPriorityForRoute(string route)
9888
};
9989
}
10090

101-
[LoggerMessage(Level = LogLevel.Information, Message = "sitemap.xml successfully written to {XmlPath}")]
102-
private static partial void LogSitemapWritten(ILogger logger, string xmlPath);
10391
}

EssentialCSharp.Web/Program.cs

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -569,27 +569,19 @@ await McpJsonRpcResponseWriter.WriteErrorAsync(
569569

570570
app.MapFallbackToController("Index", "Home");
571571

572-
// Generate sitemap.xml at startup
573-
var wwwrootDirectory = new DirectoryInfo(app.Environment.WebRootPath);
572+
// Validate sitemap data at startup to fail fast on bad content
574573
var siteMappingService = app.Services.GetRequiredService<ISiteMappingService>();
575574
var logger = app.Services.GetRequiredService<ILogger<Program>>();
576575

577-
// Extract base URL from configuration
578-
var baseUrl = app.Services.GetRequiredService<IOptions<SiteSettings>>().Value.BaseUrl;
579-
580576
try
581577
{
582-
// Create a scope to resolve scoped services
583-
var routeConfigurationService = app.Services.GetRequiredService<IRouteConfigurationService>();
584-
585578
SitemapXmlHelpers.EnsureSitemapHealthy(siteMappingService.SiteMappings.ToList());
586-
SitemapXmlHelpers.GenerateAndSerializeSitemapXml(wwwrootDirectory, siteMappingService.SiteMappings.ToList(), initialLogger, routeConfigurationService, baseUrl);
587579
LogSitemapGenerationSucceeded(logger);
588580
}
589581
catch (Exception ex)
590582
{
591583
LogSitemapGenerationFailed(logger, ex);
592-
// Continue startup even if sitemap generation fails
584+
// Continue startup even if sitemap validation fails
593585
}
594586

595587
app.Run();

0 commit comments

Comments
 (0)