Skip to content

Commerce with CMS Integration

⏱ 35 minutes advanced
📜Advancedcommercecms

A product catalog alone does not sell products. Customers need context — editorial descriptions, lifestyle images, comparison guides, buying advice, and cross-sell suggestions. This content lives naturally in the CMS.

When Commerce and CMS operate separately, you end up maintaining product information in two places: the catalog for transactional data (price, SKU, inventory) and a separate content system for the story around the product. This duplication creates inconsistencies and slows time-to-market.

Optimizely’s Commerce-CMS integration solves this by letting catalog data and CMS content coexist on the same page. A product page pulls price and availability from the catalog while content authors add editorial blocks, videos, and cross-sell recommendations through the CMS editor. One system, one editing experience, one deployment pipeline.

  1. Create CMS page types that display catalog products
  2. Build a shared content model for product pages
  3. Render catalog data alongside editorial content
  4. Set up catalog-aware navigation and routing

Commerce content types inherit from specialized base classes that give them access to catalog data. A product page, for example, inherits from your product content type and gains CMS rendering capabilities.

A product page type with CMS properties
csharp
using EPiServer.Commerce.Catalog.ContentTypes;
using EPiServer.Core;
using EPiServer.DataAnnotations;
using System.ComponentModel.DataAnnotations;

namespace MySite.Models.Catalog
{
    [ContentType(
        DisplayName = "Product Page",
        GUID = "a1b2c3d4-e5f6-7890-abcd-ef0123456789",
        Description = "A product with catalog data "
            + "and editorial content")]
    public class ProductPage : ProductContent
    {
        // Editorial content managed by authors
        [Display(Name = "Marketing Description",
            GroupName = "Editorial", Order = 10)]
        public virtual XhtmlString
            MarketingDescription { get; set; }

        [Display(Name = "Product Video",
            GroupName = "Editorial", Order = 20)]
        public virtual ContentReference
            ProductVideo { get; set; }

        [Display(Name = "Related Content",
            GroupName = "Editorial", Order = 30)]
        public virtual ContentArea
            RelatedContentArea { get; set; }

        [Display(Name = "Size Guide",
            GroupName = "Editorial", Order = 40)]
        public virtual ContentReference
            SizeGuide { get; set; }

        // Catalog data (inherited from ProductContent)
        // - Code, DisplayName, Prices, Inventory
        // are all available automatically
    }
}

The key principle: Catalog data (price, SKU, stock) comes from the catalog subsystem. Editorial data (marketing copy, videos, guides) comes from CMS properties. Both appear on the same page, edited through the same interface.

Plan how catalog data and CMS content interact across your storefront.

Data typeSourceManaged byExample
Product nameCatalogProduct managers”Trail Runner Pro”
Price and currencyPricing serviceCommerce admins$129.99 USD
Stock availabilityInventory serviceWarehouse systems”In Stock”
Marketing descriptionCMS propertyContent authorsRich HTML with lifestyle imagery
Cross-sell productsCMS content areaContent authors / Recommendations”You might also like” block
Size guideCMS page referenceContent authorsLink to shared size guide page
ReviewsExternal serviceCustomersThird-party review widget

This separation means product managers update prices and inventory without touching content, while content authors craft the product story without worrying about transactional data.

Render catalog data alongside editorial content

Section titled “Render catalog data alongside editorial content”

Your product page controller and view combine data from both sources.

Product page controller
csharp
using EPiServer.Commerce.Catalog.ContentTypes;
using EPiServer.Commerce.Order;
using EPiServer.Web.Mvc;
using Mediachase.Commerce;

public class ProductPageController
    : ContentController<ProductPage>
{
    private readonly ICurrentMarket _currentMarket;
    private readonly IPriceService _priceService;
    private readonly IInventoryService _inventoryService;
    private readonly IContentLoader _contentLoader;

    public ProductPageController(
        ICurrentMarket currentMarket,
        IPriceService priceService,
        IInventoryService inventoryService,
        IContentLoader contentLoader)
    {
        _currentMarket = currentMarket;
        _priceService = priceService;
        _inventoryService = inventoryService;
        _contentLoader = contentLoader;
    }

    public ActionResult Index(ProductPage currentPage)
    {
        var market = _currentMarket.GetCurrentMarket();

        // Load variants for this product
        var variants = _contentLoader
            .GetChildren<VariationContent>(
                currentPage.ContentLink)
            .ToList();

        // Get pricing for all variants
        var prices = variants.Select(v => new
        {
            Variant = v,
            Price = GetDefaultPrice(v, market)
        }).ToList();

        var viewModel = new ProductPageViewModel
        {
            CurrentPage = currentPage,
            Variants = prices,
            InStock = CheckAvailability(variants)
        };

        return View(viewModel);
    }
}
Product page view (Razor)
html
@model ProductPageViewModel

