913 lines
38 KiB
C#
913 lines
38 KiB
C#
using System.Globalization;
|
|
using System.Text;
|
|
using System.Xml;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
|
using Microsoft.AspNetCore.Mvc.Routing;
|
|
using Nop.Core;
|
|
using Nop.Core.Caching;
|
|
using Nop.Core.Domain.Blogs;
|
|
using Nop.Core.Domain.Catalog;
|
|
using Nop.Core.Domain.Common;
|
|
using Nop.Core.Domain.Forums;
|
|
using Nop.Core.Domain.Localization;
|
|
using Nop.Core.Domain.News;
|
|
using Nop.Core.Domain.Seo;
|
|
using Nop.Core.Domain.Topics;
|
|
using Nop.Core.Events;
|
|
using Nop.Core.Infrastructure;
|
|
using Nop.Services.Blogs;
|
|
using Nop.Services.Catalog;
|
|
using Nop.Services.Customers;
|
|
using Nop.Services.Localization;
|
|
using Nop.Services.News;
|
|
using Nop.Services.Seo;
|
|
using Nop.Services.Topics;
|
|
using Nop.Web.Framework.Mvc.Routing;
|
|
using Nop.Web.Infrastructure.Cache;
|
|
using Nop.Web.Models.Sitemap;
|
|
|
|
namespace Nop.Web.Factories;
|
|
|
|
/// <summary>
|
|
/// Represents the sitemap model factory implementation
|
|
/// </summary>
|
|
public partial class SitemapModelFactory : ISitemapModelFactory
|
|
{
|
|
#region Fields
|
|
|
|
protected readonly BlogSettings _blogSettings;
|
|
protected readonly ForumSettings _forumSettings;
|
|
protected readonly IActionContextAccessor _actionContextAccessor;
|
|
protected readonly IBlogService _blogService;
|
|
protected readonly ICategoryService _categoryService;
|
|
protected readonly ICustomerService _customerService;
|
|
protected readonly IEventPublisher _eventPublisher;
|
|
protected readonly ILanguageService _languageService;
|
|
protected readonly ILocalizationService _localizationService;
|
|
protected readonly ILocker _locker;
|
|
protected readonly IManufacturerService _manufacturerService;
|
|
protected readonly INewsService _newsService;
|
|
protected readonly INopFileProvider _nopFileProvider;
|
|
protected readonly INopUrlHelper _nopUrlHelper;
|
|
protected readonly IProductService _productService;
|
|
protected readonly IProductTagService _productTagService;
|
|
protected readonly IStaticCacheManager _staticCacheManager;
|
|
protected readonly IStoreContext _storeContext;
|
|
protected readonly ITopicService _topicService;
|
|
protected readonly IUrlHelperFactory _urlHelperFactory;
|
|
protected readonly IUrlRecordService _urlRecordService;
|
|
protected readonly IWebHelper _webHelper;
|
|
protected readonly IWorkContext _workContext;
|
|
protected readonly LocalizationSettings _localizationSettings;
|
|
protected readonly NewsSettings _newsSettings;
|
|
protected readonly SitemapSettings _sitemapSettings;
|
|
protected readonly SitemapXmlSettings _sitemapXmlSettings;
|
|
|
|
#endregion
|
|
|
|
#region Ctor
|
|
|
|
public SitemapModelFactory(BlogSettings blogSettings,
|
|
ForumSettings forumSettings,
|
|
IActionContextAccessor actionContextAccessor,
|
|
IBlogService blogService,
|
|
ICategoryService categoryService,
|
|
ICustomerService customerService,
|
|
IEventPublisher eventPublisher,
|
|
ILanguageService languageService,
|
|
ILocalizationService localizationService,
|
|
ILocker locker,
|
|
IManufacturerService manufacturerService,
|
|
INewsService newsService,
|
|
INopFileProvider nopFileProvider,
|
|
INopUrlHelper nopUrlHelper,
|
|
IProductService productService,
|
|
IProductTagService productTagService,
|
|
IStaticCacheManager staticCacheManager,
|
|
IStoreContext storeContext,
|
|
ITopicService topicService,
|
|
IUrlHelperFactory urlHelperFactory,
|
|
IUrlRecordService urlRecordService,
|
|
IWebHelper webHelper,
|
|
IWorkContext workContext,
|
|
LocalizationSettings localizationSettings,
|
|
NewsSettings newsSettings,
|
|
SitemapSettings sitemapSettings,
|
|
SitemapXmlSettings sitemapXmlSettings)
|
|
{
|
|
_blogSettings = blogSettings;
|
|
_forumSettings = forumSettings;
|
|
_actionContextAccessor = actionContextAccessor;
|
|
_blogService = blogService;
|
|
_categoryService = categoryService;
|
|
_customerService = customerService;
|
|
_eventPublisher = eventPublisher;
|
|
_languageService = languageService;
|
|
_localizationService = localizationService;
|
|
_locker = locker;
|
|
_manufacturerService = manufacturerService;
|
|
_newsService = newsService;
|
|
_nopFileProvider = nopFileProvider;
|
|
_nopUrlHelper = nopUrlHelper;
|
|
_productService = productService;
|
|
_productTagService = productTagService;
|
|
_staticCacheManager = staticCacheManager;
|
|
_storeContext = storeContext;
|
|
_topicService = topicService;
|
|
_urlHelperFactory = urlHelperFactory;
|
|
_urlRecordService = urlRecordService;
|
|
_webHelper = webHelper;
|
|
_workContext = workContext;
|
|
_localizationSettings = localizationSettings;
|
|
_newsSettings = newsSettings;
|
|
_sitemapSettings = sitemapSettings;
|
|
_sitemapXmlSettings = sitemapXmlSettings;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Utilities
|
|
|
|
/// <summary>
|
|
/// Get UrlHelper
|
|
/// </summary>
|
|
/// <returns>UrlHelper</returns>
|
|
protected virtual IUrlHelper GetUrlHelper()
|
|
{
|
|
return _urlHelperFactory.GetUrlHelper(_actionContextAccessor.ActionContext);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get HTTP protocol
|
|
/// </summary>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation
|
|
/// The task result contains the protocol name as string
|
|
/// </returns>
|
|
protected virtual async Task<string> GetHttpProtocolAsync()
|
|
{
|
|
var store = await _storeContext.GetCurrentStoreAsync();
|
|
|
|
return store.SslEnabled ? Uri.UriSchemeHttps : Uri.UriSchemeHttp;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generate URLs for the sitemap
|
|
/// </summary>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation
|
|
/// The task result contains the list of sitemap URLs
|
|
/// </returns>
|
|
protected virtual async Task<IList<SitemapUrlModel>> GenerateUrlsAsync()
|
|
{
|
|
var sitemapUrls = new List<SitemapUrlModel>
|
|
{
|
|
//home page
|
|
await PrepareLocalizedSitemapUrlAsync("Homepage"),
|
|
|
|
//search products
|
|
await PrepareLocalizedSitemapUrlAsync("ProductSearch"),
|
|
|
|
//contact us
|
|
await PrepareLocalizedSitemapUrlAsync("ContactUs")
|
|
};
|
|
|
|
//news
|
|
if (_newsSettings.Enabled)
|
|
sitemapUrls.Add(await PrepareLocalizedSitemapUrlAsync("NewsArchive"));
|
|
|
|
//blog
|
|
if (_blogSettings.Enabled)
|
|
sitemapUrls.Add(await PrepareLocalizedSitemapUrlAsync("Blog"));
|
|
|
|
//forum
|
|
if (_forumSettings.ForumsEnabled)
|
|
sitemapUrls.Add(await PrepareLocalizedSitemapUrlAsync("Boards"));
|
|
|
|
//categories
|
|
if (_sitemapXmlSettings.SitemapXmlIncludeCategories)
|
|
sitemapUrls.AddRange(await GetCategoryUrlsAsync());
|
|
|
|
//manufacturers
|
|
if (_sitemapXmlSettings.SitemapXmlIncludeManufacturers)
|
|
sitemapUrls.AddRange(await GetManufacturerUrlsAsync());
|
|
|
|
//products
|
|
if (_sitemapXmlSettings.SitemapXmlIncludeProducts)
|
|
sitemapUrls.AddRange(await GetProductUrlsAsync());
|
|
|
|
//product tags
|
|
if (_sitemapXmlSettings.SitemapXmlIncludeProductTags)
|
|
sitemapUrls.AddRange(await GetProductTagUrlsAsync());
|
|
|
|
//news
|
|
if (_sitemapXmlSettings.SitemapXmlIncludeNews && _newsSettings.Enabled)
|
|
sitemapUrls.AddRange(await GetNewsItemUrlsAsync());
|
|
|
|
//blog posts
|
|
if (_sitemapXmlSettings.SitemapXmlIncludeBlogPosts && _blogSettings.Enabled)
|
|
sitemapUrls.AddRange(await GetBlogPostUrlsAsync());
|
|
|
|
//topics
|
|
if (_sitemapXmlSettings.SitemapXmlIncludeTopics)
|
|
sitemapUrls.AddRange(await GetTopicUrlsAsync());
|
|
|
|
//custom URLs
|
|
if (_sitemapXmlSettings.SitemapXmlIncludeCustomUrls)
|
|
sitemapUrls.AddRange(GetCustomUrls());
|
|
|
|
//event notification
|
|
await _eventPublisher.PublishAsync(new SitemapCreatedEvent(sitemapUrls));
|
|
|
|
return sitemapUrls;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get news item URLs for the sitemap
|
|
/// </summary>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation
|
|
/// The task result contains the sitemap URLs
|
|
/// </returns>
|
|
protected virtual async Task<IEnumerable<SitemapUrlModel>> GetNewsItemUrlsAsync()
|
|
{
|
|
var store = await _storeContext.GetCurrentStoreAsync();
|
|
|
|
return await (await _newsService.GetAllNewsAsync(storeId: store.Id))
|
|
.SelectAwait(async news => await PrepareLocalizedSitemapUrlAsync("NewsItem",
|
|
async lang => new { SeName = await _urlRecordService.GetSeNameAsync(news, news.LanguageId, ensureTwoPublishedLanguages: false) },
|
|
news.CreatedOnUtc)).ToListAsync();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get category URLs for the sitemap
|
|
/// </summary>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation
|
|
/// The task result contains the sitemap URLs
|
|
/// </returns>
|
|
protected virtual async Task<IEnumerable<SitemapUrlModel>> GetCategoryUrlsAsync()
|
|
{
|
|
var store = await _storeContext.GetCurrentStoreAsync();
|
|
|
|
return await (await _categoryService.GetAllCategoriesAsync(storeId: store.Id))
|
|
.SelectAwait(async category => await PrepareLocalizedSitemapUrlAsync("Category", GetSeoRouteParamsAwait(category), category.UpdatedOnUtc)).ToListAsync();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get manufacturer URLs for the sitemap
|
|
/// </summary>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation
|
|
/// The task result contains the sitemap URLs
|
|
/// </returns>
|
|
protected virtual async Task<IEnumerable<SitemapUrlModel>> GetManufacturerUrlsAsync()
|
|
{
|
|
var store = await _storeContext.GetCurrentStoreAsync();
|
|
|
|
return await (await _manufacturerService.GetAllManufacturersAsync(storeId: store.Id))
|
|
.SelectAwait(async manufacturer => await PrepareLocalizedSitemapUrlAsync("Manufacturer", GetSeoRouteParamsAwait(manufacturer), manufacturer.UpdatedOnUtc)).ToListAsync();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get product URLs for the sitemap
|
|
/// </summary>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation
|
|
/// The task result contains the sitemap URLs
|
|
/// </returns>
|
|
protected virtual async Task<IEnumerable<SitemapUrlModel>> GetProductUrlsAsync()
|
|
{
|
|
var store = await _storeContext.GetCurrentStoreAsync();
|
|
|
|
return await (await _productService.SearchProductsAsync(0, storeId: store.Id,
|
|
visibleIndividuallyOnly: true, orderBy: ProductSortingEnum.CreatedOn))
|
|
.SelectAwait(async product => await PrepareLocalizedSitemapUrlAsync("Product", GetSeoRouteParamsAwait(product), product.UpdatedOnUtc)).ToListAsync();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get product tag URLs for the sitemap
|
|
/// </summary>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation
|
|
/// The task result contains the sitemap URLs
|
|
/// </returns>
|
|
protected virtual async Task<IEnumerable<SitemapUrlModel>> GetProductTagUrlsAsync()
|
|
{
|
|
return await (await _productTagService.GetAllProductTagsAsync())
|
|
.SelectAwait(async productTag => await PrepareLocalizedSitemapUrlAsync("ProductsByTag", GetSeoRouteParamsAwait(productTag))).ToListAsync();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get topic URLs for the sitemap
|
|
/// </summary>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation
|
|
/// The task result contains the sitemap URLs
|
|
/// </returns>
|
|
protected virtual async Task<IEnumerable<SitemapUrlModel>> GetTopicUrlsAsync()
|
|
{
|
|
var store = await _storeContext.GetCurrentStoreAsync();
|
|
|
|
return await (await _topicService.GetAllTopicsAsync(store.Id)).Where(t => t.IncludeInSitemap)
|
|
.SelectAwait(async topic => await PrepareLocalizedSitemapUrlAsync("Topic", GetSeoRouteParamsAwait(topic))).ToListAsync();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Return localized blog post url
|
|
/// </summary>
|
|
/// <param name="post">Blog post to generate URL</param>
|
|
/// <param name="updateFreq">How often to update url</param>
|
|
/// <returns>A task that represents the asynchronous operation</returns>
|
|
protected virtual async Task<SitemapUrlModel> PrepareLocalizedBlogPostUrlModelAsync(BlogPost post, UpdateFrequency updateFreq = UpdateFrequency.Weekly)
|
|
{
|
|
//url for current language
|
|
var url = await _nopUrlHelper.RouteGenericUrlAsync<BlogPost>(new
|
|
{
|
|
SeName = await _urlRecordService.GetSeNameAsync(post, null,
|
|
ensureTwoPublishedLanguages: false)
|
|
}, await GetHttpProtocolAsync());
|
|
|
|
var updatedOn = post.CreatedOnUtc;
|
|
|
|
if (!_localizationSettings.SeoFriendlyUrlsForLanguagesEnabled)
|
|
return new SitemapUrlModel(url, new List<string>(), updateFreq, updatedOn);
|
|
|
|
var lang = await _languageService.GetLanguageByIdAsync(post.LanguageId);
|
|
|
|
if (lang == null || !lang.Published)
|
|
return new SitemapUrlModel(url, new List<string>(), updateFreq, updatedOn);
|
|
|
|
url = await _nopUrlHelper.RouteGenericUrlAsync<BlogPost>(new
|
|
{
|
|
SeName = await _urlRecordService.GetSeNameAsync(post, post.LanguageId,
|
|
ensureTwoPublishedLanguages: false)
|
|
}, await GetHttpProtocolAsync());
|
|
|
|
url = GetLocalizedUrl(url, lang);
|
|
|
|
return new SitemapUrlModel(url, new List<string>(), updateFreq, updatedOn);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get blog post URLs for the sitemap
|
|
/// </summary>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation
|
|
/// The task result contains the sitemap URLs
|
|
/// </returns>
|
|
protected virtual async Task<IEnumerable<SitemapUrlModel>> GetBlogPostUrlsAsync()
|
|
{
|
|
var store = await _storeContext.GetCurrentStoreAsync();
|
|
|
|
var urls = await (await _blogService.GetAllBlogPostsAsync(store.Id))
|
|
.Where(p => p.IncludeInSitemap)
|
|
.SelectAwait(async post =>
|
|
await PrepareLocalizedBlogPostUrlModelAsync(post)
|
|
).ToListAsync();
|
|
|
|
return urls;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get custom URLs for the sitemap
|
|
/// </summary>
|
|
/// <returns>Sitemap URLs</returns>
|
|
protected virtual IEnumerable<SitemapUrlModel> GetCustomUrls()
|
|
{
|
|
var storeLocation = _webHelper.GetStoreLocation();
|
|
|
|
return _sitemapXmlSettings.SitemapCustomUrls.Select(customUrl =>
|
|
new SitemapUrlModel(string.Concat(storeLocation, customUrl), new List<string>(), UpdateFrequency.Weekly, DateTime.UtcNow));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get route params for URL localization
|
|
/// </summary>
|
|
/// <typeparam name="T">Model type</typeparam>
|
|
/// <param name="model">Model</param>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation
|
|
/// The task result contains the lambda for route params
|
|
/// </returns>
|
|
protected virtual Func<int?, Task<object>> GetSeoRouteParamsAwait<T>(T model)
|
|
where T : BaseEntity, ISlugSupported
|
|
{
|
|
return async lang => new { SeName = await _urlRecordService.GetSeNameAsync(model, lang) };
|
|
}
|
|
|
|
/// <summary>
|
|
/// Write sitemap index file into the stream
|
|
/// </summary>
|
|
/// <param name="stream">Stream</param>
|
|
/// <param name="sitemapNumber">The number of sitemaps</param>
|
|
/// <returns>A task that represents the asynchronous operation</returns>
|
|
protected virtual async Task WriteSitemapIndexAsync(MemoryStream stream, int sitemapNumber)
|
|
{
|
|
await using var writer = XmlWriter.Create(stream, new XmlWriterSettings
|
|
{
|
|
Async = true,
|
|
Encoding = Encoding.UTF8,
|
|
Indent = true,
|
|
ConformanceLevel = ConformanceLevel.Auto
|
|
});
|
|
|
|
await writer.WriteStartDocumentAsync();
|
|
await writer.WriteStartElementAsync(prefix: null, localName: "sitemapindex", ns: "http://www.sitemaps.org/schemas/sitemap/0.9");
|
|
|
|
//write URLs of all available sitemaps
|
|
var urlHelper = GetUrlHelper();
|
|
|
|
for (var id = 1; id <= sitemapNumber; id++)
|
|
{
|
|
var url = urlHelper.RouteUrl("sitemap-indexed.xml", new { Id = id }, await GetHttpProtocolAsync());
|
|
var location = await XmlHelper.XmlEncodeAsync(url);
|
|
|
|
await writer.WriteStartElementAsync(null, "sitemap", null);
|
|
await writer.WriteElementStringAsync(null, "loc", null, location);
|
|
await writer.WriteElementStringAsync(null, "lastmod", null, DateTime.UtcNow.ToString(NopSeoDefaults.SitemapDateFormat));
|
|
await writer.WriteEndElementAsync();
|
|
}
|
|
|
|
await writer.WriteEndElementAsync();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Write sitemap file into the stream
|
|
/// </summary>
|
|
/// <param name="stream">Stream</param>
|
|
/// <param name="sitemapUrls">List of sitemap URLs</param>
|
|
/// <returns>A task that represents the asynchronous operation</returns>
|
|
protected virtual async Task WriteSitemapAsync(MemoryStream stream, IList<SitemapUrlModel> sitemapUrls)
|
|
{
|
|
await using var writer = XmlWriter.Create(stream, new XmlWriterSettings
|
|
{
|
|
Async = true,
|
|
Encoding = Encoding.UTF8,
|
|
Indent = true,
|
|
ConformanceLevel = ConformanceLevel.Auto
|
|
});
|
|
|
|
await writer.WriteStartDocumentAsync();
|
|
await writer.WriteStartElementAsync(prefix: null, localName: "urlset", ns: "http://www.sitemaps.org/schemas/sitemap/0.9");
|
|
await writer.WriteAttributeStringAsync(prefix: "xsi", "schemaLocation",
|
|
"http://www.w3.org/2001/XMLSchema-instance",
|
|
"http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd http://www.w3.org/1999/xhtml http://www.w3.org/2002/08/xhtml/xhtml1-strict.xsd");
|
|
await writer.WriteAttributeStringAsync(prefix: "xmlns", "xhtml", null, "http://www.w3.org/1999/xhtml");
|
|
|
|
//write URLs from list to the sitemap
|
|
foreach (var sitemapUrl in sitemapUrls)
|
|
{
|
|
//write base url
|
|
await WriteSitemapUrlAsync(writer, sitemapUrl);
|
|
|
|
//write all alternate url if exists
|
|
foreach (var alternate in sitemapUrl.AlternateLocations
|
|
.Where(p => !p.Equals(sitemapUrl.Location, StringComparison.InvariantCultureIgnoreCase)))
|
|
{
|
|
await WriteSitemapUrlAsync(writer, new SitemapUrlModel(alternate, sitemapUrl));
|
|
}
|
|
}
|
|
|
|
await writer.WriteEndElementAsync();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Write sitemap
|
|
/// </summary>
|
|
/// <param name="writer">XML stream writer</param>
|
|
/// <param name="sitemapUrl">Sitemap URL</param>
|
|
/// <returns>A task that represents the asynchronous operation</returns>
|
|
protected virtual async Task WriteSitemapUrlAsync(XmlWriter writer, SitemapUrlModel sitemapUrl)
|
|
{
|
|
if (string.IsNullOrEmpty(sitemapUrl.Location))
|
|
return;
|
|
|
|
await writer.WriteStartElementAsync(null, "url", null);
|
|
|
|
var loc = await XmlHelper.XmlEncodeAsync(sitemapUrl.Location);
|
|
await writer.WriteElementStringAsync(null, "loc", null, loc);
|
|
|
|
//write all related url
|
|
foreach (var alternate in sitemapUrl.AlternateLocations)
|
|
{
|
|
if (string.IsNullOrEmpty(alternate))
|
|
continue;
|
|
|
|
//extract seo code
|
|
var altLoc = await XmlHelper.XmlEncodeAsync(alternate);
|
|
var altLocPath = new Uri(altLoc).PathAndQuery;
|
|
var (_, lang) = await altLocPath.IsLocalizedUrlAsync(_actionContextAccessor.ActionContext.HttpContext.Request.PathBase, true);
|
|
|
|
if (string.IsNullOrEmpty(lang?.UniqueSeoCode))
|
|
continue;
|
|
|
|
await writer.WriteStartElementAsync("xhtml", "link", null);
|
|
await writer.WriteAttributeStringAsync(null, "rel", null, "alternate");
|
|
await writer.WriteAttributeStringAsync(null, "hreflang", null, lang.UniqueSeoCode);
|
|
await writer.WriteAttributeStringAsync(null, "href", null, altLoc);
|
|
await writer.WriteEndElementAsync();
|
|
}
|
|
|
|
await writer.WriteElementStringAsync(null, "changefreq", null, sitemapUrl.UpdateFrequency.ToString().ToLowerInvariant());
|
|
await writer.WriteElementStringAsync(null, "lastmod", null, sitemapUrl.UpdatedOn.ToString(NopSeoDefaults.SitemapDateFormat, CultureInfo.InvariantCulture));
|
|
await writer.WriteEndElementAsync();
|
|
}
|
|
|
|
/// <summary>
|
|
/// This will build an XML sitemap for better index with search engines.
|
|
/// See http://en.wikipedia.org/wiki/Sitemaps for more information.
|
|
/// </summary>
|
|
/// <param name="fullPath">The path and name of the sitemap file</param>
|
|
/// <param name="id">Sitemap identifier</param>
|
|
/// <returns>A task that represents the asynchronous operation</returns>
|
|
protected virtual async Task GenerateAsync(string fullPath, int id = 0)
|
|
{
|
|
//generate all URLs for the sitemap
|
|
var sitemapUrls = await GenerateUrlsAsync();
|
|
|
|
//split URLs into separate lists based on the max size
|
|
var numberOfParts = (int)Math.Ceiling((decimal)sitemapUrls.Sum(x => (x.AlternateLocations?.Count ?? 0) + 1) / NopSeoDefaults.SitemapMaxUrlNumber);
|
|
|
|
var sitemaps = sitemapUrls
|
|
.Select((url, index) => new { Index = index, Value = url })
|
|
.GroupBy(group => group.Index % numberOfParts)
|
|
.Select(group => group
|
|
.Select(url => url.Value)
|
|
.ToList()).ToList();
|
|
|
|
if (!sitemaps.Any())
|
|
return;
|
|
|
|
await using var stream = new MemoryStream();
|
|
|
|
if (id > 0)
|
|
{
|
|
//requested sitemap does not exist
|
|
if (id > sitemaps.Count)
|
|
return;
|
|
|
|
//otherwise write a certain numbered sitemap file into the stream
|
|
await WriteSitemapAsync(stream, sitemaps.ElementAt(id - 1));
|
|
}
|
|
else
|
|
{
|
|
//URLs more than the maximum allowable, so generate a sitemap index file
|
|
if (numberOfParts > 1)
|
|
{
|
|
//write a sitemap index file into the stream
|
|
await WriteSitemapIndexAsync(stream, sitemaps.Count);
|
|
}
|
|
else
|
|
{
|
|
//otherwise generate a standard sitemap
|
|
await WriteSitemapAsync(stream, sitemaps.First());
|
|
}
|
|
}
|
|
|
|
if (_nopFileProvider.FileExists(fullPath))
|
|
_nopFileProvider.DeleteFile(fullPath);
|
|
|
|
|
|
using var fileStream = _nopFileProvider.GetOrCreateFile(fullPath);
|
|
stream.Position = 0;
|
|
await stream.CopyToAsync(fileStream, 81920);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets localized URL with SEO code
|
|
/// </summary>
|
|
/// <param name="currentUrl">URL to add SEO code</param>
|
|
/// <param name="lang">Language for localization</param>
|
|
/// <returns>Localized URL with SEO code</returns>
|
|
protected string GetLocalizedUrl(string currentUrl, Language lang)
|
|
{
|
|
if (string.IsNullOrEmpty(currentUrl))
|
|
return null;
|
|
|
|
if (_actionContextAccessor.ActionContext == null)
|
|
return null;
|
|
|
|
var pathBase = _actionContextAccessor.ActionContext.HttpContext.Request.PathBase;
|
|
|
|
//Extract server and path from url
|
|
var scheme = new Uri(currentUrl).GetComponents(UriComponents.SchemeAndServer, UriFormat.Unescaped);
|
|
var path = new Uri(currentUrl).PathAndQuery;
|
|
|
|
//Replace seo code
|
|
var localizedPath = path
|
|
.RemoveLanguageSeoCodeFromUrl(pathBase, true)
|
|
.AddLanguageSeoCodeToUrl(pathBase, true, lang);
|
|
|
|
return new Uri(new Uri(scheme), localizedPath).ToString();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Methods
|
|
|
|
/// <summary>
|
|
/// Prepare the sitemap model
|
|
/// </summary>
|
|
/// <param name="pageModel">Sitemap page model</param>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation
|
|
/// The task result contains the sitemap model
|
|
/// </returns>
|
|
public virtual async Task<SitemapModel> PrepareSitemapModelAsync(SitemapPageModel pageModel)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(pageModel);
|
|
|
|
var language = await _workContext.GetWorkingLanguageAsync();
|
|
var customer = await _workContext.GetCurrentCustomerAsync();
|
|
var customerRoleIds = await _customerService.GetCustomerRoleIdsAsync(customer);
|
|
var store = await _storeContext.GetCurrentStoreAsync();
|
|
var cacheKey = _staticCacheManager.PrepareKeyForDefaultCache(NopModelCacheDefaults.SitemapPageModelKey,
|
|
language, customerRoleIds, store);
|
|
|
|
var cachedModel = await _staticCacheManager.GetAsync(cacheKey, async () =>
|
|
{
|
|
//get URL helper
|
|
var urlHelper = GetUrlHelper();
|
|
|
|
var model = new SitemapModel();
|
|
|
|
//prepare common items
|
|
var commonGroupTitle = await _localizationService.GetResourceAsync("Sitemap.General");
|
|
|
|
//home page
|
|
model.Items.Add(new SitemapModel.SitemapItemModel
|
|
{
|
|
GroupTitle = commonGroupTitle,
|
|
Name = await _localizationService.GetResourceAsync("Homepage"),
|
|
Url = urlHelper.RouteUrl("Homepage")
|
|
});
|
|
|
|
//search
|
|
model.Items.Add(new SitemapModel.SitemapItemModel
|
|
{
|
|
GroupTitle = commonGroupTitle,
|
|
Name = await _localizationService.GetResourceAsync("Search"),
|
|
Url = urlHelper.RouteUrl("ProductSearch")
|
|
});
|
|
|
|
//news
|
|
if (_newsSettings.Enabled)
|
|
{
|
|
model.Items.Add(new SitemapModel.SitemapItemModel
|
|
{
|
|
GroupTitle = commonGroupTitle,
|
|
Name = await _localizationService.GetResourceAsync("News"),
|
|
Url = urlHelper.RouteUrl("NewsArchive")
|
|
});
|
|
}
|
|
|
|
//blog
|
|
if (_blogSettings.Enabled)
|
|
{
|
|
model.Items.Add(new SitemapModel.SitemapItemModel
|
|
{
|
|
GroupTitle = commonGroupTitle,
|
|
Name = await _localizationService.GetResourceAsync("Blog"),
|
|
Url = urlHelper.RouteUrl("Blog")
|
|
});
|
|
}
|
|
|
|
//forums
|
|
if (_forumSettings.ForumsEnabled)
|
|
{
|
|
model.Items.Add(new SitemapModel.SitemapItemModel
|
|
{
|
|
GroupTitle = commonGroupTitle,
|
|
Name = await _localizationService.GetResourceAsync("Forum.Forums"),
|
|
Url = urlHelper.RouteUrl("Boards")
|
|
});
|
|
}
|
|
|
|
//contact us
|
|
model.Items.Add(new SitemapModel.SitemapItemModel
|
|
{
|
|
GroupTitle = commonGroupTitle,
|
|
Name = await _localizationService.GetResourceAsync("ContactUs"),
|
|
Url = urlHelper.RouteUrl("ContactUs")
|
|
});
|
|
|
|
//customer info
|
|
model.Items.Add(new SitemapModel.SitemapItemModel
|
|
{
|
|
GroupTitle = commonGroupTitle,
|
|
Name = await _localizationService.GetResourceAsync("Account.MyAccount"),
|
|
Url = urlHelper.RouteUrl("CustomerInfo")
|
|
});
|
|
|
|
//at the moment topics are in general category too
|
|
if (_sitemapSettings.SitemapIncludeTopics)
|
|
{
|
|
var topics = (await _topicService.GetAllTopicsAsync(storeId: store.Id))
|
|
.Where(topic => topic.IncludeInSitemap);
|
|
|
|
model.Items.AddRange(await topics.SelectAwait(async topic => new SitemapModel.SitemapItemModel
|
|
{
|
|
GroupTitle = commonGroupTitle,
|
|
Name = await _localizationService.GetLocalizedAsync(topic, x => x.Title),
|
|
Url = await _nopUrlHelper.RouteGenericUrlAsync<Topic>(new { SeName = await _urlRecordService.GetSeNameAsync(topic) })
|
|
}).ToListAsync());
|
|
}
|
|
|
|
//blog posts
|
|
if (_sitemapSettings.SitemapIncludeBlogPosts && _blogSettings.Enabled)
|
|
{
|
|
var blogPostsGroupTitle = await _localizationService.GetResourceAsync("Sitemap.BlogPosts");
|
|
var blogPosts = (await _blogService.GetAllBlogPostsAsync(storeId: store.Id))
|
|
.Where(p => p.IncludeInSitemap);
|
|
|
|
model.Items.AddRange(await blogPosts.SelectAwait(async post => new SitemapModel.SitemapItemModel
|
|
{
|
|
GroupTitle = blogPostsGroupTitle,
|
|
Name = post.Title,
|
|
Url = await _nopUrlHelper
|
|
.RouteGenericUrlAsync<BlogPost>(new { SeName = await _urlRecordService.GetSeNameAsync(post, post.LanguageId, ensureTwoPublishedLanguages: false) })
|
|
}).ToListAsync());
|
|
}
|
|
|
|
//news
|
|
if (_sitemapSettings.SitemapIncludeNews && _newsSettings.Enabled)
|
|
{
|
|
var newsGroupTitle = await _localizationService.GetResourceAsync("Sitemap.News");
|
|
var news = await _newsService.GetAllNewsAsync(storeId: store.Id);
|
|
model.Items.AddRange(await news.SelectAwait(async newsItem => new SitemapModel.SitemapItemModel
|
|
{
|
|
GroupTitle = newsGroupTitle,
|
|
Name = newsItem.Title,
|
|
Url = await _nopUrlHelper
|
|
.RouteGenericUrlAsync<NewsItem>(new { SeName = await _urlRecordService.GetSeNameAsync(newsItem, newsItem.LanguageId, ensureTwoPublishedLanguages: false) })
|
|
}).ToListAsync());
|
|
}
|
|
|
|
//categories
|
|
if (_sitemapSettings.SitemapIncludeCategories)
|
|
{
|
|
var categoriesGroupTitle = await _localizationService.GetResourceAsync("Sitemap.Categories");
|
|
var categories = await _categoryService.GetAllCategoriesAsync(storeId: store.Id);
|
|
model.Items.AddRange(await categories.SelectAwait(async category => new SitemapModel.SitemapItemModel
|
|
{
|
|
GroupTitle = categoriesGroupTitle,
|
|
Name = await _localizationService.GetLocalizedAsync(category, x => x.Name),
|
|
Url = await _nopUrlHelper.RouteGenericUrlAsync<Category>(new { SeName = await _urlRecordService.GetSeNameAsync(category) })
|
|
}).ToListAsync());
|
|
}
|
|
|
|
//manufacturers
|
|
if (_sitemapSettings.SitemapIncludeManufacturers)
|
|
{
|
|
var manufacturersGroupTitle = await _localizationService.GetResourceAsync("Sitemap.Manufacturers");
|
|
var manufacturers = await _manufacturerService.GetAllManufacturersAsync(storeId: store.Id);
|
|
model.Items.AddRange(await manufacturers.SelectAwait(async manufacturer => new SitemapModel.SitemapItemModel
|
|
{
|
|
GroupTitle = manufacturersGroupTitle,
|
|
Name = await _localizationService.GetLocalizedAsync(manufacturer, x => x.Name),
|
|
Url = await _nopUrlHelper.RouteGenericUrlAsync<Manufacturer>(new { SeName = await _urlRecordService.GetSeNameAsync(manufacturer) })
|
|
}).ToListAsync());
|
|
}
|
|
|
|
//products
|
|
if (_sitemapSettings.SitemapIncludeProducts)
|
|
{
|
|
var productsGroupTitle = await _localizationService.GetResourceAsync("Sitemap.Products");
|
|
var products = await _productService.SearchProductsAsync(0, storeId: store.Id, visibleIndividuallyOnly: true);
|
|
model.Items.AddRange(await products.SelectAwait(async product => new SitemapModel.SitemapItemModel
|
|
{
|
|
GroupTitle = productsGroupTitle,
|
|
Name = await _localizationService.GetLocalizedAsync(product, x => x.Name),
|
|
Url = await _nopUrlHelper.RouteGenericUrlAsync<Product>(new { SeName = await _urlRecordService.GetSeNameAsync(product) })
|
|
}).ToListAsync());
|
|
}
|
|
|
|
//product tags
|
|
if (_sitemapSettings.SitemapIncludeProductTags)
|
|
{
|
|
var productTagsGroupTitle = await _localizationService.GetResourceAsync("Sitemap.ProductTags");
|
|
var productTags = await _productTagService.GetAllProductTagsAsync();
|
|
model.Items.AddRange(await productTags.SelectAwait(async productTag => new SitemapModel.SitemapItemModel
|
|
{
|
|
GroupTitle = productTagsGroupTitle,
|
|
Name = await _localizationService.GetLocalizedAsync(productTag, x => x.Name),
|
|
Url = await _nopUrlHelper.RouteGenericUrlAsync<ProductTag>(new { SeName = await _urlRecordService.GetSeNameAsync(productTag) })
|
|
}).ToListAsync());
|
|
}
|
|
|
|
return model;
|
|
});
|
|
|
|
//prepare model with pagination
|
|
pageModel.PageSize = Math.Max(pageModel.PageSize, _sitemapSettings.SitemapPageSize);
|
|
pageModel.PageNumber = Math.Max(pageModel.PageNumber, 1);
|
|
|
|
var pagedItems = new PagedList<SitemapModel.SitemapItemModel>(cachedModel.Items, pageModel.PageNumber - 1, pageModel.PageSize);
|
|
var sitemapModel = new SitemapModel { Items = pagedItems };
|
|
sitemapModel.PageModel.LoadPagedList(pagedItems);
|
|
|
|
return sitemapModel;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Prepare sitemap model.
|
|
/// This will build an XML sitemap for better index with search engines.
|
|
/// See http://en.wikipedia.org/wiki/Sitemaps for more information.
|
|
/// </summary>
|
|
/// <param name="id">Sitemap identifier</param>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation
|
|
/// The task result contains the sitemap model with sitemap.xml as string
|
|
/// </returns>
|
|
public virtual async Task<SitemapXmlModel> PrepareSitemapXmlModelAsync(int id = 0)
|
|
{
|
|
var language = await _workContext.GetWorkingLanguageAsync();
|
|
var store = await _storeContext.GetCurrentStoreAsync();
|
|
|
|
var fileName = string.Format(NopSeoDefaults.SitemapXmlFilePattern, store.Id, language.Id, id);
|
|
var fullPath = _nopFileProvider.GetAbsolutePath(NopSeoDefaults.SitemapXmlDirectory, fileName);
|
|
|
|
if (_nopFileProvider.FileExists(fullPath) && _nopFileProvider.GetLastWriteTimeUtc(fullPath) > DateTime.UtcNow.AddHours(-_sitemapXmlSettings.RebuildSitemapXmlAfterHours))
|
|
{
|
|
return new SitemapXmlModel { SitemapXmlPath = fullPath };
|
|
}
|
|
|
|
//execute task with lock
|
|
if (!await _locker.PerformActionWithLockAsync(
|
|
fullPath,
|
|
TimeSpan.FromSeconds(_sitemapXmlSettings.SitemapBuildOperationDelay),
|
|
async () => await GenerateAsync(fullPath, id)))
|
|
{
|
|
throw new InvalidOperationException();
|
|
}
|
|
|
|
return new SitemapXmlModel { SitemapXmlPath = fullPath };
|
|
}
|
|
|
|
/// <summary>
|
|
/// Return localized urls
|
|
/// </summary>
|
|
/// <param name="routeName">Route name</param>
|
|
/// <param name="getRouteParamsAwait">Lambda for route params object</param>
|
|
/// <param name="dateTimeUpdatedOn">A time when URL was updated last time</param>
|
|
/// <param name="updateFreq">How often to update url</param>
|
|
/// <returns>A task that represents the asynchronous operation</returns>
|
|
public virtual async Task<SitemapUrlModel> PrepareLocalizedSitemapUrlAsync(string routeName,
|
|
Func<int?, Task<object>> getRouteParamsAwait = null,
|
|
DateTime? dateTimeUpdatedOn = null,
|
|
UpdateFrequency updateFreq = UpdateFrequency.Weekly)
|
|
{
|
|
async Task<string> routeUrlAsync(string routeName, object values, string protocol) => routeName switch
|
|
{
|
|
var name when name.Equals(nameof(Product), StringComparison.InvariantCultureIgnoreCase)
|
|
=> await _nopUrlHelper.RouteGenericUrlAsync<Product>(values, protocol),
|
|
var name when name.Equals(nameof(Category), StringComparison.InvariantCultureIgnoreCase)
|
|
=> await _nopUrlHelper.RouteGenericUrlAsync<Category>(values, protocol),
|
|
var name when name.Equals(nameof(Manufacturer), StringComparison.InvariantCultureIgnoreCase)
|
|
=> await _nopUrlHelper.RouteGenericUrlAsync<Manufacturer>(values, protocol),
|
|
var name when name.Equals(nameof(BlogPost), StringComparison.InvariantCultureIgnoreCase)
|
|
=> await _nopUrlHelper.RouteGenericUrlAsync<BlogPost>(values, protocol),
|
|
var name when name.Equals(nameof(NewsItem), StringComparison.InvariantCultureIgnoreCase)
|
|
=> await _nopUrlHelper.RouteGenericUrlAsync<NewsItem>(values, protocol),
|
|
var name when name.Equals(nameof(Topic), StringComparison.InvariantCultureIgnoreCase)
|
|
=> await _nopUrlHelper.RouteGenericUrlAsync<Topic>(values, protocol),
|
|
var name when name.Equals(nameof(ProductTag), StringComparison.InvariantCultureIgnoreCase)
|
|
=> await _nopUrlHelper.RouteGenericUrlAsync<ProductTag>(values, protocol),
|
|
_ => GetUrlHelper().RouteUrl(routeName, values, protocol)
|
|
};
|
|
|
|
//url for current language
|
|
var url = await routeUrlAsync(routeName,
|
|
getRouteParamsAwait != null ? await getRouteParamsAwait(null) : null,
|
|
await GetHttpProtocolAsync());
|
|
|
|
var store = await _storeContext.GetCurrentStoreAsync();
|
|
|
|
var updatedOn = dateTimeUpdatedOn ?? DateTime.UtcNow;
|
|
var languages = _localizationSettings.SeoFriendlyUrlsForLanguagesEnabled
|
|
? await _languageService.GetAllLanguagesAsync(storeId: store.Id)
|
|
: null;
|
|
|
|
if (languages == null || languages.Count == 1)
|
|
return new SitemapUrlModel(url, new List<string>(), updateFreq, updatedOn);
|
|
|
|
//return list of localized urls
|
|
var localizedUrls = await languages
|
|
.SelectAwait(async lang =>
|
|
{
|
|
var currentUrl = await routeUrlAsync(routeName,
|
|
getRouteParamsAwait != null ? await getRouteParamsAwait(lang.Id) : null,
|
|
await GetHttpProtocolAsync());
|
|
|
|
return GetLocalizedUrl(currentUrl, lang);
|
|
})
|
|
.Where(value => !string.IsNullOrEmpty(value))
|
|
.ToListAsync();
|
|
|
|
return new SitemapUrlModel(url, localizedUrls, updateFreq, updatedOn);
|
|
}
|
|
|
|
#endregion
|
|
} |