Skip to content
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
//+:cnd:noEmit
using Boilerplate.Shared.Dtos.Statistics;
using Boilerplate.Shared.Controllers.Products;
using Boilerplate.Shared.Controllers.Statistics;
//#if(module == "Sales")
using Boilerplate.Shared.Dtos.Products;
using Boilerplate.Shared.Controllers.Products;
//#endif
namespace Boilerplate.Client.Core.Components.Pages;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
User-agent: *
Disallow:

Sitemap: https://use-your-web-app-url-here.com/sitemap.xml
Sitemap: https://use-your-web-app-url-here.com/sitemap_index.xml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ self.serverHandledUrls = [
/\/signin-/,
/\/.well-known/,
/\/sitemap.xml/,
//#if (module == "Sales")
/\/products.xml/,
//#endif
/\/sitemap_index.xml/
];

self.defaultUrl = "/";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,12 @@ public static void AddServerApiProjectServices(this WebApplicationBuilder builde

services
.AddControllers()
.AddJsonOptions(options => options.JsonSerializerOptions.TypeInfoResolverChain.AddRange([AppJsonContext.Default, IdentityJsonContext.Default, ServerJsonContext.Default]))
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.CamelCase;
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
options.JsonSerializerOptions.TypeInfoResolverChain.AddRange([AppJsonContext.Default, IdentityJsonContext.Default, ServerJsonContext.Default]);
})
//#if (api == "Integrated")
.AddApplicationPart(typeof(AppControllerBase).Assembly)
//#endif
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
//+:cnd:noEmit
using System.Net;
using System.Web;
using System.Reflection;
using System.Runtime.Loader;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.FileProviders;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Components.Endpoints;
using Microsoft.AspNetCore.Localization.Routing;
using System.Text.RegularExpressions;
using Boilerplate.Shared;
using Boilerplate.Shared.Attributes;
using Boilerplate.Client.Core.Services;
//#if(module == "Sales")
using Boilerplate.Shared.Dtos.Products;
using Boilerplate.Shared.Controllers.Products;
//#endif

namespace Boilerplate.Server.Web;

Expand Down Expand Up @@ -175,30 +183,79 @@ public static void ConfigureMiddlewares(this WebApplication app)

private static void UseSiteMap(this WebApplication app)
{
var urls = Urls.All!;
const string siteMapHeader = @"<?xml version=""1.0"" encoding=""UTF-8""?>
<urlset xmlns=""http://www.sitemaps.org/schemas/sitemap/0.9"">";

urls = CultureInfoManager.MultilingualEnabled ?
urls.Union(CultureInfoManager.SupportedCultures.SelectMany(sc => urls.Select(url => $"{sc.Culture.Name}{url}"))).ToArray() :
urls;
app.MapGet("/sitemap_index.xml", [AppResponseCache(SharedMaxAge = 3600 * 24 * 7)] async (context) =>
{
const string SITEMAP_INDEX_FORMAT = @"<?xml version=""1.0"" encoding=""UTF-8""?>
<sitemapindex xmlns=""http://www.sitemaps.org/schemas/sitemap/0.9"">
<sitemap>
<loc>{0}sitemap.xml</loc>
</sitemap>
//#if(module == 'Sales')
<sitemap>
<loc>{0}products.xml</loc>
</sitemap>
//#endif
</sitemapindex>";

var baseUrl = context.Request.GetBaseUrl();

await context.Response.WriteAsync(string.Format(SITEMAP_INDEX_FORMAT, baseUrl), context.RequestAborted);
}).CacheOutput("AppResponseCachePolicy").WithTags("Sitemaps");

app.MapGet("/sitemap.xml", [AppResponseCache(SharedMaxAge = 3600 * 24 * 7)] async (context) =>
{
var urls = AssemblyLoadContext.Default.Assemblies.Where(asm => asm.GetName().Name?.Contains("Boilerplate.Client") is true)
.SelectMany(asm => asm.ExportedTypes)
.Where(att => att.GetCustomAttribute<AuthorizeAttribute>(inherit: true) is null)
.SelectMany(t => t.GetCustomAttributes<Microsoft.AspNetCore.Components.RouteAttribute>())
.Where(att => RouteRegex().IsMatch(att.Template) is false)
.Select(att => att.Template)
.Except([Urls.NotFoundPage, Urls.NotAuthorizedPage])
.ToArray();

const string siteMapHeader = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n<urlset\r\n xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\"\r\n xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\r\n xsi:schemaLocation=\"http://www.sitemaps.org/schemas/sitemap/0.9\r\n http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd\">";
urls = CultureInfoManager.MultilingualEnabled
? urls.Union(CultureInfoManager.SupportedCultures.SelectMany(sc => urls.Select(url => $"{sc.Culture.Name}{url}"))).ToArray()
: urls;

app.MapGet("/sitemap.xml", [AppResponseCache(MaxAge = 3600 * 24 * 7)] async (context) =>
{
if (siteMap is null)
{
var baseUrl = context.Request.GetBaseUrl();
var baseUrl = context.Request.GetBaseUrl();

siteMap = $"{siteMapHeader}{string.Join(Environment.NewLine, urls.Select(u => $"<url><loc>{new Uri(baseUrl, u)}</loc></url>"))}</urlset>";
}
var siteMap = @$"{siteMapHeader}
{string.Join(Environment.NewLine, urls.Select(u => $"<url><loc>{new Uri(baseUrl, u)}</loc></url>"))}
</urlset>";

context.Response.Headers.ContentType = "application/xml";

await context.Response.WriteAsync(siteMap, context.RequestAborted);
}).CacheOutput("AppResponseCachePolicy");
}).CacheOutput("AppResponseCachePolicy").WithTags("Sitemaps");

//#if(module == "Sales")
app.MapGet("/products.xml", [AppResponseCache(SharedMaxAge = 60 * 5)] async (IProductViewController controller, HttpContext context) =>
{
var baseUrl = context.Request.GetBaseUrl();
controller.AddQueryString(new ODataQuery() { Select = nameof(ProductDto.Id) });
var products = await controller.Get(context.RequestAborted);
var productsUrls = products.Select(p => $"{Urls.ProductPage}/{p.Id}").ToArray();

productsUrls = CultureInfoManager.MultilingualEnabled
? productsUrls.Union(CultureInfoManager.SupportedCultures.SelectMany(sc => productsUrls.Select(url => $"{sc.Culture.Name}{url}"))).ToArray()
: productsUrls;

var productsMap = @$"{siteMapHeader}
{string.Join(Environment.NewLine, productsUrls.Select(productUrl => $"<url><loc>{new Uri(baseUrl, productUrl)}</loc></url>"))}
</urlset>";

context.Response.Headers.ContentType = "application/xml";

await context.Response.WriteAsync(productsMap, context.RequestAborted);
}).CacheOutput("AppResponseCachePolicy").WithTags("Sitemaps");
//#endif
}

private static string? siteMap;
[GeneratedRegex(@"\{.*?\}")]
private static partial Regex RouteRegex();

/// <summary>
/// Prior to the introduction of .NET 8, the Blazor router effectively managed NotFound and NotAuthorized components during pre-rendering.
Expand Down
Loading