diff --git a/examples/DancingGoat/PageTemplates/_ViewImports.cshtml b/examples/DancingGoat/PageTemplates/_ViewImports.cshtml
index d0f38c6..477580f 100644
--- a/examples/DancingGoat/PageTemplates/_ViewImports.cshtml
+++ b/examples/DancingGoat/PageTemplates/_ViewImports.cshtml
@@ -4,7 +4,7 @@
@using Kentico.Web.Mvc
@using Kentico.Content.Web.Mvc
-@using Kentico.PageBuilder.Web.Mvc
+@using Kentico.Content.Web.Mvc.PageBuilder
@using Microsoft.AspNetCore.Mvc.Localization
diff --git a/examples/DancingGoat/Program.cs b/examples/DancingGoat/Program.cs
index 6d85630..df487f1 100644
--- a/examples/DancingGoat/Program.cs
+++ b/examples/DancingGoat/Program.cs
@@ -1,3 +1,6 @@
+using System;
+using System.Threading.Tasks;
+
using DancingGoat;
using DancingGoat.Models;
@@ -8,24 +11,21 @@
using Kentico.PageBuilder.Web.Mvc;
using Kentico.Web.Mvc;
-using Kentico.Xperience.Cloud;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.Extensions.DependencyInjection;
using DancingGoat.Search;
-var builder = WebApplication.CreateBuilder(args);
-builder.Services.AddXperienceCloudApplicationInsights(builder.Configuration);
+var builder = WebApplication.CreateBuilder(args);
-if (builder.Environment.IsQa() || builder.Environment.IsUat() || builder.Environment.IsProduction())
-{
- builder.Services.AddKenticoCloud(builder.Configuration);
- builder.Services.AddXperienceCloudSendGrid(builder.Configuration);
-}
builder.Services.AddKentico(features =>
{
@@ -73,10 +73,6 @@
app.UseAuthentication();
-if (builder.Environment.IsQa() || builder.Environment.IsUat() || builder.Environment.IsProduction())
-{
- app.UseKenticoCloud();
-}
app.UseKentico();
@@ -110,8 +106,6 @@
}
);
-app.MapControllers();
-
app.Run();
diff --git a/examples/DancingGoat/Properties/launchSettings.json b/examples/DancingGoat/Properties/launchSettings.json
index 0cb5bc0..1809696 100644
--- a/examples/DancingGoat/Properties/launchSettings.json
+++ b/examples/DancingGoat/Properties/launchSettings.json
@@ -3,7 +3,7 @@
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
- "applicationUrl": "http://localhost:63328",
+ "applicationUrl": "http://localhost:15037",
"sslPort": 0
}
},
@@ -18,7 +18,7 @@
"DancingGoat": {
"commandName": "Project",
"launchBrowser": true,
- "applicationUrl": "http://localhost:63328",
+ "applicationUrl": "http://localhost:15037",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
diff --git a/examples/DancingGoat/README.md b/examples/DancingGoat/README.md
index 13de7bd..4428c25 100644
--- a/examples/DancingGoat/README.md
+++ b/examples/DancingGoat/README.md
@@ -1,7 +1,6 @@
# Xperience by Kentico: Dancing Goat Sample Project
-This project implements a company website of a fictional coffee shop franchise with the aim of demonstrating
-the content management and digital marketing features of the Xperience solution.
+This project implements a company website of a fictional coffee shop franchise to demonstrate the Xperience solution's content management and digital marketing features.
## Installation and setup
@@ -10,13 +9,25 @@ to troubleshoot any installation or configuration issues.
## Project notes
-### Content type code files
+### Content type and reusable field schema code files
-[Content type](https://docs.xperience.io/x/gYHWCQ) code files under `./Models/Reusable` and `./Models/WebPage` are
-generated using [code generators](https://docs.xperience.io/x/5IbWCQ) provided by Xperience.
+[Content type](https://docs.xperience.io/x/gYHWCQ) and [reusable field schema](https://docs.xperience.io/x/D4_OD) code files under
-If you add new content types or make changes to existing ones (e.g., add or remove fields), you can
-run the following commands from the root of the Dancing Goat project:
+- `./Models/Reusable`
+- `./Models/WebPage`
+- `./Models/Schema`
+
+are generated using Xperience's [code generators](https://docs.xperience.io/x/5IbWCQ).
+
+If you change the site's content model (add or remove fields, define new content types or schemas, etc.), you can run the following commands from the root of the Dancing Goat project to regenerate the files.
+
+For _reusable field schemas_:
+
+```powershell
+dotnet run --no-build -- --kxp-codegen --location "./Models/Schema/" --type ReusableFieldSchemas --namespace "DancingGoat.Models"
+```
+
+This command regenerates the interfaces for all reusable field schemas in the project. Note that the specified `--namespace` must match the namespace where content type code files that reference the schemas are generated. You will get uncompilable code otherwise.
For _reusable_ content types:
@@ -26,7 +37,7 @@ dotnet run --no-build -- --kxp-codegen --location "./Models/Reusable/{name}/" --
This command generates code files for content types with the `DancingGoat` namespace under the `./Models/Reusable` directory.
-You can use a similar approach for _page_ content types:
+For _page_ content types:
```powershell
dotnet run --no-build -- --kxp-codegen --location "./Models/WebPage/{name}/" --type PageContentTypes --include "DancingGoat.*" --namespace "DancingGoat.Models"
@@ -34,4 +45,4 @@ dotnet run --no-build -- --kxp-codegen --location "./Models/WebPage/{name}/" --t
This command generates code files for content types with the `DancingGoat` namespace under the `./Models/WebPage` directory.
-You can of course adapt these example for use in projects with a different folder structure by modifying the `location` parameter accordingly.
+You can adapt these examples for use in projects with a different folder structure by modifying the `location` parameter accordingly.
diff --git a/examples/DancingGoat/Readme.txt b/examples/DancingGoat/Readme.txt
deleted file mode 100644
index 2f558d0..0000000
--- a/examples/DancingGoat/Readme.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-This project provides a starting point for developing an Xperience by Kentico website, along with tools and configuration for deploying the site to the SaaS environment.
-
-For more information visit:
-https://docs.xperience.io/x/IgKQC
\ No newline at end of file
diff --git a/examples/DancingGoat/Search/AdvancedSearchIndexingStrategy.cs b/examples/DancingGoat/Search/AdvancedSearchIndexingStrategy.cs
index 61f7747..2b60a05 100644
--- a/examples/DancingGoat/Search/AdvancedSearchIndexingStrategy.cs
+++ b/examples/DancingGoat/Search/AdvancedSearchIndexingStrategy.cs
@@ -47,57 +47,54 @@ WebCrawlerService webCrawler
}
};
- public override async Task
> MapToAlgoliaJObjectsOrNull(IIndexEventItemModel algoliaPageItem)
+ public override async Task?> MapToAlgoliaJObjectsOrNull(IIndexEventItemModel algoliaPageItem)
{
var resultProperties = new DancingGoatSearchResultModel();
// IIndexEventItemModel could be a reusable content item or a web page item, so we use
// pattern matching to get access to the web page item specific type and fields
- if (algoliaPageItem is IndexEventWebPageItemModel indexedPage)
+ if (algoliaPageItem is not IndexEventWebPageItemModel indexedPage)
{
- if (string.Equals(algoliaPageItem.ContentTypeName, ArticlePage.CONTENT_TYPE_NAME, StringComparison.OrdinalIgnoreCase))
+ return null;
+ }
+ if (string.Equals(algoliaPageItem.ContentTypeName, ArticlePage.CONTENT_TYPE_NAME, StringComparison.OrdinalIgnoreCase))
+ {
+ // The implementation of GetPage() is below
+ var page = await GetPage(
+ indexedPage.ItemGuid,
+ indexedPage.WebsiteChannelName,
+ indexedPage.LanguageName,
+ ArticlePage.CONTENT_TYPE_NAME);
+
+ if (page is null)
{
- // The implementation of GetPage() is below
- var page = await GetPage(
- indexedPage.ItemGuid,
- indexedPage.WebsiteChannelName,
- indexedPage.LanguageName,
- ArticlePage.CONTENT_TYPE_NAME);
-
- if (page is null)
- {
- return null;
- }
-
- resultProperties.SortableTitle = resultProperties.Title = page?.ArticleTitle ?? "";
-
- string rawContent = await webCrawler.CrawlWebPage(page!);
- resultProperties.Content = htmlSanitizer.SanitizeHtmlDocument(rawContent);
+ return null;
}
- else if (string.Equals(algoliaPageItem.ContentTypeName, HomePage.CONTENT_TYPE_NAME, StringComparison.OrdinalIgnoreCase))
+
+ resultProperties.SortableTitle = resultProperties.Title = page?.ArticleTitle ?? string.Empty;
+
+ string rawContent = await webCrawler.CrawlWebPage(page!);
+ resultProperties.Content = htmlSanitizer.SanitizeHtmlDocument(rawContent);
+ }
+ else if (string.Equals(algoliaPageItem.ContentTypeName, HomePage.CONTENT_TYPE_NAME, StringComparison.OrdinalIgnoreCase))
+ {
+ var page = await GetPage(
+ indexedPage.ItemGuid,
+ indexedPage.WebsiteChannelName,
+ indexedPage.LanguageName,
+ HomePage.CONTENT_TYPE_NAME);
+
+ if (page is null)
{
- var page = await GetPage(
- indexedPage.ItemGuid,
- indexedPage.WebsiteChannelName,
- indexedPage.LanguageName,
- HomePage.CONTENT_TYPE_NAME);
-
- if (page is null)
- {
- return null;
- }
-
- if (page.HomePageBanner.IsNullOrEmpty())
- {
- return null;
- }
-
- resultProperties.Title = page!.HomePageBanner.First().BannerHeaderText;
+ return null;
}
- else
+
+ if (page.HomePageBanner.IsNullOrEmpty())
{
return null;
}
+
+ resultProperties.Title = page!.HomePageBanner.First().BannerHeaderText;
}
else
{
diff --git a/examples/DancingGoat/Search/DancingGoatSearchStartupExtensions.cs b/examples/DancingGoat/Search/DancingGoatSearchStartupExtensions.cs
index 59306ca..dca5211 100644
--- a/examples/DancingGoat/Search/DancingGoatSearchStartupExtensions.cs
+++ b/examples/DancingGoat/Search/DancingGoatSearchStartupExtensions.cs
@@ -10,6 +10,7 @@ public static IServiceCollection AddKenticoAlgoliaServices(this IServiceCollecti
services.AddKenticoAlgolia(builder => {
builder.RegisterStrategy("DancingGoatAdvancedExampleStrategy");
builder.RegisterStrategy("DancingGoatMinimalExampleStrategy");
+ builder.RegisterStrategy(nameof(ReusableContentItemsIndexingStrategy));
}, configuration);
services.AddHttpClient();
diff --git a/examples/DancingGoat/Search/Models/DancingGoatSearchResultModel.cs b/examples/DancingGoat/Search/Models/DancingGoatSearchResultModel.cs
index 9a8d4b9..192efd3 100644
--- a/examples/DancingGoat/Search/Models/DancingGoatSearchResultModel.cs
+++ b/examples/DancingGoat/Search/Models/DancingGoatSearchResultModel.cs
@@ -4,7 +4,7 @@ namespace DancingGoat.Search.Models;
public class DancingGoatSearchResultModel : AlgoliaSearchResultModel
{
- public string Title { get; set; }
- public string SortableTitle { get; set; }
- public string Content { get; set; }
+ public string Title { get; set; } = string.Empty;
+ public string SortableTitle { get; set; } = string.Empty;
+ public string Content { get; set; } = string.Empty;
}
diff --git a/examples/DancingGoat/Search/ReusableContentItemsIndexingStrategy.cs b/examples/DancingGoat/Search/ReusableContentItemsIndexingStrategy.cs
new file mode 100644
index 0000000..c7d2f5e
--- /dev/null
+++ b/examples/DancingGoat/Search/ReusableContentItemsIndexingStrategy.cs
@@ -0,0 +1,141 @@
+using Algolia.Search.Models.Settings;
+
+using CMS.ContentEngine;
+using CMS.Websites;
+
+using DancingGoat.Models;
+using DancingGoat.Search.Models;
+using DancingGoat.Search.Services;
+
+using Kentico.Xperience.Algolia.Indexing;
+using Kentico.Xperience.Algolia.Search;
+
+using Newtonsoft.Json.Linq;
+
+namespace DancingGoat.Search;
+
+public class ReusableContentItemsIndexingStrategy : DefaultAlgoliaIndexingStrategy
+{
+ public const string SORTABLE_TITLE_FIELD_NAME = "SortableTitle";
+
+ private readonly IWebPageQueryResultMapper webPageMapper;
+ private readonly IContentQueryExecutor queryExecutor;
+ private readonly IWebPageUrlRetriever urlRetriever;
+ private readonly WebScraperHtmlSanitizer htmlSanitizer;
+ private readonly WebCrawlerService webCrawler;
+
+ public const string FACET_DIMENSION = "ContentType";
+ public const string INDEXED_WEBSITECHANNEL_NAME = "DancingGoatPages";
+ public const string CRAWLER_CONTENT_FIELD_NAME = "Content";
+
+ public ReusableContentItemsIndexingStrategy(
+ IWebPageQueryResultMapper webPageMapper,
+ IContentQueryExecutor queryExecutor,
+ IWebPageUrlRetriever urlRetriever,
+ WebScraperHtmlSanitizer htmlSanitizer,
+ WebCrawlerService webCrawler
+ )
+ {
+ this.urlRetriever = urlRetriever;
+ this.webPageMapper = webPageMapper;
+ this.queryExecutor = queryExecutor;
+ this.htmlSanitizer = htmlSanitizer;
+ this.webCrawler = webCrawler;
+ }
+
+ public override IndexSettings GetAlgoliaIndexSettings() => new()
+ {
+ AttributesToRetrieve = new List
+ {
+ nameof(DancingGoatSearchResultModel.Title),
+ nameof(DancingGoatSearchResultModel.SortableTitle),
+ nameof(DancingGoatSearchResultModel.Content)
+ },
+ AttributesForFaceting = new List
+ {
+ nameof(DancingGoatSearchResultModel.ContentTypeName)
+ }
+ };
+
+ public override async Task?> MapToAlgoliaJObjectsOrNull(IIndexEventItemModel algoliaPageItem)
+ {
+ var resultProperties = new DancingGoatSearchResultModel();
+
+ // IIndexEventItemModel could be a reusable content item or a web page item, so we use
+ // pattern matching to get access to the web page item specific type and fields
+ if (algoliaPageItem is not IndexEventReusableItemModel indexedItem)
+ {
+ return null;
+ }
+ if (string.Equals(algoliaPageItem.ContentTypeName, Banner.CONTENT_TYPE_NAME, StringComparison.OrdinalIgnoreCase))
+ {
+ var query = new ContentItemQueryBuilder()
+ .ForContentType(HomePage.CONTENT_TYPE_NAME,
+ config =>
+ config
+ .WithLinkedItems(4)
+ // Because the changedItem is a reusable content item, we don't have a website channel name to use here
+ // so we use a hardcoded channel name.
+ .ForWebsite(INDEXED_WEBSITECHANNEL_NAME)
+ // Retrieves all HomePages that link to the Banner through the HomePage.HomePageBanner field
+ .Linking(nameof(HomePage.HomePageBanner), new[] { indexedItem.ItemID }))
+ .InLanguage(indexedItem.LanguageName);
+
+ var associatedWebPageItem = (await queryExecutor.GetWebPageResult(query, webPageMapper.Map)).First();
+ string url = string.Empty;
+ try
+ {
+ url = (await urlRetriever.Retrieve(associatedWebPageItem.SystemFields.WebPageItemTreePath,
+ INDEXED_WEBSITECHANNEL_NAME, indexedItem.LanguageName)).RelativePath;
+ }
+ catch (Exception)
+ {
+ // Retrieve can throw an exception when processing a page update LuceneQueueItem
+ // and the page was deleted before the update task has processed. In this case, return no item.
+ return null;
+ }
+
+ //If the indexed item is a reusable content item, we need to set the url manually.
+ resultProperties.Url = url;
+ resultProperties.SortableTitle = resultProperties.Title = associatedWebPageItem!.HomePageBanner.First().BannerText;
+ string rawContent = await webCrawler.CrawlWebPage(associatedWebPageItem!);
+ resultProperties.Content = htmlSanitizer.SanitizeHtmlDocument(rawContent);
+
+ //If the indexed item is a reusable content item, we need to set the url manually.
+ var result = new List()
+ {
+ AssignProperties(resultProperties)
+ };
+
+ return result;
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ private JObject AssignProperties(T value) where T : AlgoliaSearchResultModel
+ {
+ var jObject = new JObject();
+
+ foreach (var prop in value.GetType().GetProperties())
+ {
+ var type = prop.PropertyType;
+ if (type == typeof(string))
+ {
+ jObject[prop.Name] = prop.GetValue(value) as string;
+ }
+ else if (type == typeof(int))
+ {
+ jObject[prop.Name] = (int?)prop.GetValue(value) ?? 0;
+ }
+ else if (type == typeof(bool))
+ {
+ jObject[prop.Name] = (bool?)prop.GetValue(value) ?? false;
+ }
+ }
+
+ return jObject;
+ }
+}
diff --git a/examples/DancingGoat/Search/Services/WebCrawlerService.cs b/examples/DancingGoat/Search/Services/WebCrawlerService.cs
index c4af3e5..133d948 100644
--- a/examples/DancingGoat/Search/Services/WebCrawlerService.cs
+++ b/examples/DancingGoat/Search/Services/WebCrawlerService.cs
@@ -43,7 +43,7 @@ public async Task CrawlWebPage(IWebPageFieldsSource page)
ex,
$"Tree Path: {page.SystemFields.WebPageItemTreePath}");
}
- return "";
+ return string.Empty;
}
public async Task CrawlPage(string url)
@@ -61,6 +61,6 @@ public async Task CrawlPage(string url)
ex,
$"Url: {url}");
}
- return "";
+ return string.Empty;
}
}
diff --git a/examples/DancingGoat/Search/Services/WebScraperHtmlSanitizer.cs b/examples/DancingGoat/Search/Services/WebScraperHtmlSanitizer.cs
index da935d6..473e19e 100644
--- a/examples/DancingGoat/Search/Services/WebScraperHtmlSanitizer.cs
+++ b/examples/DancingGoat/Search/Services/WebScraperHtmlSanitizer.cs
@@ -53,8 +53,8 @@ public virtual string SanitizeHtmlDocument(string htmlContent)
textContent = HTMLHelper.RegexHtmlToTextWhiteSpace.Replace(textContent, " ");
textContent = textContent.Trim();
- string title = doc.Head?.QuerySelector("title")?.TextContent ?? "";
- string description = doc.Head?.QuerySelector("meta[name='description']")?.GetAttribute("content") ?? "";
+ string title = doc.Head?.QuerySelector("title")?.TextContent ?? string.Empty;
+ string description = doc.Head?.QuerySelector("meta[name='description']")?.GetAttribute("content") ?? string.Empty;
return string.Join(
" ",
diff --git a/examples/DancingGoat/Search/SimpleSearchIndexingStrategy.cs b/examples/DancingGoat/Search/SimpleSearchIndexingStrategy.cs
index 30ab833..0dac327 100644
--- a/examples/DancingGoat/Search/SimpleSearchIndexingStrategy.cs
+++ b/examples/DancingGoat/Search/SimpleSearchIndexingStrategy.cs
@@ -32,40 +32,37 @@ public override IndexSettings GetAlgoliaIndexSettings() =>
}
};
- public override async Task> MapToAlgoliaJObjectsOrNull(IIndexEventItemModel algoliaPageItem)
+ public override async Task?> MapToAlgoliaJObjectsOrNull(IIndexEventItemModel algoliaPageItem)
{
var result = new List();
- string title = "";
+ string title;
// IIndexEventItemModel could be a reusable content item or a web page item, so we use
// pattern matching to get access to the web page item specific type and fields
- if (algoliaPageItem is IndexEventWebPageItemModel indexedPage)
+ if (algoliaPageItem is not IndexEventWebPageItemModel indexedPage)
{
- if (string.Equals(algoliaPageItem.ContentTypeName, HomePage.CONTENT_TYPE_NAME, StringComparison.OrdinalIgnoreCase))
+ return null;
+ }
+ if (string.Equals(algoliaPageItem.ContentTypeName, HomePage.CONTENT_TYPE_NAME, StringComparison.OrdinalIgnoreCase))
+ {
+ var page = await GetPage(
+ indexedPage.ItemGuid,
+ indexedPage.WebsiteChannelName,
+ indexedPage.LanguageName,
+ HomePage.CONTENT_TYPE_NAME);
+
+ if (page is null)
{
- var page = await GetPage(
- indexedPage.ItemGuid,
- indexedPage.WebsiteChannelName,
- indexedPage.LanguageName,
- HomePage.CONTENT_TYPE_NAME);
-
- if (page is null)
- {
- return null;
- }
-
- if (page.HomePageBanner.IsNullOrEmpty())
- {
- return null;
- }
-
- title = page!.HomePageBanner.First().BannerHeaderText;
+ return null;
}
- else
+
+ if (page.HomePageBanner.IsNullOrEmpty())
{
return null;
}
+
+ title = page!.HomePageBanner.First().BannerHeaderText;
}
else
{
diff --git a/examples/DancingGoat/Services/IServiceCollectionExtensions.cs b/examples/DancingGoat/Services/IServiceCollectionExtensions.cs
index 0d8c542..7a92104 100644
--- a/examples/DancingGoat/Services/IServiceCollectionExtensions.cs
+++ b/examples/DancingGoat/Services/IServiceCollectionExtensions.cs
@@ -14,7 +14,7 @@ public static void AddDancingGoatServices(this IServiceCollection services)
{
AddViewComponentServices(services);
AddRepositories(services);
-
+
services.AddSingleton();
}
@@ -27,10 +27,15 @@ private static void AddRepositories(IServiceCollection services)
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
- services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton