diff --git a/Directory.Packages.props b/Directory.Packages.props
index 182f7e19..30cf682f 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -40,6 +40,7 @@
+
diff --git a/EssentialCSharp.Web.Tests/RouteConfigurationServiceTests.cs b/EssentialCSharp.Web.Tests/RouteConfigurationServiceTests.cs
new file mode 100644
index 00000000..9280e3fd
--- /dev/null
+++ b/EssentialCSharp.Web.Tests/RouteConfigurationServiceTests.cs
@@ -0,0 +1,35 @@
+using EssentialCSharp.Web.Services;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace EssentialCSharp.Web.Tests;
+
+public class RouteConfigurationServiceTests : IClassFixture
+{
+ private readonly WebApplicationFactory _Factory;
+
+ public RouteConfigurationServiceTests(WebApplicationFactory factory)
+ {
+ _Factory = factory;
+ }
+
+ [Fact]
+ public void GetStaticRoutes_ShouldReturnExpectedRoutes()
+ {
+ // Act
+ var routes = _Factory.InServiceScope(serviceProvider =>
+ {
+ var routeConfigurationService = serviceProvider.GetRequiredService();
+ return routeConfigurationService.GetStaticRoutes().ToList();
+ });
+
+ // Assert
+ Assert.NotEmpty(routes);
+
+ // Check for expected routes from the HomeController
+ Assert.Contains("home", routes);
+ Assert.Contains("about", routes);
+ Assert.Contains("guidelines", routes);
+ Assert.Contains("announcements", routes);
+ Assert.Contains("termsofservice", routes);
+ }
+}
diff --git a/EssentialCSharp.Web.Tests/SitemapXmlHelpersTests.cs b/EssentialCSharp.Web.Tests/SitemapXmlHelpersTests.cs
new file mode 100644
index 00000000..6c801d1e
--- /dev/null
+++ b/EssentialCSharp.Web.Tests/SitemapXmlHelpersTests.cs
@@ -0,0 +1,259 @@
+using System.Globalization;
+using DotnetSitemapGenerator;
+using EssentialCSharp.Web.Helpers;
+using EssentialCSharp.Web.Services;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace EssentialCSharp.Web.Tests;
+
+public class SitemapXmlHelpersTests : IClassFixture
+{
+ private readonly WebApplicationFactory _Factory;
+
+ public SitemapXmlHelpersTests(WebApplicationFactory factory)
+ {
+ _Factory = factory;
+ }
+
+ [Fact]
+ public void EnsureSitemapHealthy_WithValidSiteMappings_DoesNotThrow()
+ {
+ // Arrange
+ var siteMappings = new List
+ {
+ CreateSiteMapping(1, 1, true),
+ CreateSiteMapping(1, 2, true),
+ CreateSiteMapping(2, 1, true)
+ };
+
+ // Act & Assert
+ var exception = Record.Exception(() => SitemapXmlHelpers.EnsureSitemapHealthy(siteMappings));
+ Assert.Null(exception);
+ }
+
+ [Fact]
+ public void EnsureSitemapHealthy_WithMultipleCanonicalLinksForSamePage_ThrowsException()
+ {
+ // Arrange - Two mappings for the same chapter/page both marked as canonical
+ var siteMappings = new List
+ {
+ CreateSiteMapping(1, 1, true),
+ CreateSiteMapping(1, 1, true) // Same chapter/page, also canonical
+ };
+
+ // Act & Assert
+ var exception = Assert.Throws(() =>
+ SitemapXmlHelpers.EnsureSitemapHealthy(siteMappings));
+
+ Assert.Contains("Chapter 1, Page 1", exception.Message);
+ Assert.Contains("more than one canonical link", exception.Message);
+ }
+
+ [Fact]
+ public void EnsureSitemapHealthy_WithNoCanonicalLinksForPage_ThrowsException()
+ {
+ // Arrange - No mappings marked as canonical for this page
+ var siteMappings = new List
+ {
+ CreateSiteMapping(1, 1, false),
+ CreateSiteMapping(1, 1, false) // Same chapter/page, neither canonical
+ };
+
+ // Act & Assert
+ var exception = Assert.Throws(() =>
+ SitemapXmlHelpers.EnsureSitemapHealthy(siteMappings));
+
+ Assert.Contains("Chapter 1, Page 1", exception.Message);
+ }
+
+ [Fact]
+ public void GenerateSitemapXml_DoesNotIncludeIdentityRoutes()
+ {
+ // Arrange
+ var tempDir = new DirectoryInfo(Path.GetTempPath());
+ var siteMappings = new List { CreateSiteMapping(1, 1, true) };
+ var baseUrl = "https://test.example.com/";
+
+ // Act & Assert
+ var routeConfigurationService = _Factory.Services.GetRequiredService();
+ SitemapXmlHelpers.GenerateSitemapXml(
+ tempDir,
+ siteMappings,
+ routeConfigurationService,
+ baseUrl,
+ out var nodes);
+
+ var allUrls = nodes.Select(n => n.Url).ToList();
+
+ // Verify no Identity routes are included
+ Assert.DoesNotContain(allUrls, url => url.Contains("Identity", StringComparison.OrdinalIgnoreCase));
+ Assert.DoesNotContain(allUrls, url => url.Contains("Account", StringComparison.OrdinalIgnoreCase));
+
+ // But verify that expected routes are included
+ Assert.Contains(allUrls, url => url.Contains("/home", StringComparison.OrdinalIgnoreCase));
+ Assert.Contains(allUrls, url => url.Contains("/about", StringComparison.OrdinalIgnoreCase));
+ }
+
+ [Fact]
+ public void GenerateSitemapXml_IncludesBaseUrl()
+ {
+ // Arrange
+ var tempDir = new DirectoryInfo(Path.GetTempPath());
+ var siteMappings = new List();
+ var baseUrl = "https://test.example.com/";
+
+ // Act & Assert
+ var routeConfigurationService = _Factory.Services.GetRequiredService();
+ SitemapXmlHelpers.GenerateSitemapXml(
+ tempDir,
+ siteMappings,
+ routeConfigurationService,
+ baseUrl,
+ out var nodes);
+
+ Assert.Contains(nodes, node => node.Url == baseUrl);
+
+ // Verify the root URL has highest priority
+ var rootNode = nodes.First(node => node.Url == baseUrl);
+ Assert.Equal(1.0M, rootNode.Priority);
+ Assert.Equal(ChangeFrequency.Daily, rootNode.ChangeFrequency);
+ }
+
+ [Fact]
+ public void GenerateSitemapXml_IncludesSiteMappingsMarkedForXml()
+ {
+ // Arrange
+ var tempDir = new DirectoryInfo(Path.GetTempPath());
+ var baseUrl = "https://test.example.com/";
+
+ var siteMappings = new List
+ {
+ CreateSiteMapping(1, 1, true, "test-page-1"),
+ CreateSiteMapping(1, 2, false, "test-page-2"), // Not included in XML
+ CreateSiteMapping(2, 1, true, "test-page-3")
+ };
+
+ // Act & Assert
+ var routeConfigurationService = _Factory.Services.GetRequiredService();
+ SitemapXmlHelpers.GenerateSitemapXml(
+ tempDir,
+ siteMappings,
+ routeConfigurationService,
+ baseUrl,
+ out var nodes);
+
+ var allUrls = nodes.Select(n => n.Url).ToList();
+
+ Assert.Contains(allUrls, url => url.Contains("test-page-1"));
+ Assert.DoesNotContain(allUrls, url => url.Contains("test-page-2")); // Not marked for XML
+ Assert.Contains(allUrls, url => url.Contains("test-page-3"));
+ }
+
+ [Fact]
+ public void GenerateSitemapXml_DoesNotIncludeIndexRoutes()
+ {
+ // Arrange
+ var tempDir = new DirectoryInfo(Path.GetTempPath());
+ var siteMappings = new List();
+ var baseUrl = "https://test.example.com/";
+
+ // Act & Assert
+ var routeConfigurationService = _Factory.Services.GetRequiredService();
+ SitemapXmlHelpers.GenerateSitemapXml(
+ tempDir,
+ siteMappings,
+ routeConfigurationService,
+ baseUrl,
+ out var nodes);
+
+ var allUrls = nodes.Select(n => n.Url).ToList();
+
+ // Should not include Index action routes (they're the default)
+ Assert.DoesNotContain(allUrls, url => url.Contains("/Index", StringComparison.OrdinalIgnoreCase));
+ }
+
+ [Fact]
+ public void GenerateSitemapXml_DoesNotIncludeErrorRoutes()
+ {
+ // Arrange
+ var tempDir = new DirectoryInfo(Path.GetTempPath());
+ var siteMappings = new List();
+ var baseUrl = "https://test.example.com/";
+
+ // Act & Assert
+ var routeConfigurationService = _Factory.Services.GetRequiredService();
+ SitemapXmlHelpers.GenerateSitemapXml(
+ tempDir,
+ siteMappings,
+ routeConfigurationService,
+ baseUrl,
+ out var nodes);
+
+ var allUrls = nodes.Select(n => n.Url).ToList();
+
+ // Should not include Error action routes
+ Assert.DoesNotContain(allUrls, url => url.Contains("/Error", StringComparison.OrdinalIgnoreCase));
+ }
+
+ [Fact]
+ public void GenerateAndSerializeSitemapXml_CreatesFileSuccessfully()
+ {
+ // Arrange
+ var logger = _Factory.Services.GetRequiredService>();
+ var tempDir = new DirectoryInfo(Path.GetTempPath());
+ var siteMappings = new List { CreateSiteMapping(1, 1, true) };
+ var baseUrl = "https://test.example.com/";
+
+ // Clean up any existing file
+ var expectedXmlPath = Path.Combine(tempDir.FullName, "sitemap.xml");
+ File.Delete(expectedXmlPath);
+
+ try
+ {
+ // Act
+ var routeConfigurationService = _Factory.Services.GetRequiredService();
+ SitemapXmlHelpers.GenerateAndSerializeSitemapXml(
+ tempDir,
+ siteMappings,
+ logger,
+ routeConfigurationService,
+ baseUrl);
+
+ // Assert
+ Assert.True(File.Exists(expectedXmlPath));
+
+ var xmlContent = File.ReadAllText(expectedXmlPath);
+ Assert.Contains("
+public sealed class WebApplicationFactory : WebApplicationFactory
{
private static string SqlConnectionString => $"DataSource=file:{Guid.NewGuid()}?mode=memory&cache=shared";
private SqliteConnection? _Connection;
@@ -42,6 +42,30 @@ protected override void ConfigureWebHost(IWebHostBuilder builder)
});
}
+ ///
+ /// Executes an action within a service scope, handling scope creation and cleanup automatically.
+ ///
+ /// The return type of the action
+ /// The action to execute with the scoped service provider
+ /// The result of the action
+ public T InServiceScope(Func action)
+ {
+ var factory = Services.GetRequiredService();
+ using var scope = factory.CreateScope();
+ return action(scope.ServiceProvider);
+ }
+
+ ///
+ /// Executes an action within a service scope, handling scope creation and cleanup automatically.
+ ///
+ /// The action to execute with the scoped service provider
+ public void InServiceScope(Action action)
+ {
+ var factory = Services.GetRequiredService();
+ using var scope = factory.CreateScope();
+ action(scope.ServiceProvider);
+ }
+
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
diff --git a/EssentialCSharp.Web/Controllers/BaseController.cs b/EssentialCSharp.Web/Controllers/BaseController.cs
new file mode 100644
index 00000000..f1bdbb89
--- /dev/null
+++ b/EssentialCSharp.Web/Controllers/BaseController.cs
@@ -0,0 +1,22 @@
+using EssentialCSharp.Web.Services;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Filters;
+
+namespace EssentialCSharp.Web.Controllers;
+
+public abstract class BaseController : Controller
+{
+ private readonly IRouteConfigurationService _RouteConfigurationService;
+
+ protected BaseController(IRouteConfigurationService routeConfigurationService)
+ {
+ _RouteConfigurationService = routeConfigurationService;
+ }
+
+ public override void OnActionExecuting(ActionExecutingContext context)
+ {
+ // Automatically add static routes to all views
+ ViewBag.StaticRoutes = System.Text.Json.JsonSerializer.Serialize(_RouteConfigurationService.GetStaticRoutes());
+ base.OnActionExecuting(context);
+ }
+}
diff --git a/EssentialCSharp.Web/EssentialCSharp.Web.csproj b/EssentialCSharp.Web/EssentialCSharp.Web.csproj
index 4cb85e02..2c2b6426 100644
--- a/EssentialCSharp.Web/EssentialCSharp.Web.csproj
+++ b/EssentialCSharp.Web/EssentialCSharp.Web.csproj
@@ -35,6 +35,7 @@
+
diff --git a/EssentialCSharp.Web/Helpers/SitemapXmlHelpers.cs b/EssentialCSharp.Web/Helpers/SitemapXmlHelpers.cs
new file mode 100644
index 00000000..0247b6f5
--- /dev/null
+++ b/EssentialCSharp.Web/Helpers/SitemapXmlHelpers.cs
@@ -0,0 +1,100 @@
+using DotnetSitemapGenerator;
+using DotnetSitemapGenerator.Serialization;
+using EssentialCSharp.Web.Services;
+
+namespace EssentialCSharp.Web.Helpers;
+
+public static class SitemapXmlHelpers
+{
+ public static void EnsureSitemapHealthy(List siteMappings)
+ {
+ var groups = siteMappings.GroupBy(item => new { item.ChapterNumber, item.PageNumber });
+ foreach (var group in groups)
+ {
+ var count = group.Count(item => item.IncludeInSitemapXml);
+ if (count != 1)
+ {
+ throw new InvalidOperationException($"Sitemap error: Chapter {group.Key.ChapterNumber}, Page {group.Key.PageNumber} has more than one canonical link, or none");
+ }
+ }
+ }
+
+ public static void GenerateAndSerializeSitemapXml(DirectoryInfo wwwrootDirectory, List siteMappings, ILogger logger, IRouteConfigurationService routeConfigurationService, string baseUrl)
+ {
+ GenerateSitemapXml(wwwrootDirectory, siteMappings, routeConfigurationService, baseUrl, out List nodes);
+ XmlSerializer sitemapProvider = new();
+ var xmlPath = Path.Combine(wwwrootDirectory.FullName, "sitemap.xml");
+ sitemapProvider.Serialize(new SitemapModel(nodes), xmlPath, true);
+ logger.LogInformation("sitemap.xml successfully written to {XmlPath}", xmlPath);
+ }
+
+ public static void GenerateSitemapXml(DirectoryInfo wwwrootDirectory, List siteMappings, IRouteConfigurationService routeConfigurationService, string baseUrl, out List nodes)
+ {
+ DateTime newDateTime = DateTime.UtcNow;
+
+ // Routes should end up with leading slash
+ baseUrl = baseUrl.TrimEnd('/');
+
+ // Start with the root URL
+ nodes = new() {
+ new($"{baseUrl}/")
+ {
+ LastModificationDate = newDateTime,
+ ChangeFrequency = ChangeFrequency.Daily,
+ Priority = 1.0M
+ }
+ };
+
+ // Add routes dynamically discovered from controllers
+ var allRoutes = routeConfigurationService.GetStaticRoutes();
+ var controllerRoutes = allRoutes
+ .Where(route => !route.Contains("error", StringComparison.OrdinalIgnoreCase)) // Skip Error actions for sitemap
+ .Where(route => !route.Contains("index", StringComparison.OrdinalIgnoreCase)) // Skip Index actions for sitemap
+ .Where(route => !route.Contains("identity", StringComparison.OrdinalIgnoreCase)) // Skip Identity actions for sitemap
+ // All routes should have leading slash
+ .Select(route => $"/{route}") // Add leading slash for sitemap URLs
+ .ToList();
+
+ foreach (var route in controllerRoutes)
+ {
+ nodes.Add(new($"{baseUrl}{route}")
+ {
+ LastModificationDate = newDateTime,
+ ChangeFrequency = GetChangeFrequencyForRoute(route),
+ Priority = GetPriorityForRoute(route)
+ });
+ }
+
+ // Add site mappings from content
+ nodes.AddRange(siteMappings.Where(item => item.IncludeInSitemapXml).Select(siteMapping => new($"{baseUrl.TrimEnd('/')}/{siteMapping.Keys.First()}")
+ {
+ LastModificationDate = newDateTime,
+ ChangeFrequency = ChangeFrequency.Daily,
+ Priority = 0.8M
+ }));
+ }
+
+ private static ChangeFrequency GetChangeFrequencyForRoute(string route)
+ {
+ return route.ToLowerInvariant() switch
+ {
+ "/termsofservice" => ChangeFrequency.Yearly,
+ "/announcements" => ChangeFrequency.Monthly,
+ "/guidelines" => ChangeFrequency.Monthly,
+ _ => ChangeFrequency.Monthly
+ };
+ }
+
+ private static decimal GetPriorityForRoute(string route)
+ {
+ return route.ToLowerInvariant() switch
+ {
+ "/home" => 0.5M,
+ "/about" => 0.5M,
+ "/announcements" => 0.5M,
+ "/guidelines" => 0.9M,
+ "/termsofservice" => 0.2M,
+ _ => 0.5M
+ };
+ }
+}
diff --git a/EssentialCSharp.Web/Program.cs b/EssentialCSharp.Web/Program.cs
index 56f97373..f0f807b6 100644
--- a/EssentialCSharp.Web/Program.cs
+++ b/EssentialCSharp.Web/Program.cs
@@ -3,6 +3,7 @@
using EssentialCSharp.Web.Areas.Identity.Services.PasswordValidators;
using EssentialCSharp.Web.Data;
using EssentialCSharp.Web.Extensions;
+using EssentialCSharp.Web.Helpers;
using EssentialCSharp.Web.Middleware;
using EssentialCSharp.Web.Services;
using EssentialCSharp.Web.Services.Referrals;
@@ -41,7 +42,7 @@ private static void Main(string[] args)
// Create a temporary logger for startup logging
using var loggerFactory = LoggerFactory.Create(loggingBuilder =>
loggingBuilder.AddConsole().SetMinimumLevel(LogLevel.Information));
- var logger = loggerFactory.CreateLogger();
+ var initialLogger = loggerFactory.CreateLogger();
if (!builder.Environment.IsDevelopment())
{
@@ -51,17 +52,17 @@ private static void Main(string[] args)
if (!string.IsNullOrEmpty(appInsightsConnectionString))
{
- builder.Services.AddOpenTelemetry().UseAzureMonitor(
- options =>
- {
- options.ConnectionString = appInsightsConnectionString;
- });
+ builder.Services.AddOpenTelemetry().UseAzureMonitor(
+ options =>
+ {
+ options.ConnectionString = appInsightsConnectionString;
+ });
builder.Services.AddApplicationInsightsTelemetry();
builder.Services.AddServiceProfiler();
}
else
{
- logger.LogWarning("Application Insights connection string not found. Telemetry collection will be disabled.");
+ initialLogger.LogWarning("Application Insights connection string not found. Telemetry collection will be disabled.");
}
}
@@ -146,6 +147,7 @@ private static void Main(string[] args)
builder.Services.AddRazorPages();
builder.Services.AddCaptchaService(builder.Configuration.GetSection(CaptchaOptions.CaptchaSender));
builder.Services.AddSingleton();
+ builder.Services.AddSingleton();
builder.Services.AddHostedService();
builder.Services.AddScoped();
@@ -182,7 +184,6 @@ private static void Main(string[] args)
WebApplication app = builder.Build();
-
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
@@ -215,6 +216,29 @@ private static void Main(string[] args)
app.MapFallbackToController("Index", "Home");
+ // Generate sitemap.xml at startup
+ var wwwrootDirectory = new DirectoryInfo(app.Environment.WebRootPath);
+ var siteMappingService = app.Services.GetRequiredService();
+ var logger = app.Services.GetRequiredService>();
+
+ // Extract base URL from configuration
+ var baseUrl = configuration.GetSection("SiteSettings")["BaseUrl"] ?? "https://essentialcsharp.com";
+
+ try
+ {
+ // Create a scope to resolve scoped services
+ var routeConfigurationService = app.Services.GetRequiredService();
+
+ SitemapXmlHelpers.EnsureSitemapHealthy(siteMappingService.SiteMappings.ToList());
+ SitemapXmlHelpers.GenerateAndSerializeSitemapXml(wwwrootDirectory, siteMappingService.SiteMappings.ToList(), logger, routeConfigurationService, baseUrl);
+ logger.LogInformation("Sitemap.xml generation completed successfully during application startup");
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Failed to generate sitemap.xml during application startup");
+ // Continue startup even if sitemap generation fails
+ }
+
app.Run();
}
}
diff --git a/EssentialCSharp.Web/Services/IRouteConfigurationService.cs b/EssentialCSharp.Web/Services/IRouteConfigurationService.cs
new file mode 100644
index 00000000..b22b1f0a
--- /dev/null
+++ b/EssentialCSharp.Web/Services/IRouteConfigurationService.cs
@@ -0,0 +1,6 @@
+namespace EssentialCSharp.Web.Services;
+
+public interface IRouteConfigurationService
+{
+ IReadOnlySet GetStaticRoutes();
+}
diff --git a/EssentialCSharp.Web/Services/RouteConfigurationService.cs b/EssentialCSharp.Web/Services/RouteConfigurationService.cs
new file mode 100644
index 00000000..0429d6b0
--- /dev/null
+++ b/EssentialCSharp.Web/Services/RouteConfigurationService.cs
@@ -0,0 +1,63 @@
+using Microsoft.AspNetCore.Mvc.Infrastructure;
+
+namespace EssentialCSharp.Web.Services;
+
+public class RouteConfigurationService : IRouteConfigurationService
+{
+ private readonly IActionDescriptorCollectionProvider _ActionDescriptorCollectionProvider;
+ private readonly HashSet _StaticRoutes;
+
+ public RouteConfigurationService(IActionDescriptorCollectionProvider actionDescriptorCollectionProvider)
+ {
+ _ActionDescriptorCollectionProvider = actionDescriptorCollectionProvider;
+ _StaticRoutes = ExtractStaticRoutes();
+ }
+
+ public IReadOnlySet GetStaticRoutes()
+ {
+ return _StaticRoutes;
+ }
+
+ private HashSet ExtractStaticRoutes()
+ {
+ var routes = new HashSet(StringComparer.OrdinalIgnoreCase);
+
+ // Get all action descriptors
+ var actionDescriptors = _ActionDescriptorCollectionProvider.ActionDescriptors.Items;
+
+ foreach (var actionDescriptor in actionDescriptors)
+ {
+ // Look for route attributes
+ if (actionDescriptor.AttributeRouteInfo?.Template != null)
+ {
+ string template = actionDescriptor.AttributeRouteInfo.Template;
+
+ // Remove leading slash and add to our set
+ string routePath = template.TrimStart('/').ToLowerInvariant();
+ routes.Add(routePath);
+ }
+
+ // Skip the default fallback route (Index action in HomeController)
+ if (actionDescriptor.RouteValues.TryGetValue("action", out var action) && action == "Index")
+ continue;
+
+ // Skip Error actions
+ if (action == "Error")
+ continue;
+
+ // For actions without attribute routes, use conventional routing
+ if (actionDescriptor.AttributeRouteInfo?.Template == null &&
+ actionDescriptor.RouteValues.TryGetValue("action", out var actionName) &&
+ actionDescriptor.RouteValues.TryGetValue("controller", out var controllerName))
+ {
+ if (controllerName?.Equals("Home", StringComparison.OrdinalIgnoreCase) == true && actionName != null)
+ {
+ // Use the action name directly as the route
+ routes.Add(actionName.ToLowerInvariant());
+ }
+ }
+ }
+
+ return routes;
+ }
+}
diff --git a/EssentialCSharp.Web/appsettings.Development.json b/EssentialCSharp.Web/appsettings.Development.json
index 9f4ebdc8..f7e1d576 100644
--- a/EssentialCSharp.Web/appsettings.Development.json
+++ b/EssentialCSharp.Web/appsettings.Development.json
@@ -8,5 +8,8 @@
},
"ConnectionStrings": {
"EssentialCSharpWebContextConnection": "Server=localhost;Database=EssentialCSharp.Web;Trusted_Connection=True;MultipleActiveResultSets=true;TrustServerCertificate=true;"
+ },
+ "SiteSettings": {
+ "BaseUrl": "https://localhost:7184"
}
}
diff --git a/EssentialCSharp.Web/appsettings.json b/EssentialCSharp.Web/appsettings.json
index 711f6a36..f7622d36 100644
--- a/EssentialCSharp.Web/appsettings.json
+++ b/EssentialCSharp.Web/appsettings.json
@@ -9,5 +9,8 @@
"HCaptcha": {
"SecretKey": "0x0000000000000000000000000000000000000000",
"SiteKey": "10000000-ffff-ffff-ffff-000000000001"
+ },
+ "SiteSettings": {
+ "BaseUrl": "https://essentialcsharp.com"
}
}
\ No newline at end of file