Episerver (Optimizely)
Now Optimizely CMS. Platform integrating content management with experimentation and optimization features.
CMS
Episerver (Optimizely)
Overview
Episerver (now Optimizely CMS) is a cloud-native enterprise CMS platform that integrates content management with experimentation and optimization features.
Details
Optimizely CMS (formerly Episerver) is an integrated digital experience platform created when Episerver acquired Optimizely in 2020 and rebranded as Optimizely in 2021. Built on .NET/C#, its defining feature is the deep integration between CMS and experimentation platform. It enables A/B testing, multivariate testing, and feature experiments directly within the CMS, supporting data-driven decision making. In 2024, the new Visual Builder beta was introduced, allowing marketers to easily create and edit pages with drag-and-drop functionality. It provides advanced enterprise features including Blue-Green Deployment for safe large-scale change testing and CMP integration for efficient content production workflows. Adopted by over 9,000 companies, it has received high acclaim particularly from organizations that prioritize experiment-driven optimization.
Pros and Cons
Pros
- Integrated experimentation platform: Run A/B tests directly within the CMS
- Visual Builder: Intuitive page creation with drag-and-drop
- Fast experiment execution: High-speed UX with edge-based experiment execution
- AI-driven optimization: Accelerated experiments with Stats Accelerator
- Cloud-native: Fully managed SaaS environment
- Comprehensive DXP: Integrated content, commerce, and experimentation
- 24/7 support: Enterprise-grade support system
Cons
- High pricing: Costs of $50,000+ annually
- Microsoft technology dependency: Limited to .NET/C# environment
- Customization limitations: Constraints in SaaS environment
- Requires expertise: Specialists needed for effective experimentation
- Learning curve: Time needed to master the integrated platform
- Vendor lock-in: Dependency on Optimizely ecosystem
Key Links
- Optimizely Official Site
- Optimizely Documentation
- Optimizely Academy
- Optimizely Community
- Optimizely Support
- Optimizely Blog
Usage Examples
Project Setup
# Install Optimizely CLI
dotnet tool install -g EPiServer.Net.Cli
# Create new project
dotnet new epi-alloy-mvc -n MyOptimizelyProject
cd MyOptimizelyProject
# Restore NuGet packages
dotnet restore
# Initialize database
dotnet-episerver create-cms-database
# Start development server
dotnet run
Content Type Definition
using EPiServer.Core;
using EPiServer.DataAbstraction;
using EPiServer.DataAnnotations;
using System.ComponentModel.DataAnnotations;
namespace MyOptimizelyProject.Models.Pages
{
[ContentType(
DisplayName = "Product Page",
GUID = "19671657-B684-4D95-A61F-8DD4FE60D559",
Description = "Page template for product information"
)]
[ImageUrl("~/Static/gfx/product-page.png")]
public class ProductPage : PageData
{
[Display(
Name = "Product Name",
Description = "The name of the product",
GroupName = SystemTabNames.Content,
Order = 10
)]
public virtual string ProductName { get; set; }
[Display(
Name = "Product Description",
Description = "Detailed product description",
GroupName = SystemTabNames.Content,
Order = 20
)]
[UIHint(UIHint.Textarea)]
public virtual string Description { get; set; }
[Display(
Name = "Price",
GroupName = SystemTabNames.Content,
Order = 30
)]
[Required]
public virtual decimal Price { get; set; }
[Display(
Name = "Product Image",
GroupName = SystemTabNames.Content,
Order = 40
)]
[UIHint(UIHint.Image)]
public virtual ContentReference ProductImage { get; set; }
[Display(
Name = "In Stock",
GroupName = SystemTabNames.Content,
Order = 50
)]
public virtual bool InStock { get; set; }
}
}
A/B Testing Implementation
// Controllers/ProductPageController.cs
using EPiServer.Web.Mvc;
using Optimizely.Web.Experimentation;
using Microsoft.AspNetCore.Mvc;
namespace MyOptimizelyProject.Controllers
{
public class ProductPageController : PageController<ProductPage>
{
private readonly IExperimentationService _experimentationService;
public ProductPageController(IExperimentationService experimentationService)
{
_experimentationService = experimentationService;
}
public IActionResult Index(ProductPage currentPage)
{
// Determine A/B test variation
var variation = _experimentationService.GetVariation("product-page-layout");
var model = new ProductPageViewModel
{
CurrentPage = currentPage,
LayoutVariation = variation,
RecommendedProducts = GetRecommendedProducts(currentPage)
};
// Select view based on variation
var viewName = variation == "B" ? "ProductPageAlternative" : "Index";
// Track conversion events
if (User.Identity.IsAuthenticated)
{
_experimentationService.Track("product-view", new
{
productId = currentPage.ContentLink.ID,
price = currentPage.Price,
variation = variation
});
}
return View(viewName, model);
}
}
}
Visual Builder Component
// components/HeroBlock.tsx
import React from 'react';
import { BlockComponent, EditableText, EditableImage } from '@optimizely/visual-builder';
interface HeroBlockProps {
heading: string;
subheading: string;
backgroundImage: string;
ctaText: string;
ctaUrl: string;
}
export const HeroBlock: BlockComponent<HeroBlockProps> = ({
heading,
subheading,
backgroundImage,
ctaText,
ctaUrl
}) => {
return (
<div
className="hero-block"
style={{ backgroundImage: `url(${backgroundImage})` }}
>
<div className="hero-content">
<EditableText
field="heading"
tag="h1"
value={heading}
/>
<EditableText
field="subheading"
tag="p"
value={subheading}
/>
<a href={ctaUrl} className="cta-button">
<EditableText
field="ctaText"
value={ctaText}
/>
</a>
</div>
<EditableImage
field="backgroundImage"
value={backgroundImage}
className="hero-background"
/>
</div>
);
};
// Block configuration
HeroBlock.displayName = 'Hero Block';
HeroBlock.description = 'A hero section with background image and CTA';
HeroBlock.defaultProps = {
heading: 'Welcome to Our Site',
subheading: 'Discover amazing products',
ctaText: 'Shop Now',
ctaUrl: '/products'
};
Personalization Implementation
// Services/PersonalizationService.cs
using EPiServer.Personalization;
using Optimizely.Data.Platform;
public class PersonalizationService
{
private readonly IProfileStore _profileStore;
private readonly IContentRepository _contentRepository;
public async Task<IEnumerable<ContentReference>> GetPersonalizedContent(
string visitorId,
string contentType)
{
// Get visitor profile
var profile = await _profileStore.GetProfileAsync(visitorId);
// Predict interests using machine learning
var interests = PredictInterests(profile);
// Select personalized content
var criteria = new PropertyCriteria
{
Name = "Tags",
Type = PropertyDataType.String,
Value = string.Join(",", interests),
Condition = CompareCondition.Contained
};
var pages = _contentRepository.FindPagesWithCriteria(
ContentReference.StartPage,
new PropertyCriteriaCollection { criteria }
);
return pages.Select(p => p.ContentLink);
}
private List<string> PredictInterests(VisitorProfile profile)
{
// Interest prediction using Adaptive Audiences
var predictedInterests = new List<string>();
if (profile.BrowsingHistory.Any(h => h.Category == "electronics"))
predictedInterests.Add("technology");
if (profile.PurchaseHistory.Sum(p => p.Amount) > 1000)
predictedInterests.Add("premium");
return predictedInterests;
}
}
Experiment Data Analysis
// Analytics/ExperimentAnalyzer.cs
public class ExperimentAnalyzer
{
private readonly IExperimentRepository _experimentRepository;
public async Task<ExperimentResults> AnalyzeExperiment(string experimentId)
{
var experiment = await _experimentRepository.GetExperimentAsync(experimentId);
var results = new ExperimentResults();
foreach (var variation in experiment.Variations)
{
var metrics = await CalculateMetrics(experimentId, variation.Id);
results.VariationResults.Add(new VariationResult
{
VariationId = variation.Id,
VariationName = variation.Name,
ConversionRate = metrics.ConversionRate,
AverageOrderValue = metrics.AverageOrderValue,
StatisticalSignificance = CalculateSignificance(metrics),
Improvement = CalculateImprovement(metrics, results.Control)
});
}
// Determine winner
results.Winner = DetermineWinner(results.VariationResults);
return results;
}
private async Task<VariationMetrics> CalculateMetrics(
string experimentId,
string variationId)
{
var events = await _experimentRepository.GetEventsAsync(
experimentId,
variationId
);
return new VariationMetrics
{
Visitors = events.Select(e => e.VisitorId).Distinct().Count(),
Conversions = events.Count(e => e.EventType == "conversion"),
Revenue = events.Where(e => e.EventType == "purchase")
.Sum(e => e.Value),
ConversionRate = (double)conversions / visitors * 100,
AverageOrderValue = revenue / conversions
};
}
}
Content Delivery API
// API/ContentDeliveryController.cs
[ApiController]
[Route("api/content")]
public class ContentDeliveryController : ControllerBase
{
private readonly IContentRepository _contentRepository;
private readonly IContentSerializer _contentSerializer;
[HttpGet("{contentId}")]
public async Task<IActionResult> GetContent(
int contentId,
[FromQuery] string language = "en")
{
var content = _contentRepository.Get<IContent>(
new ContentReference(contentId),
new LanguageSelector(language)
);
if (content == null)
return NotFound();
var serialized = _contentSerializer.Serialize(content, new ContentSerializerSettings
{
IncludePersonalization = true,
ExpandReferences = true,
MaxDepth = 3
});
return Ok(serialized);
}
[HttpPost("search")]
public async Task<IActionResult> SearchContent([FromBody] SearchRequest request)
{
var searchService = ServiceLocator.Current.GetInstance<IContentSearchService>();
var results = searchService.Search(request.Query)
.Filter(x => x.MatchType(typeof(PageData)))
.Filter(x => x.ExcludeDeleted())
.OrderByDescending(x => x.SearchScore())
.Take(request.PageSize)
.Skip((request.Page - 1) * request.PageSize)
.GetContentResult();
return Ok(new
{
results = results.Select(r => _contentSerializer.Serialize(r)),
totalCount = results.TotalMatching,
facets = results.Facets
});
}
}