diff --git a/Directory.Build.props b/Directory.Build.props
index 08749c48..09fa8d1d 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -7,7 +7,7 @@
VirtoCommerce
- 6.23.0
+ 6.24.0
$(VersionSuffix)-$(BuildNumber)
diff --git a/VirtoCommerce.Storefront.Model/ISeoInfoService.cs b/VirtoCommerce.Storefront.Model/ISeoInfoService.cs
index ee11239e..b2968cde 100644
--- a/VirtoCommerce.Storefront.Model/ISeoInfoService.cs
+++ b/VirtoCommerce.Storefront.Model/ISeoInfoService.cs
@@ -1,4 +1,5 @@
using System.Threading.Tasks;
+using VirtoCommerce.Storefront.Model.StaticContent;
using VirtoCommerce.Storefront.Model.Stores;
namespace VirtoCommerce.Storefront.Model
@@ -8,5 +9,7 @@ public interface ISeoInfoService
Task GetSeoInfosBySlug(string slug);
Task GetBestMatchingSeoInfos(string slug, Store store, string currentCulture);
+
+ ContentItem GetContentItem(string slug, WorkContext context);
}
}
diff --git a/VirtoCommerce.Storefront.Model/ISpaRouteService.cs b/VirtoCommerce.Storefront.Model/ISpaRouteService.cs
new file mode 100644
index 00000000..99269d2c
--- /dev/null
+++ b/VirtoCommerce.Storefront.Model/ISpaRouteService.cs
@@ -0,0 +1,9 @@
+using System.Threading.Tasks;
+
+namespace VirtoCommerce.Storefront.Model
+{
+ public interface ISpaRouteService
+ {
+ Task IsSpaRoute(string route);
+ }
+}
diff --git a/VirtoCommerce.Storefront/Controllers/Api/ApiCommonController.cs b/VirtoCommerce.Storefront/Controllers/Api/ApiCommonController.cs
index 56926462..5e0dc391 100644
--- a/VirtoCommerce.Storefront/Controllers/Api/ApiCommonController.cs
+++ b/VirtoCommerce.Storefront/Controllers/Api/ApiCommonController.cs
@@ -89,8 +89,8 @@ public async Task GetInfoBySlugAsync(string slug, [FromQuery] st
);
var page = pages.FirstOrDefault(x => x.Language.CultureName.EqualsInvariant(culture))
- ?? pages.FirstOrDefault(x => x.Language.IsInvariant)
- ?? pages.FirstOrDefault(x => x.AliasesUrls.Contains(pageUrl, StringComparer.OrdinalIgnoreCase));
+ ?? pages.FirstOrDefault(x => x.Language.IsInvariant)
+ ?? pages.FirstOrDefault(x => x.AliasesUrls.Contains(pageUrl, StringComparer.OrdinalIgnoreCase));
result.ContentItem = page;
}
diff --git a/VirtoCommerce.Storefront/Controllers/ErrorController.cs b/VirtoCommerce.Storefront/Controllers/ErrorController.cs
index 01c6c939..ff7092eb 100644
--- a/VirtoCommerce.Storefront/Controllers/ErrorController.cs
+++ b/VirtoCommerce.Storefront/Controllers/ErrorController.cs
@@ -1,28 +1,70 @@
+using System.Linq;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using VirtoCommerce.Storefront.Infrastructure;
using VirtoCommerce.Storefront.Model;
+using VirtoCommerce.Storefront.Model.Common;
using VirtoCommerce.Storefront.Model.Common.Exceptions;
namespace VirtoCommerce.Storefront.Controllers
{
[StorefrontRoute("error")]
- public class ErrorController : Controller
+ public class ErrorController : StorefrontControllerBase
{
- private readonly IWorkContextAccessor _workContextAccessor;
- public ErrorController(IWorkContextAccessor workContextAccessor)
+ private readonly ISeoInfoService _seoInfoService;
+ private readonly ISpaRouteService _spaRouteService;
+
+ public ErrorController(
+ IWorkContextAccessor workContextAccessor,
+ IStorefrontUrlBuilder urlBuilder,
+ ISeoInfoService seoInfoService,
+ ISpaRouteService spaRouteService)
+ : base(workContextAccessor, urlBuilder)
{
- _workContextAccessor = workContextAccessor;
+ _seoInfoService = seoInfoService;
+ _spaRouteService = spaRouteService;
}
[Route("{errCode}")]
- public IActionResult Error(int? errCode)
+ public async Task Error(int? errCode)
{
//Returns index page on 404 error when the store.IsSpa flag is activated
- if (errCode == StatusCodes.Status404NotFound && _workContextAccessor.WorkContext.CurrentStore.IsSpa)
+ if (errCode == StatusCodes.Status404NotFound && WorkContext.CurrentStore.IsSpa)
{
- Response.StatusCode = StatusCodes.Status200OK;
+ var path = TrimTwoLetterLangSegment(Request.HttpContext.Features.Get()?.OriginalPath);
+ Response.StatusCode = StatusCodes.Status404NotFound;
+
+ if (path == "/")
+ {
+ Response.StatusCode = StatusCodes.Status200OK;
+ return View("index");
+ }
+
+ if (string.IsNullOrEmpty(path))
+ {
+ return View("index");
+ }
+
+ var slug = path.Split('/').Last();
+ if (!string.IsNullOrEmpty(slug))
+ {
+ var seoInfos = await _seoInfoService.GetBestMatchingSeoInfos(slug, WorkContext.CurrentStore, WorkContext.CurrentLanguage.CultureName);
+ Response.StatusCode = seoInfos.Any() ? StatusCodes.Status200OK : StatusCodes.Status404NotFound;
+ }
+
+ if (Response.StatusCode == StatusCodes.Status404NotFound)
+ {
+ Response.StatusCode = _seoInfoService.GetContentItem($"/{slug}", WorkContext) != null ? StatusCodes.Status200OK : StatusCodes.Status404NotFound;
+ }
+
+ if (Response.StatusCode == StatusCodes.Status404NotFound)
+ {
+ Response.StatusCode = await _spaRouteService.IsSpaRoute(path) ? StatusCodes.Status200OK : StatusCodes.Status404NotFound;
+ }
+
return View("index");
}
var exceptionFeature = base.HttpContext.Features.Get();
@@ -43,5 +85,17 @@ public IActionResult AccessDenied()
Response.StatusCode = StatusCodes.Status403Forbidden;
return View("AccessDenied");
}
+
+ private string TrimTwoLetterLangSegment(string path)
+ {
+ var language = WorkContext.CurrentStore.Languages.FirstOrDefault(x => Regex.IsMatch(path, @"^/\b" + x.TwoLetterLanguageName + @"\b/", RegexOptions.IgnoreCase));
+
+ if (language != null)
+ {
+ path = Regex.Replace(path, @"/\b" + language.TwoLetterLanguageName + @"\b/", "/", RegexOptions.IgnoreCase);
+ }
+
+ return path;
+ }
}
}
diff --git a/VirtoCommerce.Storefront/Domain/SeoInfoService.cs b/VirtoCommerce.Storefront/Domain/SeoInfoService.cs
new file mode 100644
index 00000000..99e4b8a2
--- /dev/null
+++ b/VirtoCommerce.Storefront/Domain/SeoInfoService.cs
@@ -0,0 +1,61 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using VirtoCommerce.Storefront.AutoRestClients.CoreModuleApi;
+using VirtoCommerce.Storefront.Common;
+using VirtoCommerce.Storefront.Model;
+using VirtoCommerce.Storefront.Model.Common;
+using VirtoCommerce.Storefront.Model.StaticContent;
+using VirtoCommerce.Storefront.Model.Stores;
+
+namespace VirtoCommerce.Storefront.Domain
+{
+ public class SeoInfoService : ISeoInfoService
+ {
+ private readonly ICommerce _coreModuleApi;
+
+ public SeoInfoService(ICommerce coreModuleApi)
+ {
+ _coreModuleApi = coreModuleApi;
+ }
+
+ public async Task GetSeoInfosBySlug(string slug)
+ {
+ var result = (await _coreModuleApi.GetSeoInfoBySlugAsync(slug)).Select(x => x.ToSeoInfo()).ToArray();
+
+ return result;
+ }
+
+ public async Task GetBestMatchingSeoInfos(string slug, Store store, string currentCulture)
+ {
+ var result = (await _coreModuleApi.GetSeoInfoBySlugAsync(slug)).GetBestMatchingSeoInfos(store, currentCulture, slug).Select(x => x.ToSeoInfo()).ToArray();
+
+ return result;
+ }
+
+ public ContentItem GetContentItem(string slug, WorkContext context)
+ {
+ ContentItem result = null;
+ var pageUrl = slug == "__index__home__page__" ? "/" : $"/{slug}";
+ try
+ {
+ var pages = context.Pages.Where(p =>
+ string.Equals(p.Url, pageUrl, StringComparison.OrdinalIgnoreCase)
+ || string.Equals(p.Url, slug, StringComparison.OrdinalIgnoreCase)
+ );
+
+ var page = pages.FirstOrDefault(x => x.Language.CultureName.EqualsInvariant(context.CurrentLanguage.CultureName))
+ ?? pages.FirstOrDefault(x => x.Language.IsInvariant)
+ ?? pages.FirstOrDefault(x => x.AliasesUrls.Contains(pageUrl, StringComparer.OrdinalIgnoreCase));
+ result = page;
+
+ }
+ catch
+ {
+ //do nothing
+ }
+
+ return result;
+ }
+ }
+}
diff --git a/VirtoCommerce.Storefront/Domain/SeoInfoServise.cs b/VirtoCommerce.Storefront/Domain/SeoInfoServise.cs
deleted file mode 100644
index e5099eb4..00000000
--- a/VirtoCommerce.Storefront/Domain/SeoInfoServise.cs
+++ /dev/null
@@ -1,32 +0,0 @@
-using System.Linq;
-using System.Threading.Tasks;
-using VirtoCommerce.Storefront.AutoRestClients.CoreModuleApi;
-using VirtoCommerce.Storefront.Common;
-using VirtoCommerce.Storefront.Model;
-using VirtoCommerce.Storefront.Model.Stores;
-
-namespace VirtoCommerce.Storefront.Domain
-{
- public class SeoInfoServise : ISeoInfoService
- {
- private readonly ICommerce _coreModuleApi;
- public SeoInfoServise(ICommerce coreModuleApi)
- {
- _coreModuleApi = coreModuleApi;
- }
-
- public async Task GetSeoInfosBySlug(string slug)
- {
- var result = (await _coreModuleApi.GetSeoInfoBySlugAsync(slug)).Select(x => x.ToSeoInfo()).ToArray();
-
- return result;
- }
-
- public async Task GetBestMatchingSeoInfos(string slug, Store store, string currentCulture)
- {
- var result = (await _coreModuleApi.GetSeoInfoBySlugAsync(slug)).GetBestMatchingSeoInfos(store, currentCulture, slug).Select(x => x.ToSeoInfo()).ToArray();
-
- return result;
- }
- }
-}
diff --git a/VirtoCommerce.Storefront/Domain/SpaRouteService.cs b/VirtoCommerce.Storefront/Domain/SpaRouteService.cs
new file mode 100644
index 00000000..ab6730ca
--- /dev/null
+++ b/VirtoCommerce.Storefront/Domain/SpaRouteService.cs
@@ -0,0 +1,79 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using Newtonsoft.Json;
+using VirtoCommerce.Storefront.Model;
+using VirtoCommerce.Storefront.Model.Caching;
+using VirtoCommerce.Storefront.Model.Common;
+using VirtoCommerce.Storefront.Model.Common.Caching;
+using VirtoCommerce.Storefront.Model.StaticContent;
+
+namespace VirtoCommerce.Storefront.Domain
+{
+ public class SpaRouteService : ISpaRouteService
+ {
+ private readonly IContentBlobProvider _contentBlobProvider;
+ private readonly IStorefrontMemoryCache _memoryCache;
+ private readonly IWorkContextAccessor _workContextAccessor;
+
+ public SpaRouteService(
+ IContentBlobProvider contentBlobProvider,
+ IStorefrontMemoryCache memoryCache,
+ IWorkContextAccessor workContextAccessor)
+ {
+ _contentBlobProvider = contentBlobProvider;
+ _memoryCache = memoryCache;
+ _workContextAccessor = workContextAccessor;
+ }
+
+ public virtual async Task IsSpaRoute(string route)
+ {
+ var workContext = _workContextAccessor.WorkContext;
+ var cacheKey = CacheKey.With(GetType(), nameof(IsSpaRoute), workContext.CurrentStore.Id, route);
+ var result = await _memoryCache.GetOrCreateExclusiveAsync(cacheKey, async (_) =>
+ {
+ var routes = await GetSpaRoutes();
+
+ var isSpaRoute = routes.Any(jsPattern =>
+ {
+ // Input sample: jsPattern = "/^\\/account\\/profile\\/?$/i"
+ // Only the char "i" can be an ending. The others chars are not used
+ // when generating RegExp patterns in the `routes.json` file.
+ var options = jsPattern.EndsWith("i") ? RegexOptions.IgnoreCase : RegexOptions.None;
+ var pattern = Regex.Replace(jsPattern, @"^\/|\/i?$", string.Empty);
+
+ return Regex.IsMatch(route, pattern, options);
+ });
+
+ return isSpaRoute;
+ });
+
+ return result;
+ }
+
+ protected virtual async Task> GetSpaRoutes()
+ {
+ var workContext = _workContextAccessor.WorkContext;
+ var cacheKey = CacheKey.With(GetType(), nameof(GetSpaRoutes), workContext.CurrentStore.Id);
+ var routes = await _memoryCache.GetOrCreateExclusiveAsync(cacheKey, async (_) =>
+ {
+ var result = new List();
+ var currentThemeName = !string.IsNullOrEmpty(workContext.CurrentStore.ThemeName) ? workContext.CurrentStore.ThemeName : "default";
+ var currentThemePath = Path.Combine("Themes", workContext.CurrentStore.Id, currentThemeName);
+ var currentThemeSettingPath = Path.Combine(currentThemePath, "config", "routes.json");
+
+ if (_contentBlobProvider.PathExists(currentThemeSettingPath))
+ {
+ await using var stream = _contentBlobProvider.OpenRead(currentThemeSettingPath);
+ result = JsonConvert.DeserializeObject>(await stream.ReadToStringAsync());
+ }
+
+ return result;
+ });
+
+ return routes;
+ }
+ }
+}
diff --git a/VirtoCommerce.Storefront/Startup.cs b/VirtoCommerce.Storefront/Startup.cs
index e45956f3..d109dbec 100644
--- a/VirtoCommerce.Storefront/Startup.cs
+++ b/VirtoCommerce.Storefront/Startup.cs
@@ -93,7 +93,8 @@ public void ConfigureServices(IServiceCollection services)
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
- services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
services.AddSingleton();
services.AddSingleton();