Exam Area: Content Area 3 – Website Implementation & Delivery (25%)
Source: https://academy.optimizely.com/student/path/3128969/activity/4970325
Published: Feb 24, 2026
- Architectural Choice: Search & Navigation for .NET-heavy sites, or Optimizely Graph for decoupled, SaaS-first delivery.
- Sync Strategy: Combine real-time
IContentEventsfor immediate updates with scheduled jobs for full-tree integrity.- Precision Control: Use the
[Searchable]attribute and Conventions API to eliminate index bloat.- Security Default: Enforce
FilterForVisitor()andIExcludeFromSearchto ensure data privacy.
IContent repositorydotnet add package EPiServer.Find
dotnet add package EPiServer.Find.Cms
{
"EPiServer": {
"Find": {
"ServiceUrl": "https://search-api.episerver.net/path/",
"DefaultIndex": "mysite_dev"
}
}
}
// Automatic indexing via IContentEvents
[InitializableModule]
public class SearchIndexingModule : IInitializableModule
{
private IContentEvents _contentEvents;
private IClient _searchClient;
public void Initialize(InitializationEngine context)
{
_contentEvents = context.Locate.Advanced.GetInstance<IContentEvents>();
_searchClient = context.Locate.Advanced.GetInstance<IClient>();
_contentEvents.PublishedContent += OnPublished;
_contentEvents.DeletedContent += OnDeleted;
}
private void OnPublished(object sender, ContentEventArgs e)
{
_searchClient.Index(e.Content);
}
private void OnDeleted(object sender, DeleteContentEventArgs e)
{
_searchClient.Delete<IContent>(x => x.ContentLink.ID.Match(e.ContentLink.ID));
}
}
using EPiServer.Find;
using EPiServer.Find.Cms;
public class SearchService
{
private readonly IClient _searchClient;
public SearchService(IClient searchClient)
{
_searchClient = searchClient;
}
public SearchResults<IContent> Search(string query, int page = 1, int pageSize = 10)
{
return _searchClient.Search<IContent>()
.For(query)
.FilterForVisitor() // Security: only return content the user can access
.ExcludeDeleted()
.Skip((page - 1) * pageSize)
.Take(pageSize)
.GetResult();
}
}
public class ArticlePage : PageData
{
// Included in full-text index (default)
[Searchable]
public virtual string Heading { get; set; }
// Excluded from full-text index
[Searchable(false)]
public virtual string InternalNotes { get; set; }
// Searchable but with boosted weight
[Searchable]
[Display(Order = 10)]
public virtual XhtmlString MainBody { get; set; }
}
// Exclude entire content types or properties via Conventions API
[InitializableModule]
public class SearchConventionsModule : IInitializableModule
{
public void Initialize(InitializationEngine context)
{
var client = context.Locate.Advanced.GetInstance<IClient>();
// Exclude entire content type
client.Conventions.ForInstancesOf<PrivatePage>()
.ExcludeFromIndex();
// Include block content inside page index
client.Conventions.ForInstancesOf<ArticlePage>()
.IncludeField(x => x.MainBody);
}
}
// ALWAYS call FilterForVisitor() in search queries
// Ensures only content the current user has Read access to is returned
var results = _searchClient.Search<PageData>()
.For(query)
.FilterForVisitor() // ← Required for access control
.GetResult();
// Best Bets: promote specific results for certain search terms
// Configured in CMS Admin → Search & Navigation → Best Bets
// No code required — managed through the UI
// To retrieve best bets programmatically:
var bestBets = _searchClient.Search<PageData>()
.For(query)
.ApplyBestBets()
.FilterForVisitor()
.GetResult();
EPiServer.Find and EPiServer.Find.Cms)[Searchable(false)] do? (Excludes a specific property from the full-text index)FilterForVisitor() be used? (Security – ensures only content the user has permission to view is returned)IContentEvents – when content is published or deleted, a delta index update is automatically triggered)IndexInContentAreas attribute used? (When you want to include block content in the parent page's search index)