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
@if (Model != null && Model.Any())
{
Recently Viewed Products
foreach (var product in Model)
{
@product.DisplayName
@product.ListPriceWithCurrency
Now every product you view will update the cookie and reveal product history as you browse, allowing you to click through to previous products.
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:
?
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!