Follow the following instructions to create a new Alloy example site: https://support.optimizely.com/hc/en-us/articles/14970775439757-Setting-Up-an-Alloy-Site-for-CMS-12
dotnet package add Optimizely.ContentGraph.Cms --project DemoAlloyWithGraph.csproj
Add the following inside appsettings.json
"Optimizely": {
"ContentGraph": {
"GatewayAddress": "https://latest.cg.optimizely.com",
"AppKey": "",
"Secret": "",
"SingleKey": "",
"BufferedIndexingGracePeriod": 10000,
"PreventFieldCollision": true,
"UseDateTimeGQLType": true
}
},
Set appKey, Secret, and SingleKey to the values you have in your Graph account
Create a new folder named "Graph" under the project root, and add a new file in the folder named 'GraphStartup.cs' with the following content
using EPiServer.DependencyInjection;
using Optimizely.ContentGraph.Cms.Configuration;
using Optimizely.ContentGraph.Cms.Configuration.Internal;
namespace DemoAlloyWithGraph.Graph
{
public class GraphStartup
{
public static void RegisterGraph(IServiceCollection services)
{
services.AddContentDeliveryApi();
services.AddContentGraph(options =>
{
options.ExtractMedia = false;
});
services.ConfigureScheduleJob(
checkStatusInterval: TimeSpan.FromSeconds(60),
checkStatusSleepOnFailure: TimeSpan.FromSeconds(15),
waitUntilFinalResult: false
);
services.Configure<EventIndexingOptions>(options =>
{
options.IndexReferencingContent = ReferencingPropertyTypes.Inlined;
options.SyncContentsInParallelTaskAndForget = false; // Synchronous processing for better control
});
}
}
}
Make sure this code is being run at startup, by adding the following code in the 'ConfigureServices' method inside 'Startup.cs':
GraphStartup.RegisterGraph(services);
Create a new file under the "Graph" folder named 'GraphInitializableModule.cs' with the following content
using EPiServer.Framework;
using EPiServer.Framework.Initialization;
using Optimizely.ContentGraph.Cms.NetCore.ConventionsApi;
using EPiServer.ServiceLocation;
using DemoAlloyWithGraph.Models.Pages;
[ModuleDependency]
public class GraphInitializableModule : IInitializableModule
{
public void Initialize(InitializationEngine context)
{
var conventionRepository = context.Locate.Advanced.GetInstance<ConventionRepository>();
RegisterConventions(conventionRepository);
}
public void Uninitialize(InitializationEngine context)
{
}
private static void RegisterConventions(ConventionRepository conventionRepository)
{
conventionRepository.IncludeAbstract<ContentData>();
}
}
Create a new file under the "Graph" folder named 'ContentExtensions.cs' with the following content
public static class ContentExtensions
{
public static string SearchTitle(this ContentData contentData)
{
if (contentData is SitePageData sitePageData && !string.IsNullOrWhiteSpace(sitePageData.MetaTitle))
{
return sitePageData.MetaTitle;
}
if (contentData is IContent content)
{
return content.Name;
}
return null;
}
public static string SearchDescription(this ContentData contentData)
{
if (contentData is StandardPage standardPage && !string.IsNullOrWhiteSpace(standardPage.MainBody?.ToString()))
{
return standardPage.MainBody.ToString();
}
if (contentData is SitePageData sitePageData)
{
if (!string.IsNullOrWhiteSpace(sitePageData.TeaserText))
{
return sitePageData.TeaserText;
}
if (!string.IsNullOrWhiteSpace(sitePageData.MetaDescription))
{
return sitePageData.MetaDescription;
}
}
return null;
}
public static List<string> SearchKeywords(this ContentData contentData)
{
if (contentData is SitePageData sitePageData && sitePageData.MetaKeywords != null)
{
return [.. sitePageData.MetaKeywords];
}
return [];
}
public static List<string> SearchCategories(this ContentData contentData)
{
var categoryRepository = ServiceLocator.Current.GetInstance<CategoryRepository>();
if (contentData is SitePageData sitePageData && sitePageData.Category != null)
{
return sitePageData.Category.Select(x => categoryRepository?.Get(x)?.Name)?.ToList();
}
return [];
}
}
Add the extensions to Graph, by uppdating the RegisterConventions method in GraphInitializableModule.cs
using EPiServer.Framework;
using EPiServer.Framework.Initialization;
using Optimizely.ContentGraph.Cms.NetCore.ConventionsApi;
using EPiServer.ServiceLocation;
using DemoAlloyWithGraph.Models.Pages;
[ModuleDependency]
public class GraphInitializableModule : IInitializableModule
{
public void Initialize(InitializationEngine context)
{
var conventionRepository = context.Locate.Advanced.GetInstance<ConventionRepository>();
RegisterConventions(conventionRepository);
}
public void Uninitialize(InitializationEngine context)
{
}
private static void RegisterConventions(ConventionRepository conventionRepository)
{
conventionRepository.IncludeAbstract<ContentData>();
conventionRepository.ForInstancesOf<ContentData>()
.IncludeField(x => x.SearchTitle()).Set(x => x.SearchTitle(), IndexingType.Searchable)
.IncludeField(x => x.SearchDescription()).Set(x => x.SearchDescription(), IndexingType.Searchable)
.IncludeField(x => x.SearchKeywords()).Set(x => x.SearchKeywords(), IndexingType.Searchable)
.IncludeField(x => x.SearchCategories()).Set(x => x.SearchCategories(), IndexingType.Searchable);
}
}
query SearchQuery(
$searchPhrase:String,
) {
ContentData(
where: {
_fulltext: { match: $searchPhrase }
}
) {
items {
SearchTitle
SearchDescription(highlight: {enabled:true})
RelativePath
_score
}
}
}
query SearchQuery(
$searchPhrase:String,
$searchKeywords: [String!],
$searchCategories: [String!]
) {
ContentData(
where: {
_fulltext: { match: $searchPhrase }
}
) {
items {
SearchTitle
SearchDescription(highlight: {enabled:true})
RelativePath
_score
}
facets {
SearchKeywords(filters:$searchKeywords) {
name
count
}
SearchCategories(filters:$searchCategories) {
name
count
}
}
}
}
query SearchQuery(
$locales: [Locales],
$searchPhrase:String,
$searchKeywords: [String!],
$searchCategories: [String!]
) {
ContentData(
locale: $locales
where: {
_fulltext: { match: $searchPhrase }
}
) {
items {
SearchTitle
SearchDescription(highlight: {enabled:true})
RelativePath
_score
}
facets {
SearchKeywords(filters:$searchKeywords) {
name
count
}
SearchCategories(filters:$searchCategories) {
name
count
}
}
}
}
query SearchQuery(
$locales: [Locales],
$searchPhrase:String,
$searchKeywords: [String!],
$searchCategories: [String!]
) {
ContentData(
locale: $locales
where: {
_fulltext: { match: $searchPhrase }
}
orderBy: { _ranking: RELEVANCE }
) {
items {
SearchTitle
SearchDescription(highlight: {enabled:true})
RelativePath
_score
}
facets {
SearchKeywords(filters:$searchKeywords) {
name
count
}
SearchCategories(filters:$searchCategories) {
name
count
}
}
}
}
Give the LLM the following instructions:
I would like to use GraphQL Codegen with the GraphQL endpoint 'https://latest.cg.optimizely.com/content/v2?auth=Jjn2D2rGWZsu20EFvQ2ftf1x3GXcIBpnsLtxxDbyqPrKFYu3' inside the vew Index.cshtml
I would like to use ReactJs, to do client side rendering inside the view.
I would like to use GraphQL Codegen with preset: 'client'
I would like you to create a simple GraphQL query for verification using the following query: 'query TotalItems { Data { total } }'
I would like you to test that the page is working after implementation, by starting the site using dotnet run, and then checking the response on url localhost:5000/en/search
I would like you to update the SearchPage.tsx to use the following query instead:
query SearchQuery(
$locales: [Locales],
$searchPhrase:String,
$searchKeywords: [String!],
$searchCategories: [String!]
) {
ContentData(
locale: $locales
where: {
_fulltext: { match: $searchPhrase }
}
orderBy: { _ranking: RELEVANCE }
) {
items {
SearchTitle
SearchDescription(highlight: {enabled:true})
RelativePath
_score
}
facets {
SearchKeywords(filters:$searchKeywords) {
name
count
}
SearchCategories(filters:$searchCategories) {
name
count
}
}
}
}
Make sure to add the query inside the queries folder, and auto generate the result using graphql codegen.
I would like you to test that the page is working after implementation by checking the response on url localhost:5000/en/search
The whole search-app-root is now being updated every time a change is being made by the user in SearchPage.tsx, for example searching for a phrase or selecting a facet value.
I would like you to update the logic, to only re-render the search result, and facets when searching on a phrase or selecting/deselecting a facet value. This will make the page feel much more responsive.
The main div in SearchPage/Index.cshtml is being collapsed when the user interacts, which makes the page blip when searching with a search phrase, selecting facet values, or deselecting facet values.
query SearchQuery(
$locales: [Locales],
$searchPhrase:String,
$searchKeywords: [String!],
$searchCategories: [String!]
) {
ContentData(
locale: $locales
where: {
_fulltext: { match: $searchPhrase }
}
orderBy: { _ranking: SEMANTIC, _semanticWeight: 0.5}
) {
items {
SearchTitle
SearchDescription(highlight: {enabled:true})
RelativePath
_score
}
facets {
SearchKeywords(filters:$searchKeywords) {
name
count
}
SearchCategories(filters:$searchCategories) {
name
count
}
}
}
}
query SearchQuery(
$locales: [Locales],
$searchPhrase:String,
$searchKeywords: [String!],
$searchCategories: [String!]
) {
ContentData(
locale: $locales
where: {
_or: [
{ _fulltext: { match: $searchPhrase } },
{ SearchTitle: {match: $searchPhrase, boost: 5} }
{ SearchDescription: {match: $searchPhrase, boost: 3} }
]
}
orderBy: { _ranking: SEMANTIC }
) {
items {
SearchTitle
SearchDescription(highlight: {enabled:true})
RelativePath
_score
}
facets {
SearchKeywords(filters:$searchKeywords) {
name
count
}
SearchCategories(filters:$searchCategories) {
name
count
}
}
}
}
I would like you to update the SearchQuery to be:
query SearchQuery(
$locales: [Locales],
$searchPhrase:String,
$searchKeywords: [String!],
$searchCategories: [String!]
) {
ContentData(
locale: $locales
where: {
_or: [
{ _fulltext: { match: $searchPhrase } },
{ SearchTitle: {match: $searchPhrase, boost: 5} }
{ SearchDescription: {match: $searchPhrase, boost: 3} }
]
}
orderBy: { _ranking: SEMANTIC }
) {
items {
SearchTitle
SearchDescription(highlight: {enabled:true})
RelativePath
_score
}
facets {
SearchKeywords(filters:$searchKeywords) {
name
count
}
SearchCategories(filters:$searchCategories) {
name
count
}
}
}
}
Run the GraphQL codegen tool to update any query response. I would also like you to test that the page is working after implementation by checking the response on url localhost:5000/en/search
I would like the SearchPage.tsx to make a request to the SearchController with the query, instead of making the request directly to https://latest.cg.optimizely.com/content/v2?auth=Jjn2D2rGWZsu20EFvQ2ftf1x3GXcIBpnsLtxxDbyqPrKFYu3. I would then like the Controller to make the request to https://latest.cg.optimizely.com/content/v2?auth=Jjn2D2rGWZsu20EFvQ2ftf1x3GXcIBpnsLtxxDbyqPrKFYu3 and return the result to the SearchPage.tsx.
Make sure the page is returning a 200 response, and result when searching on phrase Alloy.
