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;
// Required for services
using EPiServer.Data.Dynamic;
using EPiServer.Security;
using EPiServer.DataAccess;
using EPiServer.Logging;
using EPiServer.SpecializedProperties;
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>();
// Use PublishedContent for post-save operations
_contentEvents.PublishedContent += ContentEvents_PublishedContent;
Logger.Information("PagePublishEventModule initialized and subscribed to PublishedContent.");
}
private void ContentEvents_PublishedContent(object? sender, ContentEventArgs e)
{
if (e.Content is PageData pageData && !ContentReference.IsNullOrEmpty(e.ContentLink))
{
var localizable = pageData as ILocalizable;
string language = localizable?.Language?.Name ?? "en";
Task.Run(() => SendPublishEventAsync(e.Content, language))
.ContinueWith(t =>
{
if (t.IsFaulted)
{
Logger.Error($"[Webhook] Fatal error in SendPublishEventAsync for {e.Content.Name}: {t.Exception?.GetBaseException().Message}", t.Exception);
}
});
}
else if (e.Content is MediaData mediaData && !ContentReference.IsNullOrEmpty(e.ContentLink))
{
var localizable = mediaData as ILocalizable;
string language = localizable?.Language?.Name ?? "en";
Task.Run(() => SendPublishEventAsync(mediaData, language));
}
}
private async Task SendPublishEventAsync(IContent content, string language)
{
if (_httpClientFactory == null || _urlResolver == null || _contentLoader == null)
{
Logger.Error("[Webhook] Critical dependencies are missing. Check service locator setup.");
return;
}
var isMedia = content is MediaData;
var contentType = content.GetOriginalType().Name;
var client = _httpClientFactory.CreateClient();
var webhookUrl = "https://carlee-newfangled-suanne.ngrok-free.dev/api/connector/6660800a32e86ecebd04dfd6?projectid=66…;
var relativeUrl = _urlResolver.GetUrl(content.ContentLink, language) ?? "/";
var fullUrl = new Uri("http://localhost:4000" + relativeUrl);
// --- 1. Property Extraction Logic ---
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;
// --- Explicit Complex Type Handling ---
if (property is PropertyContentArea contentAreaProperty && contentAreaProperty.Value is ContentArea contentArea)
{
var contentAreaItems = contentArea.Items
.Select(i =>
{
IContent? referencedContent = null;
_contentLoader.TryGet(i.ContentLink, out 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)
{
IContent? referencedContent = null;
_contentLoader.TryGet(contentRef, out referencedContent);
propertiesPayload[property.Name] = new
{
Id = contentRef.ID,
Guid = referencedContent?.ContentGuid
};
}
// Handle XhtmlString (Always convert to string)
else if (property is PropertyXhtmlString xhtmlProperty)
{
propertiesPayload[property.Name] = xhtmlProperty.ToString();
}
// --- Default Simple Property Handling (The FIX) ---
else
{
// Safely check for known primitive types that the JSON serializer can handle
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
{
// For complex types that cause serialization errors (like the one with ReadOnlySpan),
// fall back to the string representation.
propertiesPayload[property.Name] = property.ToString();
}
}
}
}
catch (Exception ex)
{
Logger.Error($"[Webhook] Error extracting properties for {content.Name}: {ex.Message}", ex);
}
// --- 2. Prepare JSON payload ---
var payload = new
{
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,
MasterLanguage = (content as ILocalizable)?.MasterLanguage?.Name,
Created = (content as IChangeTrackable)?.Created,
Modified = (content as IChangeTrackable)?.Changed,
CreatedBy = (content as IChangeTrackable)?.CreatedBy,
ChangedBy = (content as IChangeTrackable)?.ChangedBy,
StartPublish = (content as IVersionable)?.StartPublish,
StopPublish = (content as IVersionable)?.StopPublish,
Properties = propertiesPayload
};
var json = JsonSerializer.Serialize(payload, new JsonSerializerOptions { WriteIndented = true });
var httpContent = new StringContent(json, Encoding.UTF8, "application/json");
// --- 3. Send Request with better error handling ---
try
{
Logger.Debug($"[Webhook] Sending payload for: {content.Name}");
var response = await client.PostAsync(webhookUrl, httpContent);
if (response.IsSuccessStatusCode)
{
Logger.Information($"[Webhook] Successfully sent event for: {content.Name}");
}
else
{
var responseBody = await response.Content.ReadAsStringAsync();
Logger.Error($"[Webhook] Failed to send event for {content.Name}. Status: {response.StatusCode}. Response: {responseBody}");
}
}
catch (Exception ex)
{
Logger.Error($"[Webhook] Network/HTTP error sending event for {content.Name}: {ex.Message}", ex);
}
}
public void Uninitialize(InitializationEngine context)
{
if (_contentEvents != null)
{
_contentEvents.PublishedContent -= ContentEvents_PublishedContent;
}
}
}
}
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.