allBlogsList

Showing Recently Viewed Products in Sitecore SXA - Part 1


Storing a collection of recently viewed products for each visitor to your website is a great way to increase engagement and sales for your store. We will look at two different ways you can easily add this feature to a Sitecore SXA website. First, we’ll see how you can do this on the server side, using the SXA API to quickly generate the product data. In our next post, we’ll see how you can better align with SXA to do this with a client-side call.

Storing Product IDs

Obviously, we want to store each viewed item in a list, so it can later be retrieved and presented to the user. There are many ways to approach this, but to keep things simple, we’ll just put them in a pipe-delimited string in a cookie. To facilitate this, I’ve created a simple set of extension methods to the HttpContext class, so we can easily store and retrieve a cookie by name:

    public static class HttpContextExtensions
    {
        public static void SetCookie(this HttpContext context, string key, string value, int dayExpires)
        {
            context.Response.Cookies[key].Value = value;
            context.Response.Cookies[key].Expires = DateTime.Now.AddDays(dayExpires);
        }

        public static void SetCookie(this HttpContext context, string key, string value)
        {
            SetCookie(context, key, value, 360);
        }

        public static string GetCookie(this HttpContext context, string key)
        {
            if (context.Request.Cookies[key] != null)
            {
                return context.Request.Cookies[key].Value;
            }
            else
                return null;
        }
    }


Now that we have a way to update the list of viewed products, we can proceed to create the component to show them.

Recently Viewed Products Component

Our component actually needs to perform two operations: 1) List the recently viewed products and 2) Update the list when a product is viewed. We’ll be placing this component directly on the product page, so fortunately it can perform both by retrieving the currently viewed item and updating the history accordingly.

If no item is found, we simply do nothing, but still render out the product list. This allows us to reuse the component in other areas (such as the Cart page).

Just like our previous ratings and reviews component, we need to leverage several SXA services, resolving them either by injecting them into the constructor of our controller, or using a Dependency Resolver, both of which are outlined in the full code sample below.

Also note that we inherit from BaseCommerceStandardController, which includes a reference to the current StorefrontContext.

In our controller action, we want to first check if we’re looking at a product, so we can add the SKU to our history. However, since this is added to the current history, we also want to skip it when rendering, so the same product you’re viewing isn’t seen in the history list.

Finally, we just need to iterate through the history by splitting the string and loading the product info for each SKU, generating a list of CatalogItemRenderingModel items that have the properties we need to present the list on the frontend.

