Episerver (Optimizely)

現在のOptimizely CMS。コンテンツ管理と実験・最適化機能を統合したプラットフォーム。

CMSエンタープライズ.NETC#実験最適化パーソナライゼーションクラウドネイティブ
ライセンス
Commercial
言語
.NET/C#
料金
有料のみ

CMS

Episerver (Optimizely)

概要

Episerver(現Optimizely CMS)は、コンテンツ管理と実験・最適化機能を統合したクラウドネイティブなエンタープライズCMSプラットフォームです。

詳細

Optimizely CMS(旧Episerver)は、2020年にEpiserverがOptimizelyを買収し、2021年にOptimizelyとしてリブランドされた統合デジタルエクスペリエンスプラットフォームです。.NET/C#ベースで構築され、CMSと実験プラットフォームが深く統合されているのが最大の特徴です。A/Bテスト、多変量テスト、フィーチャー実験をCMS内で直接実行でき、データドリブンな意思決定を支援します。2024年には新しいVisual Builderベータ版が導入され、マーケターがドラッグ&ドロップで簡単にページを作成・編集できるようになりました。Blue-Green Deploymentによる安全な大規模変更テスト、CMP統合によるコンテンツ制作ワークフローの効率化など、エンタープライズ向けの高度な機能を提供します。9000社以上の企業に採用され、特に実験駆動の最適化を重視する組織で高い評価を得ています。

メリット・デメリット

メリット

  • 統合された実験プラットフォーム: CMS内で直接A/Bテストを実施
  • Visual Builder: ドラッグ&ドロップでの直感的なページ作成
  • 高速な実験実行: エッジでの実験実行による高速UX
  • AI駆動の最適化: Stats Acceleratorによる実験の高速化
  • クラウドネイティブ: フルマネージドのSaaS環境
  • 包括的なDXP: コンテンツ、コマース、実験を統合
  • 24/7サポート: エンタープライズグレードのサポート体制

デメリット

  • 高額な価格設定: 年間$50,000以上のコスト
  • Microsoft技術依存: .NET/C#環境に限定
  • カスタマイズの制限: SaaS環境での制約
  • 専門知識が必要: 効果的な実験には専門家が必要
  • 学習曲線: 統合プラットフォームの習得に時間が必要
  • ベンダーロックイン: Optimizelyエコシステムへの依存

主要リンク

使い方の例

プロジェクトセットアップ

# Optimizely CLIのインストール
dotnet tool install -g EPiServer.Net.Cli

# 新規プロジェクトの作成
dotnet new epi-alloy-mvc -n MyOptimizelyProject
cd MyOptimizelyProject

# NuGetパッケージの復元
dotnet restore

# データベースの初期化
dotnet-episerver create-cms-database

# 開発サーバーの起動
dotnet run

コンテンツタイプの定義

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テストの実装

// 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)
        {
            // A/Bテストのバリエーション決定
            var variation = _experimentationService.GetVariation("product-page-layout");
            
            var model = new ProductPageViewModel
            {
                CurrentPage = currentPage,
                LayoutVariation = variation,
                RecommendedProducts = GetRecommendedProducts(currentPage)
            };
            
            // バリエーションに基づいてビューを選択
            var viewName = variation == "B" ? "ProductPageAlternative" : "Index";
            
            // コンバージョンイベントのトラッキング
            if (User.Identity.IsAuthenticated)
            {
                _experimentationService.Track("product-view", new
                {
                    productId = currentPage.ContentLink.ID,
                    price = currentPage.Price,
                    variation = variation
                });
            }
            
            return View(viewName, model);
        }
    }
}

Visual Builderコンポーネント

// 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'
};

パーソナライゼーション実装

// 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)
    {
        // 訪問者プロファイルの取得
        var profile = await _profileStore.GetProfileAsync(visitorId);
        
        // 機械学習による興味の予測
        var interests = PredictInterests(profile);
        
        // パーソナライズされたコンテンツの選択
        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)
    {
        // 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;
    }
}

実験データの分析

// 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)
            });
        }
        
        // 勝者の判定
        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
        };
    }
}

コンテンツ配信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
        });
    }
}