Episerver (Optimizely)

Now Optimizely CMS. Platform integrating content management with experimentation and optimization features.

CMSEnterprise.NETC#ExperimentationOptimizationPersonalizationCloud-Native
License
Commercial
Language
.NET/C#
Pricing
Paid only
Official Site
Visit Official Site

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

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
        });
    }
}