Skip to content

Commit

Permalink
Sitemaps module (#4450)
Browse files Browse the repository at this point in the history
  • Loading branch information
deanmarcussen authored Feb 15, 2020
1 parent 8c5a86f commit a5e11d3
Show file tree
Hide file tree
Showing 124 changed files with 5,583 additions and 0 deletions.
14 changes: 14 additions & 0 deletions OrchardCore.sln
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCore.Media.Core", "s
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCore.Benchmarks", "test\OrchardCore.Benchmarks\OrchardCore.Benchmarks.csproj", "{85610AB6-07E8-45E3-9C48-34D952CC5DD2}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCore.Sitemaps.Abstractions", "src\OrchardCore\OrchardCore.Sitemaps.Abstractions\OrchardCore.Sitemaps.Abstractions.csproj", "{5899FB03-AB3F-4113-A3F0-59096B48FA37}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCore.Sitemaps", "src\OrchardCore.Modules\OrchardCore.Sitemaps\OrchardCore.Sitemaps.csproj", "{D87557AF-668B-4A0F-B079-FA35F71D1C56}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCore.Contents.TagHelpers", "src\OrchardCore\OrchardCore.Contents.TagHelpers\OrchardCore.Contents.TagHelpers.csproj", "{6236734E-507B-461B-8E92-068886058E84}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCore.Search.Abstractions", "src\OrchardCore\OrchardCore.Search.Abstractions\OrchardCore.Search.Abstractions.csproj", "{5283A8BC-DFF4-436D-AA9C-EE2DBFC5D51A}"
Expand Down Expand Up @@ -960,6 +964,14 @@ Global
{85610AB6-07E8-45E3-9C48-34D952CC5DD2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{85610AB6-07E8-45E3-9C48-34D952CC5DD2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{85610AB6-07E8-45E3-9C48-34D952CC5DD2}.Release|Any CPU.Build.0 = Release|Any CPU
{5899FB03-AB3F-4113-A3F0-59096B48FA37}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5899FB03-AB3F-4113-A3F0-59096B48FA37}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5899FB03-AB3F-4113-A3F0-59096B48FA37}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5899FB03-AB3F-4113-A3F0-59096B48FA37}.Release|Any CPU.Build.0 = Release|Any CPU
{D87557AF-668B-4A0F-B079-FA35F71D1C56}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D87557AF-668B-4A0F-B079-FA35F71D1C56}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D87557AF-668B-4A0F-B079-FA35F71D1C56}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D87557AF-668B-4A0F-B079-FA35F71D1C56}.Release|Any CPU.Build.0 = Release|Any CPU
{6236734E-507B-461B-8E92-068886058E84}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6236734E-507B-461B-8E92-068886058E84}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6236734E-507B-461B-8E92-068886058E84}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand Down Expand Up @@ -1136,6 +1148,8 @@ Global
{AEDB2C9D-938E-484A-8DD8-7429E2BBB2A2} = {3398AB60-2DB7-464C-B211-C4120BE75582}
{8BC282D3-B7AF-495A-A5B1-D175875EB90E} = {F23AC6C2-DE44-4699-999D-3C478EF3D691}
{85610AB6-07E8-45E3-9C48-34D952CC5DD2} = {B8D16C60-99B4-43D5-A3AD-4CD89AF39B25}
{5899FB03-AB3F-4113-A3F0-59096B48FA37} = {F23AC6C2-DE44-4699-999D-3C478EF3D691}
{D87557AF-668B-4A0F-B079-FA35F71D1C56} = {90030E85-0C4F-456F-B879-443E8A3F220D}
{6236734E-507B-461B-8E92-068886058E84} = {F23AC6C2-DE44-4699-999D-3C478EF3D691}
{5283A8BC-DFF4-436D-AA9C-EE2DBFC5D51A} = {F23AC6C2-DE44-4699-999D-3C478EF3D691}
EndGlobalSection
Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ nav:
- Media Azure: docs/reference/modules/Media.Azure/README.md
- ReCaptcha: docs/reference/modules/ReCaptcha/README.md
- Resources: docs/reference/modules/Resources/README.md
- Sitemaps: docs/reference/modules/Sitemaps/README.md
- Menu: docs/reference/modules/Menu/README.md
- Navigation: docs/reference/modules/Navigation/README.md
- Admin: docs/reference/modules/Admin/README.md
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
<ProjectReference Include="..\..\OrchardCore\OrchardCore.Indexing.Abstractions\OrchardCore.Indexing.Abstractions.csproj" />
<ProjectReference Include="..\..\OrchardCore\OrchardCore.MetaWeblog.Abstractions\OrchardCore.MetaWeblog.Abstractions.csproj" />
<ProjectReference Include="..\..\OrchardCore\OrchardCore.ResourceManagement\OrchardCore.ResourceManagement.csproj" />
<ProjectReference Include="..\..\OrchardCore\OrchardCore.Sitemaps.Abstractions\OrchardCore.Sitemaps.Abstractions.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using OrchardCore.Autoroute.Models;
using OrchardCore.ContentManagement;
using OrchardCore.ContentManagement.Metadata;
using OrchardCore.ContentManagement.Metadata.Models;
using OrchardCore.Sitemaps.Builders;
using OrchardCore.Sitemaps.Services;

