Sanjay Kumar
Apr 28, 2025
  354
(5 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

PuneetGarg
PuneetGarg Apr 28, 2025 03:01 PM

Great Article, Sanjay.
Thank you for sharing.

Dylan Walker
Dylan Walker Apr 29, 2025 10:06 AM

Great Article, Sanjay.

Pleasure working with you on this project and look forward to working on more going forward

Please login to comment.
Latest blogs
Content Compliance Without the Chaos: How Optimizely CMP Empowers Financial Services Marketers

In financial services, content isn’t just about telling your story — it’s about telling it right. Every blog post, product update, or social post i...

abritt | May 22, 2025 |

Opal – Optimizely’s AI-Powered Marketing Assistant

Overview Opal is Optimizely’s AI assistant designed to accelerate and enhance the entire marketing workflow. Integrated natively across...

abritt | May 22, 2025 |

Integrating Address Validation in Optimizely Using Smarty

Address validation is a crucial component of any ecommerce platform. It ensures accurate customer data, reduces shipping errors, and improves the...

PuneetGarg | May 21, 2025

The London Dev Meetup is TOMORROW!!

The rescheduled London Dev Meetup is happening tomorrow, Wednesday, 21st May, at 6pm! This meetup will be Candyspace 's first, and the first one he...

Gavin_M | May 20, 2025