World is now on Opti ID! Learn more

sanjay.kumar
Apr 28, 2025
  0
(0 votes)

Optimizely Product Recommendation Troubleshooting

In today’s fast-paced digital landscape, personalization is everything. Customers expect relevant, tailored experiences whenever they interact with a brand — and meeting that expectation can make or break your success. That’s where Optimizely's Product Recommendation feature shines.

What is Optimizely Product Recommendation?

Optimizely’s Product Recommendation feature is a powerful, AI-driven tool designed to help brands offer hyper-personalized product suggestions to their customers. It’s built into the Optimizely Commerce platform and integrates seamlessly with both Commerce Cloud and Customized Commerce (formerly Episerver Commerce).

Instead of relying on static product lists, Optimizely uses machine learning algorithms and real-time customer behavior to dynamically surface the right products to the right audience at the right time.

Why Product Recommendations Matter?

  • Boost Conversion Rates
    Relevant product suggestions keep customers engaged and help them discover products they might not have found otherwise, leading to increased sales.
  • Enhance Customer Experience
    When users feel like a site “understands” them, they’re more likely to stay, browse, and buy.
  • Increase Average Order Value
    Smart cross-selling and upselling through recommendations can encourage customers to add more to their cart.
  • Reduce Bounce Rates
    Presenting enticing alternatives or related items keeps visitors on your site longer.

Troubleshooting

During the implementation, we faced a few challenges, which I have outlined here.

Compatible Package
Before starting, ensure you verify the Commerce version you are using and identify the compatible versions of related packages.
For example, in my case, I was using EPiServer.Commerce 13.33.0, so I installed EPiServer.Personalization.Commerce and EPiServer.Tracking.Commerce version 3.2.37.

Configuration 
If you are using a single catalog, follow the single-site configuration; otherwise, use the multi-site configuration approach. Since I was working with a multi-site, channel-based setup, I made a mistake during the configuration — I used Web instead of web, and similarly Mobile instead of mobile. Please note that these values are case-sensitive, so ensure you use the correct lower-case terms to avoid configuration issues.

Single Site 

<add key="episerver:personalization.BaseApiUrl"  value="https://mysite.uat.productrecs.optimizely.com" />
<add key="episerver:personalization.Site" value="MySite" />
<add key="episerver:personalization.ClientToken" value="MyClientToken" />
<add key="episerver:personalization.AdminToken" value="MyAdminToken" />

Multi-Site:
The multi-site configuration is scope-based. If you are using more than one catalog, you need to set up the configuration in a repeated form as shown below.

<add key="episerver:personalization.ScopeAliasMapping.MyScope" value="SiteId"/>
<add key="episerver:personalization.CatalogNameForFeed.MyScope" value="CatalogName"/>
<add key="episerver:personalization.BaseApiUrl.MyScope"  value="https://sitename.uat.productrecs.episerver.net" />
<add key="episerver:personalization.Site.MyScope" value="sitename" />
<add key="episerver:personalization.AdminToken.MyScope" value="MyAdminToken" />
<add key="episerver:personalization.ClientToken.MyScope.web" value="MyWebToken" />
<add key="episerver:personalization.ClientToken.MyScope.mobile" value="MyMobileToken" />

Sync Specific Pricing (Group Price)

The 'Product Export Job' syncs only the 'All Customers' pricing group. To sync prices for a specific pricing group, use IEntryPriceService to fetch the price for that group.

e.g.

public class DefaultEntryPriceService : IEntryPriceService
{
    private readonly IPromotionEngine _promotionEngine;
    private readonly IMarketService _marketService;
    private readonly IPriceService _priceService;
    private readonly bool _calculateDiscountPrices;
    private readonly IPricingService _pricingService;

    public DefaultEntryPriceService(IPromotionEngine promotionEngine, IMarketService marketService, IPricingService pricingService)
    {
        _promotionEngine = promotionEngine;
        _marketService = marketService;
        _pricingService = pricingService;
        _calculateDiscountPrices = !bool.TryParse(ConfigurationManager.AppSettings["episerver:personalization.CalculateDiscountPrices"], out _calculateDiscountPrices) || _calculateDiscountPrices;
    }