namespace OrchardCore.Autoroute.Sitemaps
{
public class AutorouteContentTypeProvider : IRouteableContentTypeProvider
{
private readonly IContentDefinitionManager _contentDefinitionManager;
private readonly IContentManager _contentManager;

public AutorouteContentTypeProvider(
IContentDefinitionManager contentDefinitionManager,
IContentManager contentManager
)
{
_contentDefinitionManager = contentDefinitionManager;
_contentManager = contentManager;
}

public async Task<string> GetRouteAsync(SitemapBuilderContext context, ContentItem contentItem)
{
var ctd = ListRoutableTypeDefinitions()?
.FirstOrDefault(rctd => rctd.Name == contentItem.ContentType);

if (ctd != null)
{
var contentItemMetadata = await _contentManager.PopulateAspectAsync<ContentItemMetadata>(contentItem);
var routes = contentItemMetadata.DisplayRouteValues;

// UrlHelper.Action includes BasePath automatically if present.
// If content item is assigned as home route, Urlhelper resolves as site root.
return context.HostPrefix + context.UrlHelper.Action(routes["Action"].ToString(), routes);
}

return null;
}

public IEnumerable<ContentTypeDefinition> ListRoutableTypeDefinitions()
{
return _contentDefinitionManager.ListTypeDefinitions()
.Where(ctd => ctd.Parts.Any(p => p.Name == nameof(AutoroutePart)));
}
}
}
11 changes: 11 additions & 0 deletions src/OrchardCore.Modules/OrchardCore.Autoroute/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using OrchardCore.Autoroute.Routing;
using OrchardCore.Autoroute.Services;
using OrchardCore.Autoroute.Settings;
using OrchardCore.Autoroute.Sitemaps;
using OrchardCore.Autoroute.ViewModels;
using OrchardCore.ContentManagement;
using OrchardCore.ContentManagement.Display.ContentDisplay;
Expand All @@ -25,6 +26,7 @@
using OrchardCore.Modules;
using OrchardCore.Routing;
using OrchardCore.Security.Permissions;
using OrchardCore.Sitemaps.Services;
using YesSql;
using YesSql.Indexes;

Expand Down Expand Up @@ -82,4 +84,13 @@ public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder ro
routes.MapDynamicControllerRoute<AutoRouteTransformer>("/{any}/{**slug}");
}
}

[RequireFeatures("OrchardCore.Sitemaps")]
public class SitemapStartup : StartupBase
{
public override void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IRouteableContentTypeProvider, AutorouteContentTypeProvider>();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,11 @@
Dependencies = new[] { "OrchardCore.ContentLocalization" },
Category = "Internationalization"
)]

