Why integrate Commerce with CMS
Section titled “Why integrate Commerce with CMS”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.
What you will do
Section titled “What you will do”- Create CMS page types that display catalog products
- Build a shared content model for product pages
- Render catalog data alongside editorial content
- Set up catalog-aware navigation and routing
Create commerce-aware page types
Section titled “Create commerce-aware page types”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.
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.
Build the shared content model
Section titled “Build the shared content model”Plan how catalog data and CMS content interact across your storefront.
| Data type | Source | Managed by | Example |
|---|---|---|---|
| Product name | Catalog | Product managers | ”Trail Runner Pro” |
| Price and currency | Pricing service | Commerce admins | $129.99 USD |
| Stock availability | Inventory service | Warehouse systems | ”In Stock” |
| Marketing description | CMS property | Content authors | Rich HTML with lifestyle imagery |
| Cross-sell products | CMS content area | Content authors / Recommendations | ”You might also like” block |
| Size guide | CMS page reference | Content authors | Link to shared size guide page |
| Reviews | External service | Customers | Third-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.
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);
}
} @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> Set up catalog-aware navigation
Section titled “Set up catalog-aware navigation”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.
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:
| Pattern | URL example | Best for |
|---|---|---|
| Hierarchical | /shop/shoes/running/trail-runner-pro | SEO-friendly, mirrors catalog structure |
| Flat | /products/trail-runner-pro | Simple catalogs, avoids deep nesting |
| CMS-driven | /gear/best-trail-shoes (CMS page that references product) | Maximum editorial control over URLs |
Architecture considerations
Section titled “Architecture considerations”Content authoring workflow
Section titled “Content authoring workflow”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
Caching strategy
Section titled “Caching strategy”Product pages combine data with different cache lifetimes:
| Data | Changes how often | Cache strategy |
|---|---|---|
| Editorial content | Infrequently | Standard CMS output cache (hours/days) |
| Pricing | Occasionally | Short cache with market-specific variation (minutes) |
| Inventory | Frequently | No 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.
Performance at scale
Section titled “Performance at scale”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
Common issues
Section titled “Common issues”| Issue | Cause | Fix |
|---|---|---|
| Product page returns 404 | Catalog routing not registered | Call CatalogRouteHelper.MapDefaultHierarchialRouter at startup |
| Editorial properties not saving | Content type does not inherit from ProductContent | Ensure your page type extends the correct Commerce base class |
| Prices not updating on page | Aggressive output caching | Add cache variation by market, or load prices via AJAX |
| Catalog changes not reflected | Content not re-published | Publish catalog entries after import or bulk update |