Optimizely
Ai12z Optimizely Module Installation and Setup Guide
Add Connector
- Log in to the ai12z portal.
- Open Connectors from the left navigation and click Add Connectors.
- Select Magnolia and submit.
Step 2: Configure Connector
- Open Connectors and click the row action on Optimizely to edit.
- In the Connector Configuration screen, fill out the following fields:

- Name: Provide a name for your connector. This will appear in your document list. (e.g., "My Magnolia Site")
- Description: Describe your connector. (e.g., "My Optimizely Site")
- Public Instance URL: Enter the public Magnolia site URL (e.g.,
example.com/optimizelyPublic). - URL Filtering (Optional):
- Include URL Patterns: Provide patterns to include URLs for indexing (e.g.,
*/travel/*). Use commas to separate multiple patterns. Supports wildcards (*). - Exclude URL Patterns: Provide patterns to exclude URLs from indexing (e.g.,
*/about/company). Use commas to separate multiple patterns. Supports wildcards (*).
- Include URL Patterns: Provide patterns to include URLs for indexing (e.g.,
Step 3: Optimizely Module Setup
- To enable real-time indexing and page tracking, you need to integrate a webhook listener that triggers on page publish. Add the Webhook Initializer Code
- In your Optimizely project, create a new class file (e.g., PagePublishEventModule.cs) inside your business logic folder (e.g., Business/).
- Paste the following code inside the file:
Add the Webhook Initializer Code
In your Optimizely project, create a new class file (e.g., PagePublishEventModule.cs) inside your business logic folder (e.g., Business/) and paste the following code:
PagePublishEventModule.cs
using EPiServer.Core;
using EPiServer.Framework;
using EPiServer.Framework.Initialization;
using EPiServer.ServiceLocation;
using EPiServer.Web.Routing;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using EPiServer.Data.Dynamic;
using EPiServer.Security;
using EPiServer.DataAccess;
using EPiServer.Logging;
using EPiServer.SpecializedProperties;
using Newtonsoft.Json;
namespace cmsemptypage.Business
{
[InitializableModule]
[ModuleDependency(typeof(EPiServer.Web.InitializationModule))]
public class PagePublishEventModule : IInitializableModule
{
private static readonly EPiServer.Logging.ILogger Logger = LogManager.GetLogger();
private IContentEvents? _contentEvents;
private IHttpClientFactory? _httpClientFactory;
private UrlResolver? _urlResolver;
private IContentLoader? _contentLoader;
public void Initialize(InitializationEngine context)
{
var locator = ServiceLocator.Current;
_contentEvents = locator.GetInstance<IContentEvents>();
_httpClientFactory = locator.GetInstance<IHttpClientFactory>();
_urlResolver = locator.GetInstance<UrlResolver>();
_contentLoader = locator.GetInstance<IContentLoader>();
// Existing Published event
_contentEvents.PublishedContent += ContentEvents_PublishedContent;
// Capture when items are moved (this handles "Move to Trash")
_contentEvents.MovedContent += ContentEvents_MovedContent;
// Capture permanent deletion (Emptying trash)
_contentEvents.DeletedContent += ContentEvents_DeletedContent;
Logger.Information("PagePublishEventModule initialized. Monitoring Publish, MoveToTrash, and Delete events.");
}
private void ContentEvents_MovedContent(object? sender, ContentEventArgs e)
{
// In Optimizely, deleting a page usually moves it to the Waste Basket (ContentReference.WasteBasket)
if (e.TargetLink.CompareToIgnoreWorkID(ContentReference.WasteBasket))
{
Logger.Information($"[Webhook] Content {e.Content.Name} moved to Waste Basket. Sending Delete event.");
TriggerAsyncEvent(e.Content, "unpublished");
}
}
private void ContentEvents_DeletedContent(object? sender, DeleteContentEventArgs e)
{
Logger.Information($"[Webhook] Content {e.Content.Name} permanently deleted.");
TriggerAsyncEvent(e.Content, "PERMANENT_DELETE");
}
private void ContentEvents_PublishedContent(object? sender, ContentEventArgs e)
{
if ((e.Content is PageData || e.Content is MediaData) && !ContentReference.IsNullOrEmpty(e.ContentLink))
{
TriggerAsyncEvent(e.Content, "PUBLISHED");
}
}
private void TriggerAsyncEvent(IContent content, string action)
{
var localizable = content as ILocalizable;
string language = localizable?.Language?.Name ?? "en";
Task.Run(() => SendPublishEventAsync(content, language, action))
.ContinueWith(t =>
{
if (t.IsFaulted)
{
Logger.Error($"[Webhook] Fatal error in SendPublishEventAsync ({action}) for {content.Name}: {t.Exception?.GetBaseException().Message}", t.Exception);
}
});
}
private async Task SendPublishEventAsync(IContent content, string language, string action)
{
if (_httpClientFactory == null || _urlResolver == null || _contentLoader == null)
{
Logger.Error("[Webhook] Critical dependencies are missing.");
return;
}
var isMedia = content is MediaData;
var contentType = content.GetOriginalType().Name;
var client = _httpClientFactory.CreateClient();
var webhookUrl = "https://040d-110-235-234-181.ngrok-free.app/api/connector/6660800a32e86ecebd04dfd6?projectid=66210d8a76691fed1e6b227a";
var relativeUrl = _urlResolver.GetUrl(content.ContentLink, language) ?? "/";
var fullUrl = new Uri("http://localhost:4000" + relativeUrl);
var propertiesPayload = new Dictionary<string, object?>();
try
{
foreach (var property in content.Property)
{
if (property.Value == null) continue;
if (property.Name.Equals(nameof(PageData.PageName), StringComparison.OrdinalIgnoreCase) ||
property.Name.Equals(nameof(PageData.ContentLink), StringComparison.OrdinalIgnoreCase) ||
property.Name.Equals(nameof(PageData.ParentLink), StringComparison.OrdinalIgnoreCase))
continue;
if (property is PropertyContentArea contentAreaProperty && contentAreaProperty.Value is ContentArea contentArea)
{
var contentAreaItems = contentArea.Items.Select(i => {
_contentLoader.TryGet(i.ContentLink, out IContent? referencedContent);
return new { ContentLink = i.ContentLink.ID, ContentGuid = referencedContent?.ContentGuid };
}).ToList();
propertiesPayload[property.Name] = contentAreaItems;
}
else if (property is PropertyContentReference contentRefProperty && contentRefProperty.Value is ContentReference contentRef)
{
_contentLoader.TryGet(contentRef, out IContent? referencedContent);
propertiesPayload[property.Name] = new { Id = contentRef.ID, Guid = referencedContent?.ContentGuid };
}
else if (property is PropertyXhtmlString xhtmlProperty)
{
propertiesPayload[property.Name] = xhtmlProperty.ToString();
}
else if (property.Value is string || property.Value is int || property.Value is long ||
property.Value is double || property.Value is decimal || property.Value is bool ||
property.Value is DateTime || property.Value is Guid)
{
propertiesPayload[property.Name] = property.Value;
}
else
{
propertiesPayload[property.Name] = property.ToString();
}
}
}
catch (Exception ex) { Logger.Error($"[Webhook] Error extracting properties: {ex.Message}"); }
string? jsonLdString = (content is EPiServer.Reference.Commerce.Site.Features.Shared.Pages.StandardPage standardPage)
? standardPage.JsonLdScript : null;
var payload = new
{
Action = action,
PageName = content.Name,
PageId = content.ContentLink.ID,
ContentGuid = content.ContentGuid,
ParentId = content.ParentLink.ID,
IsMedia = isMedia,
PageUrl = fullUrl.ToString(),
PublishedAt = DateTime.UtcNow,
Language = language,
ContentTypeName = contentType,
Properties = propertiesPayload,
JsonLd = jsonLdString
};
var json = System.Text.Json.JsonSerializer.Serialize(payload, new System.Text.Json.JsonSerializerOptions { WriteIndented = true });
var httpContent = new StringContent(json, Encoding.UTF8, "application/json");
try
{
var response = await client.PostAsync(webhookUrl, httpContent);
if (response.IsSuccessStatusCode)
Logger.Information($"[Webhook] Sent {action} for: {content.Name}");
else
Logger.Error($"[Webhook] Failed {action} for {content.Name}. Status: {response.StatusCode}");
}
catch (Exception ex) { Logger.Error($"[Webhook] HTTP error: {ex.Message}"); }
}
public void Uninitialize(InitializationEngine context)
{
if (_contentEvents != null)
{
_contentEvents.PublishedContent -= ContentEvents_PublishedContent;
_contentEvents.MovedContent -= ContentEvents_MovedContent;
_contentEvents.DeletedContent -= ContentEvents_DeletedContent;
}
}
}
}
Note: You must register this module so it runs during application startup. The code listens to page or media publish events and sends data to the Ai12z webhook.
Update Webhook URL In the webhook handler code (inside SendPublishEventAsync method), replace the placeholder URL with your actual Ai12z webhook URL. You’ll find this in the ai12z connector configuration screen under Endpoint URL.
var webhookUrl = "https://xxxxxxxxx.ngrok-free.dev/api/connector/666080xxxxxxxdfd6?projectid=66210dxxxxxx6b227a";
What the Code Does (Summary)
- Listens for page or media publish events.
- Extracts metadata such as:
- Page name, ID, parent ID, content type
- URLs, publish timestamps, author info
- All page properties (including nested content references and content areas)
- Sends a JSON payload to your configured Ai12z webhook URL.
- Includes error handling and fallbacks for complex property types to avoid serialization issues.