Here is the complete sample for our controller:

    public class ProductController : BaseCommerceStandardController
    {
        ISiteContext siteContext;
        IVisitorContext visitorContext;
        IProductInformationRepository productInfoRepo;
        ISearchManager searchManager;
        IModelProvider modelProvider;
        ICatalogManager catalogManager;

        public ProductController() : base()
        {
            this.siteContext = DependencyResolver.Current.GetService();
            this.visitorContext = DependencyResolver.Current.GetService();
            this.productInfoRepo = DependencyResolver.Current.GetService();
            this.modelProvider = DependencyResolver.Current.GetService();
            this.searchManager = DependencyResolver.Current.GetService() ?? new SearchManager(this.StorefrontContext, this.SitecoreContext);
            this.StorefrontContext = DependencyResolver.Current.GetService();
            this.catalogManager = DependencyResolver.Current.GetService();
        }
        public ProductController(ISiteContext siteContext, IVisitorContext visitorContext, IProductInformationRepository productInfoRepo,
            ISearchManager searchManager, IModelProvider modelProvider, ICatalogManager catalogManager)
        {
            this.siteContext = siteContext;
            this.visitorContext = visitorContext;
            this.productInfoRepo = productInfoRepo;
            this.modelProvider = modelProvider;
            this.searchManager = searchManager;
            this.StorefrontContext = StorefrontContext;
            this.catalogManager = catalogManager;
        }

        public ActionResult RecentlyViewedProducts()
        {
            // get the product history from cookie
            var recentProducts = System.Web.HttpContext.Current.GetCookie(Constants.Products.RecentlyViewedProducts) ?? string.Empty;
            var recentSkus = recentProducts.Split("|".ToCharArray(), StringSplitOptions.RemoveEmptyEntries).ToList();

            // if we are vieweing a product, update the history
            CatalogItemRenderingModel currentProductModel = null;
            Item currentCatalogItem = this.siteContext.CurrentCatalogItem;
            if (currentCatalogItem != null)
            {
                currentProductModel = this.productInfoRepo.GetProductInformationRenderingModel(this.visitorContext);
                if (currentProductModel != null)
                {
                    // if this is the first time seeing it, add it
                    if (string.IsNullOrEmpty(recentProducts))
                    {
                        recentSkus.Add(currentProductModel.ProductId);
                    }
                    else
                    {
                        // if we've seen it before, move it to the front of the list
                        if (recentSkus.Contains(currentProductModel.ProductId))
                        {
                            recentSkus.Remove(currentProductModel.ProductId);
                        }

                        recentSkus.Insert(0, currentProductModel.ProductId);
                    }

                    System.Web.HttpContext.Current.SetCookie(Constants.Products.RecentlyViewedProducts, string.Join("|", recentSkus));
                }
            }

            
            List recentProductModels = new List();

            // get actual products from the history
            foreach (var sku in recentSkus)
            {
                // skip current product being viewed, if any
                if (currentProductModel != null && sku.Equals(currentProductModel.ProductId, StringComparison.OrdinalIgnoreCase)) continue;

                var productItem = searchManager.GetProduct(sku, this.StorefrontContext.CurrentStorefront.Catalog);
                if (productItem != null)
                {
                    // initialize a product item from the product ID
                    var productEntity = new ProductEntity();
                    productEntity.Initialize(this.StorefrontContext.CurrentStorefront, productItem);
                    catalogManager.GetProductPrice(StorefrontContext.CurrentStorefront, visitorContext, productEntity);

                    var productModel = modelProvider.GetModel();
                    productModel.Initialize(productEntity, false);
                    if (productModel != null && !recentProductModels.Any(p => p.ProductId == productModel.ProductId))
                    {
                        recentProductModels.Add(productModel);
                    }
                }
            }


            

            return View(recentProductModels);
        }
    }

Now we simply need a View to iterate through this list, and present the products, linking them to their individual product pages:

@model List<Sitecore.Commerce.XA.Feature.Catalog.Models.CatalogItemRenderingModel>

@if (Model != null && Model.Any())
{
    <div class="col-xs-12">
        <h3 class="recent-products-title">Recently Viewed Products</h3>
    </div>

    foreach (var product in Model)
    {
        <div class="col-md-3 col-xs-6 recent-product">
            <div class="photo">
                <a href="@product.Link">
                    <img class="img-responsive" src="@(product.Images.Any() ? Sitecore.Resources.Media.MediaManager.GetMediaUrl(product.Images.FirstOrDefault(), new Sitecore.Resources.Media.MediaUrlOptions() { AlwaysIncludeServerUrl = true }) : "")">
                </a>
            </div>

            <div class="product-info">
                <h4 class="product-title">
                    <a href="@product.Link">@product.DisplayName</a>
                </h4>

                <div class="current-price">@product.ListPriceWithCurrency</div>

            </div>
        </div>


Now every product you view will update the cookie and reveal product history as you browse, allowing you to click through to previous products.

 

Recently-Viewed-Products-Server-Side

One thing you may notice from this screenshot is that the appearance of these products are different from those in the rest of SXA, which normally have a more colorful layout:

  

SXA-Promoted-Products

This promo component is part of SXA, and has a specific layout, markup, and design that relies on Knockout JS to load data. Although we could generate markup to match this component to keep the style, a better approach might be to just build this component in that style.

That will be the focus of our next post. Until then, as always, thanks for reading and I hope this was helpful!