[assembly: Feature(
Id = "OrchardCore.ContentLocalization.Sitemaps",
Name = "Localized Content Item Sitemaps",
Description = "Provides support for localized content item sitemaps.",
Dependencies = new[] { "OrchardCore.Sitemaps" },
Category = "Internationalization"
)]
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
<ProjectReference Include="..\..\OrchardCore\OrchardCore.Liquid.Abstractions\OrchardCore.Liquid.Abstractions.csproj" />
<ProjectReference Include="..\..\OrchardCore\OrchardCore.Module.Targets\OrchardCore.Module.Targets.csproj" />
<ProjectReference Include="..\..\OrchardCore\OrchardCore.ResourceManagement\OrchardCore.ResourceManagement.csproj" />
<ProjectReference Include="..\..\OrchardCore\OrchardCore.Sitemaps.Abstractions\OrchardCore.Sitemaps.Abstractions.csproj" />

<ProjectReference Include="..\OrchardCore.Localization\OrchardCore.Localization.csproj" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using OrchardCore.ContentLocalization.Models;
using OrchardCore.ContentLocalization.Records;
using OrchardCore.ContentManagement;
using OrchardCore.ContentManagement.Records;
using OrchardCore.Localization;
using OrchardCore.Sitemaps.Builders;
using OrchardCore.Sitemaps.Models;
using OrchardCore.Sitemaps.Services;
using YesSql;
using YesSql.Services;

