diff --git a/EssentialCSharp.Web.Tests/SitemapXmlHelpersTests.cs b/EssentialCSharp.Web.Tests/SitemapXmlHelpersTests.cs index b47b49bf..86cc4818 100644 --- a/EssentialCSharp.Web.Tests/SitemapXmlHelpersTests.cs +++ b/EssentialCSharp.Web.Tests/SitemapXmlHelpersTests.cs @@ -246,6 +246,72 @@ public async Task GenerateSitemapXml_DoesNotSetLastModifiedDateWhenSiteMappingDa await Assert.That(siteMappingNode.LastModificationDate).IsNull(); } + [Test] + public async Task GenerateSitemapXml_GuidelinesRoute_HasHighPriority() + { + // Arrange + var siteMappings = new List(); + var baseUrl = "https://test.example.com/"; + + // Act + var routeConfigurationService = Factory.Services.GetRequiredService(); + SitemapXmlHelpers.GenerateSitemapXml( + siteMappings, + routeConfigurationService, + baseUrl, + out var nodes); + + // Assert — /guidelines should have priority 0.9 (higher than default 0.5) + var guidelinesNode = nodes.FirstOrDefault(n => n.Url.Contains("/guidelines", StringComparison.OrdinalIgnoreCase)); + await Assert.That(guidelinesNode).IsNotNull(); + await Assert.That(guidelinesNode!.Priority).IsEqualTo(0.9M); + await Assert.That(guidelinesNode.ChangeFrequency).IsEqualTo(ChangeFrequency.Monthly); + } + + [Test] + public async Task GenerateSitemapXml_TermsOfServiceRoute_HasYearlyFrequencyAndLowPriority() + { + // Arrange + var siteMappings = new List(); + var baseUrl = "https://test.example.com/"; + + // Act + var routeConfigurationService = Factory.Services.GetRequiredService(); + SitemapXmlHelpers.GenerateSitemapXml( + siteMappings, + routeConfigurationService, + baseUrl, + out var nodes); + + // Assert — /termsofservice changes rarely (Yearly) and has low priority (0.2) + var tosNode = nodes.FirstOrDefault(n => n.Url.Contains("/termsofservice", StringComparison.OrdinalIgnoreCase)); + await Assert.That(tosNode).IsNotNull(); + await Assert.That(tosNode!.Priority).IsEqualTo(0.2M); + await Assert.That(tosNode.ChangeFrequency).IsEqualTo(ChangeFrequency.Yearly); + } + + [Test] + public async Task GenerateSitemapXml_UnknownRoute_UsesDefaultSeoValues() + { + // Arrange + var siteMappings = new List { CreateSiteMapping(1, 1, true, "test-page-unknown-route") }; + var baseUrl = "https://test.example.com/"; + + // Act + var routeConfigurationService = Factory.Services.GetRequiredService(); + SitemapXmlHelpers.GenerateSitemapXml( + siteMappings, + routeConfigurationService, + baseUrl, + out var nodes); + + // Assert — content pages without specific config get default SEO values + var contentNode = nodes.FirstOrDefault(n => n.Url.Contains("test-page-unknown-route", StringComparison.OrdinalIgnoreCase)); + await Assert.That(contentNode).IsNotNull(); + await Assert.That(contentNode!.Priority).IsEqualTo(0.5M); + await Assert.That(contentNode.ChangeFrequency).IsEqualTo(ChangeFrequency.Monthly); + } + private static SiteMapping CreateSiteMapping( int chapterNumber, int pageNumber, diff --git a/EssentialCSharp.Web/Constants/RouteConstants.cs b/EssentialCSharp.Web/Constants/RouteConstants.cs new file mode 100644 index 00000000..0e06d8c6 --- /dev/null +++ b/EssentialCSharp.Web/Constants/RouteConstants.cs @@ -0,0 +1,59 @@ +using System.Collections.Frozen; +using DotnetSitemapGenerator; + +namespace EssentialCSharp.Web.Constants; + +/// +/// Centralized definition of application routes and their metadata. +/// This is the single source of truth for static page route paths. +/// Update these whenever a static page is added, removed, or renamed. +/// +public static class RouteConstants +{ + /// + /// Static page routes that are not content pages (e.g., informational, utility pages). + /// Content pages are dynamically discovered from sitemap.json. + /// + public static class StaticPages + { + public const string Home = "/home"; + public const string About = "/about"; + public const string Guidelines = "/guidelines"; + public const string Announcements = "/announcements"; + public const string TermsOfService = "/termsofservice"; + } + + /// + /// Immutable set of non-content route paths. Use to determine if a requested path + /// is a static page (non-content) rather than a content page (from sitemap). + /// + public static readonly FrozenSet NonContentRoutes = new HashSet(StringComparer.OrdinalIgnoreCase) + { + StaticPages.Home, + StaticPages.About, + StaticPages.Guidelines, + StaticPages.Announcements, + StaticPages.TermsOfService + }.ToFrozenSet(StringComparer.OrdinalIgnoreCase); + + /// + /// SEO metadata for static routes used in sitemap.xml generation. + /// + public static class SeoMetadata + { + /// + /// Immutable map of route paths to (ChangeFrequency, Priority) tuples. + /// Keys include the leading slash (e.g. "/home") to match how ASP.NET Core + /// exposes routes. Priority is 0.0–1.0; 0.5 is the sitemap default. + /// + public static readonly FrozenDictionary RouteConfig = + new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { StaticPages.Home, (ChangeFrequency.Monthly, 0.5m) }, + { StaticPages.About, (ChangeFrequency.Monthly, 0.5m) }, + { StaticPages.Guidelines, (ChangeFrequency.Monthly, 0.9m) }, + { StaticPages.Announcements, (ChangeFrequency.Monthly, 0.5m) }, + { StaticPages.TermsOfService,(ChangeFrequency.Yearly, 0.2m) } + }.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase); + } +} diff --git a/EssentialCSharp.Web/Controllers/HomeController.cs b/EssentialCSharp.Web/Controllers/HomeController.cs index de471129..0ef2e717 100644 --- a/EssentialCSharp.Web/Controllers/HomeController.cs +++ b/EssentialCSharp.Web/Controllers/HomeController.cs @@ -1,4 +1,5 @@ using DotnetSitemapGenerator; +using EssentialCSharp.Web.Constants; using EssentialCSharp.Web.Extensions; using EssentialCSharp.Web.Helpers; using EssentialCSharp.Web.Models; @@ -48,7 +49,7 @@ public IActionResult Index() } } - [Route("/TermsOfService", + [Route(RouteConstants.StaticPages.TermsOfService, Name = "TermsOfService")] public IActionResult TermsOfService() { @@ -56,27 +57,27 @@ public IActionResult TermsOfService() return View(); } - [Route("/Announcements", Name = "Announcements")] + [Route(RouteConstants.StaticPages.Announcements, Name = "Announcements")] public IActionResult Announcements() { ViewBag.PageTitle = "Announcements"; return View(); } - [Route("/about", Name = "about")] + [Route(RouteConstants.StaticPages.About, Name = "about")] public IActionResult About() { ViewBag.PageTitle = "About"; return View(); } - [Route("/home", Name = "home")] + [Route(RouteConstants.StaticPages.Home, Name = "home")] public IActionResult Home() { return View(); } - [Route("/guidelines", Name = "guidelines")] + [Route(RouteConstants.StaticPages.Guidelines, Name = "guidelines")] public IActionResult Guidelines() { ViewBag.PageTitle = "Coding Guidelines"; diff --git a/EssentialCSharp.Web/Helpers/SitemapXmlHelpers.cs b/EssentialCSharp.Web/Helpers/SitemapXmlHelpers.cs index 5db73953..52353453 100644 --- a/EssentialCSharp.Web/Helpers/SitemapXmlHelpers.cs +++ b/EssentialCSharp.Web/Helpers/SitemapXmlHelpers.cs @@ -1,4 +1,5 @@ using DotnetSitemapGenerator; +using EssentialCSharp.Web.Constants; using EssentialCSharp.Web.Services; namespace EssentialCSharp.Web.Helpers; @@ -74,26 +75,22 @@ private static bool IsSitemapRoute(string route) => private static ChangeFrequency GetChangeFrequencyForRoute(string route) { - return route.ToLowerInvariant() switch + if (RouteConstants.SeoMetadata.RouteConfig.TryGetValue(route, out var config)) { - "/termsofservice" => ChangeFrequency.Yearly, - "/announcements" => ChangeFrequency.Monthly, - "/guidelines" => ChangeFrequency.Monthly, - _ => ChangeFrequency.Monthly - }; + return config.Frequency; + } + + return ChangeFrequency.Monthly; } private static decimal GetPriorityForRoute(string route) { - return route.ToLowerInvariant() switch + if (RouteConstants.SeoMetadata.RouteConfig.TryGetValue(route, out var config)) { - "/home" => 0.5M, - "/about" => 0.5M, - "/announcements" => 0.5M, - "/guidelines" => 0.9M, - "/termsofservice" => 0.2M, - _ => 0.5M - }; + return config.Priority; + } + + return 0.5M; } } diff --git a/EssentialCSharp.Web/Views/Home/Home.cshtml b/EssentialCSharp.Web/Views/Home/Home.cshtml index 5c96fb51..7c9f949d 100644 --- a/EssentialCSharp.Web/Views/Home/Home.cshtml +++ b/EssentialCSharp.Web/Views/Home/Home.cshtml @@ -122,7 +122,7 @@
diff --git a/EssentialCSharp.Web/src/components/SidebarPanel.vue b/EssentialCSharp.Web/src/components/SidebarPanel.vue index 22e5f173..0bbafd78 100644 --- a/EssentialCSharp.Web/src/components/SidebarPanel.vue +++ b/EssentialCSharp.Web/src/components/SidebarPanel.vue @@ -1,35 +1,11 @@