    public IEnumerable<EntryPrice> GetPrices(IEnumerable<EntryContentBase> entries, DateTime validOn, string scope)
    {
        CustomEntryPriceService entryPriceService = this;

        List<CatalogKey> catalogKeys = entries
            .Where(x => x is IPricing)
            .Select(new Func<EntryContentBase, CatalogKey>(entryPriceService.GetCatalogKey))
            .ToList();

        Dictionary<string, ContentReference> codeContentLinkMap = entries.ToDictionary(c => c.Code, c => c.ContentLink);
        IEnumerable<IMarket> source1 = entryPriceService._marketService.GetAllMarkets().Where(x => x.IsEnabled);
        List<IPriceValue> source2 = new List<IPriceValue>();
        foreach (IMarket market in source1)
        {
            foreach (CatalogKey key in catalogKeys)
            {
				//Read price for specific group from your service
                var price = _pricingService.GetConsumerListPricing(key.CatalogEntryCode, market);
                source2.Add(new PriceValue
                {
                    CatalogKey = key,
                    MarketId = market.MarketId,
                    UnitPrice = new Money(price ?? decimal.Zero, market.DefaultCurrency)
                });
            }
        }

        foreach (var priceValue in source2.GroupBy(c => new
        {
            c.CatalogKey,
            c.UnitPrice.Currency
        }).Select(g => g.OrderBy(x => x.UnitPrice.Amount).First()).ToList())
        {
            ContentReference contentLink;
            if (codeContentLinkMap.TryGetValue(priceValue.CatalogKey.CatalogEntryCode, out contentLink))
            {
                Money salePrice = priceValue.UnitPrice;
                if (entryPriceService._calculateDiscountPrices)
                {
                    var market = _marketService.GetMarket(priceValue.MarketId);
					
					//Read Discounted Price
                    var discountPrice = _pricingService.GetConsumerDiscountPricing(
                        priceValue.CatalogKey.CatalogEntryCode,
                        market) ?? decimal.Zero;

                    salePrice = discountPrice == 0
                        ? priceValue.UnitPrice
                        : new Money(discountPrice, market.DefaultCurrency);
                }

                yield return new EntryPrice(contentLink, priceValue.UnitPrice, salePrice);
            }
        }
    }

    private CatalogKey GetCatalogKey(EntryContentBase entryContent)
    {
        return new CatalogKey(entryContent.Code);
    }
}

Target Specific Markets:
When targeting products for specific markets, use ICatalogItemFilter to filter the products from your catalog before syncing the feed into product recommendations.

In our case, for one of the sites, we are targeting more than 16 markets. However, we encountered an issue syncing the feed for the China market, as the Unicode product URLs were not compatible with the product recommendations feed (Optimizely need to fix the Unicode issue). As a result, we decided to launch this feature for specific markets only.

e.g.

 

 public class DefaultCatalogItemFilter : ICatalogItemFilter
 {
     private readonly IPublishedStateAssessor _publishedStateAssessor;
     private List<string> languages = new List<string>() { "en", "en-CA", "fr-CA" };

     public DefaultCatalogItemFilter(IPublishedStateAssessor publishedStateAssessor)
     {
         _publishedStateAssessor = publishedStateAssessor;
     }

     public bool ShouldFilter(CatalogContentBase content, string scope)
     {
         if (_publishedStateAssessor.IsPublished(content, PublishedStateCondition.None) &&
             languages.Any(x => string.Equals(x, content.Language.Name, StringComparison.InvariantCultureIgnoreCase)))
         {
             return false;
         }

         return !_publishedStateAssessor.IsPublished(content, PublishedStateCondition.None);
     }
 }

 

Sync Specific Attributes in the Product Feed
To target specific attributes for syncing into the feed for catalog entries, use IEntryAttributeService. This service helps you apply certain rules to control how products are displayed in the recommendation area.

In our case, we are using a single codebase for over 14 catalogs. However, the products and variants have some uncommon properties that cannot be included in the feed for other sites.

public class DefaultEntryAttributeService : IEntryAttributeService
{
    private readonly CatalogFeedSettings _catalogFeedSettings;
    private readonly IVariantAttributeService _variantAttributeService;

    public DefaultEntryAttributeService(CatalogFeedSettings catalogFeedSettings, IVariantAttributeService variantAttributeService)
    {
        _catalogFeedSettings = catalogFeedSettings;
        _variantAttributeService = variantAttributeService;
    }

    public bool CanBeRecommended(EntryContentBase content, decimal stock, string scope)
    {
        return stock > 0M;
    }