<article class="product-page">
    <h1>@Model.CurrentPage.DisplayName</h1>

    <!-- Catalog data: price and availability -->
    <div class="product-pricing">
        <span class="price">
            @Model.DefaultPrice.ToString("C")
        </span>
        <span class="stock-status">
            @(Model.InStock
                ? "In Stock"
                : "Out of Stock")
        </span>
    </div>

    <!-- Editorial content from CMS -->
    <div class="product-story">
        @Html.PropertyFor(
            m => m.CurrentPage.MarketingDescription)
    </div>

    <!-- Variant selector -->
    <div class="variant-picker">
        @foreach (var v in Model.Variants)
        {
            <button data-code="@v.Variant.Code">
                @v.Variant.DisplayName - @v.Price
            </button>
        }
    </div>

    <!-- CMS content area for cross-sells -->
    <div class="related-content">
        @Html.PropertyFor(
            m => m.CurrentPage.RelatedContentArea)
    </div>
</article>

Commerce catalog entries need URLs and navigation structures that work with your CMS site. Commerce provides built-in routing for catalog content, but you need to configure how catalog URLs map to your site structure.

Catalog routing configuration
csharp
using EPiServer.Commerce.Routing;

public class CatalogRouteConfig
{
    public static void RegisterRoutes()
    {
        // Map catalog content to a CMS start page
        // This tells Commerce where in the CMS tree
        // catalog content should appear
        CatalogRouteHelper.MapDefaultHierarchialRouter(
            RouteTable.Routes, false);
    }
}

// In your site's initialization module:
[InitializableModule]
public class CommerceInitialization
    : IInitializableModule
{
    public void Initialize(
        InitializationEngine context)
    {
        CatalogRouteConfig.RegisterRoutes();
    }

    public void Uninitialize(
        InitializationEngine context) { }
}

Routing decisions:

PatternURL exampleBest for
Hierarchical/shop/shoes/running/trail-runner-proSEO-friendly, mirrors catalog structure
Flat/products/trail-runner-proSimple catalogs, avoids deep nesting
CMS-driven/gear/best-trail-shoes (CMS page that references product)Maximum editorial control over URLs

With Commerce-CMS integration, content authors work in a single editor but interact with two data sources. Make this seamless by:

  • Grouping catalog properties (price, SKU) separately from editorial properties in the editing interface
  • Using [Display(GroupName = "Editorial")] to create clear tabs in the editor
  • Setting sensible defaults so products are displayable even without editorial content

Product pages combine data with different cache lifetimes:

DataChanges how oftenCache strategy
Editorial contentInfrequentlyStandard CMS output cache (hours/days)
PricingOccasionallyShort cache with market-specific variation (minutes)
InventoryFrequentlyNo cache or very short TTL (seconds)

Use a layered caching approach: cache the editorial shell of the page, and load pricing and inventory through AJAX calls or edge-side includes to keep transactional data fresh.

Large catalogs (10,000+ products) require attention to:

  • Catalog indexing — Use Commerce’s built-in search indexing (backed by Optimizely Search or a custom provider) rather than querying the catalog database directly
  • Batch operations — Import and update products in batches rather than one at a time
  • Content delivery — Consider a CDN for product images and static catalog data
IssueCauseFix
Product page returns 404Catalog routing not registeredCall CatalogRouteHelper.MapDefaultHierarchialRouter at startup
Editorial properties not savingContent type does not inherit from ProductContentEnsure your page type extends the correct Commerce base class
Prices not updating on pageAggressive output cachingAdd cache variation by market, or load prices via AJAX
Catalog changes not reflectedContent not re-publishedPublish catalog entries after import or bulk update