namespace OrchardCore.ContentLocalization.Sitemaps
{
public class LocalizedContentItemsQueryProvider : IContentItemsQueryProvider
{
private readonly ISession _session;
private readonly IRouteableContentTypeCoordinator _routeableContentTypeCoordinator;
private readonly ILocalizationService _localizationService;

public LocalizedContentItemsQueryProvider(
ISession session,
IRouteableContentTypeCoordinator routeableContentTypeCoordinator,
ILocalizationService localizationService
)
{
_session = session;
_routeableContentTypeCoordinator = routeableContentTypeCoordinator;
_localizationService = localizationService;
}

public async Task GetContentItems(ContentTypesSitemapSource source, ContentItemsQueryContext queryContext)
{
var routeableContentTypeDefinitions = _routeableContentTypeCoordinator.ListRoutableTypeDefinitions();

if (source.IndexAll)
{
// Assumption here is that at least one content type will be localized.
var rctdNames = routeableContentTypeDefinitions.Select(rctd => rctd.Name);

var queryResults = await _session.Query<ContentItem>()
.With<ContentItemIndex>(x => x.Published && x.ContentType.IsIn(rctdNames))
.OrderBy(x => x.CreatedUtc)
.ListAsync();

queryContext.ContentItems = queryResults;

// Provide all content items with localization as reference content items.
queryContext.ReferenceContentItems = queryResults
.Where(ci => ci.Has<LocalizationPart>());
}
else if (source.LimitItems)
{
// Test that content type is still valid to include in sitemap.
var contentType = routeableContentTypeDefinitions
.FirstOrDefault(ctd => String.Equals(source.LimitedContentType.ContentTypeName, ctd.Name));

if (contentType == null)
{
return;
}

if (contentType.Parts.Any(ctd => String.Equals(ctd.Name, nameof(LocalizationPart))))
{
// Get all content items here for reference. Then reduce by default culture.
// We know that the content item should be localized.
// If it doesn't have a localization part, the content item should have been saved.
var queryResults = await _session.Query<ContentItem>()
.With<ContentItemIndex>(ci => ci.ContentType == source.LimitedContentType.ContentTypeName && ci.Published)
.OrderBy(ci => ci.CreatedUtc)
.With<LocalizedContentItemIndex>()
.ListAsync();

// When limiting items Content item is valid if it is for the default culture.
var defaultCulture = await _localizationService.GetDefaultCultureAsync();

// Reduce by default culture.
var items = queryResults
.Where(ci => String.Equals(ci.As<LocalizationPart>().Culture, defaultCulture))
.Skip(source.LimitedContentType.Skip)
.Take(source.LimitedContentType.Take);

queryContext.ContentItems = items;

// Provide all content items with localization as reference content items.
queryContext.ReferenceContentItems = queryResults
.Where(ci => ci.Has<LocalizationPart>());
}
else
{
// Content type is not localized. Produce standard results.
var queryResults = await _session.Query<ContentItem>()
.With<ContentItemIndex>(x => x.ContentType == source.LimitedContentType.ContentTypeName && x.Published)
.OrderBy(x => x.CreatedUtc)
.Skip(source.LimitedContentType.Skip)
.Take(source.LimitedContentType.Take)
.ListAsync();

queryContext.ContentItems = queryResults;
}
}
else
{
// Test that content types are still valid to include in sitemap.
var typesToIndex = routeableContentTypeDefinitions
.Where(ctd => source.ContentTypes.Any(s => String.Equals(ctd.Name,s.ContentTypeName)))
.Select(x => x.Name);

// No advantage here in reducing with localized index.
var queryResults = await _session.Query<ContentItem>()
.With<ContentItemIndex>(x => x.ContentType.IsIn(typesToIndex) && x.Published)
.OrderBy(x => x.CreatedUtc)
.ListAsync();

queryContext.ContentItems = queryResults;

// Provide all content items with localization as reference content items.
queryContext.ReferenceContentItems = queryResults
.Where(ci => ci.Has<LocalizationPart>());
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System.Threading.Tasks;
using OrchardCore.ContentLocalization.Models;
using OrchardCore.ContentManagement;
using OrchardCore.Localization;
using OrchardCore.Sitemaps.Builders;

namespace OrchardCore.ContentLocalization.Sitemaps
{
public class SitemapLocalizedContentItemValidationProvider : ISitemapContentItemValidationProvider
{
private readonly ILocalizationService _localizationService;

public SitemapLocalizedContentItemValidationProvider(
ILocalizationService localizationService)
{
_localizationService = localizationService;
}

public async Task<bool> ValidateContentItem(ContentItem contentItem)
{
var part = contentItem.As<LocalizationPart>();
if (part == null)
{
return true;
}

// Content item is valid if it is for the default culture.
var defaultCulture = await _localizationService.GetDefaultCultureAsync();
if (part.Culture == defaultCulture)
{
return true;
}

return false;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using System.Linq;
using System.Threading.Tasks;
using System.Xml.Linq;
using OrchardCore.ContentLocalization.Models;
using OrchardCore.ContentManagement;
using OrchardCore.Sitemaps.Builders;
using OrchardCore.Sitemaps.Services;

namespace OrchardCore.ContentLocalization.Sitemaps
{
public class SitemapUrlHrefLangExtendedMetadataProvider : ISitemapContentItemExtendedMetadataProvider
{
private static readonly XNamespace ExtendedNamespace = "http://www.w3.org/TR/xhtml11/xhtml11_schema.html";
private static readonly XAttribute ExtendedAttribute = new XAttribute(XNamespace.Xmlns + "xhtml", ExtendedNamespace);

private readonly ISitemapPartContentItemValidationProvider _sitemapPartContentItemValidationProvider;
private readonly IRouteableContentTypeCoordinator _routeableContentTypeCoordinator;

public SitemapUrlHrefLangExtendedMetadataProvider(
ISitemapPartContentItemValidationProvider sitemapPartContentItemValidationProvider,
IRouteableContentTypeCoordinator routeableContentTypeCoordinator
)
{
_sitemapPartContentItemValidationProvider = sitemapPartContentItemValidationProvider;
_routeableContentTypeCoordinator = routeableContentTypeCoordinator;
}

public XAttribute GetExtendedAttribute => ExtendedAttribute;

public async Task<bool> ApplyExtendedMetadataAsync(
SitemapBuilderContext context,
ContentItemsQueryContext queryContext,
ContentItem contentItem,
XElement url)
{
var part = contentItem.As<LocalizationPart>();
if (part == null)
{
return true;
}

var localizedContentParts = queryContext.ReferenceContentItems
.Select(ci => ci.As<LocalizationPart>())
.Where(cp => cp.LocalizationSet == part.LocalizationSet);

foreach (var localizedPart in localizedContentParts)
{
if (!await _sitemapPartContentItemValidationProvider.ValidateContentItem(localizedPart.ContentItem))
{
continue;
}

var hrefValue = await _routeableContentTypeCoordinator.GetRouteAsync(context, localizedPart.ContentItem);

var linkNode = new XElement(ExtendedNamespace + "link",
new XAttribute("rel", "alternate"),
new XAttribute("hreflang", localizedPart.Culture),
new XAttribute("href", hrefValue));

url.Add(linkNode);
}

return true;
}
}
}
Loading

0 comments on commit a5e11d3

Please sign in to comment.