    public IDictionary<string, string> GetAttributes(EntryContentBase content, string scope)
    {
        List<string> userMetaFieldNames = GetUserMetaFields(content).ToList();
        Dictionary<string, string> attributes = new Dictionary<string, string>();

        if (!userMetaFieldNames.Any())
            return attributes;

		// Read attributes from your list that you would like to exclude from feed
        _catalogFeedSettings.ExcludedAttributes = RecommendationHelper.ExcludeAttributes; 

        HashSet<string> excludedAttributes = new HashSet<string>(_catalogFeedSettings.ExcludedAttributes, StringComparer.OrdinalIgnoreCase);

        foreach (PropertyData propertyData in content.Property.Where(x => IsValidContentProperty(x, userMetaFieldNames, excludedAttributes)))
        {
            attributes.Add(propertyData.Name, propertyData.Value.ToString());
        }

        if (content is MyVariant variant)
        {
            foreach (var attr in RecommendationHelper.IncludeAttributes) // Include specific attributes
            {
                if (attributes.Any(x => string.Equals(x.Key, attr, StringComparison.OrdinalIgnoreCase)))
                    continue;

				// Read the value for attribute if exists in catalog entries
                var value = _variantAttributeService.GetAttributeValueByName(attr, variant); 
                if (!string.IsNullOrWhiteSpace(value))
                {
                    attributes.Add(attr, value);
                }
            }            
        }

        return attributes;
    }

    public string GetDescription(EntryContentBase entryContent, string scope)
    {
        return entryContent[_catalogFeedSettings.DescriptionPropertyName]?.ToString();
    }

    public string GetTitle(EntryContentBase entryContent, string scope)
    {
        return !string.IsNullOrEmpty(entryContent.DisplayName) ? entryContent.DisplayName : entryContent.Name;
    }

    private bool IsValidContentProperty(
       PropertyData property,
       IEnumerable<string> userMetaFieldNames,
       HashSet<string> excludedAttributes)
    {
        return userMetaFieldNames.Any(x => x.Equals(property.Name))
            && !property.Name.Equals("_ExcludedCatalogEntryMarkets")
            && !property.Name.Equals("DisplayName")
            && !property.Name.Equals("ContentAssetIdInternal")
            && !property.Name.StartsWith("Epi_")
            && property.Value != null
            && !excludedAttributes.Contains(property.Name);
    }

    public IEnumerable<string> GetUserMetaFields(EntryContentBase content)
    {
        var metaClass = Mediachase.MetaDataPlus.Configurator.MetaClass.Load(new MetaDataContext()
        {
            UseCurrentThreadCulture = false,
            Language = content.Language.Name
        }, content.MetaClassId);

        return 
             metaClass != null 
            ? metaClass.GetUserMetaFields().Select(x => x.Name).ToList() 
            : null ?? Enumerable.Empty<string>();
    }
}

Product Page Tracking

Use the product code instead of the variant code to track the product.

 

If you're encountering any challenges with the native implementation, feel free to reach out for assistance.

Cheers!

Apr 28, 2025

Comments

Please login to comment.
Latest blogs
Make Global Assets Site- and Language-Aware at Indexing Time

I had a support case the other day with a question around search on global assets on a multisite. This is the result of that investigation. This co...

dada | Jun 26, 2025

The remote server returned an error: (400) Bad Request – when configuring Azure Storage for an older Optimizely CMS site

How to fix a strange issue that occurred when I moved editor-uploaded files for some old Optimizely CMS 11 solutions to Azure Storage.

Tomas Hensrud Gulla | Jun 26, 2025 |

Enable Opal AI for your Optimizely products

Learn how to enable Opal AI, and meet your infinite workforce.

Tomas Hensrud Gulla | Jun 25, 2025 |

Deploying to Optimizely Frontend Hosting: A Practical Guide

Optimizely Frontend Hosting is a cloud-based solution for deploying headless frontend applications - currently supporting only Next.js projects. It...

Szymon Uryga | Jun 25, 2025

World on Opti ID

We're excited to announce that world.optimizely.com is now integrated with Opti ID! What does this mean for you? New Users:  You can now log in wit...

Patrick Lam | Jun 22, 2025

Avoid Scandinavian Letters in File Names in Optimizely CMS

Discover how Scandinavian letters in file names can break media in Optimizely CMS—and learn a simple code fix to automatically sanitize uploads for...

Henning Sjørbotten | Jun 19, 2025 |