Episerver/Optimizely – Idio Content Recommendations Block from Back End

Episerver/Optimizely – Idio Content Recommendations Block from Back End

If you have used Content Recommendations in Episerver/Optimizely you now for sure that works more like a front end implementation than a backend one. In short, you need to add the Idio script to all the pages you want to track and then create a content recommendation block in the page you want to display the recommendations. In order to configure how the recommendations are going to look you must set your html using a mustache template. You can find more information about content recommendations and how to implement them in the Episerver/Optimizely official documentation here.

Now, if your front end implementation cannot handle this kind of templates properly, you will not be able to use the content recommendations block as it is. This post is about finding an alternative to this implementation so we can use the content recommendations from Idio but without requiring the mustache template and use plain old html with razor views. So without further due lets begin.

First, we identified how the content recommendation block currently works. So adding the block to a page we realized that it calls the endpoint https://api.idio.co/1.0/users/ with some parameters that are part of the content recommendations block and a cookie value called iv.

Using this information we decided to create our own block which inherits from the content recommendations block but has a different view implementation and gets the recommendation from the Api in the backend instead. The block will hide the recommendations template column because we are going to use a razor view.

using EPiServer.Core;
using EPiServer.DataAbstraction;
using EPiServer.DataAnnotations;
using EPiServer.Personalization.Content.UI.FrontEnd.Blocks;
using System.ComponentModel.DataAnnotations;

namespace Foundation.Cms.Features.Common.Blocks.IdioRecommendedContent
{
    [ContentType(DisplayName = "Idio Recommended Content",
        GUID = "01d514cb-b845-4eef-bf97-b531fa1cfd18",
        Description = "Display the user's recommended content using Idio.",
        GroupName = "Content",
        AvailableInEditMode = true)]
    public class IdioRecommendedContentBlock : ContentRecommendationsBlock
    {
        [Required]
        [Display(Name = "Heading", GroupName = "Content", Order = 10)]
        public virtual string Heading { get; set; }

        [ScaffoldColumn(false)]
        public new virtual string RecommendationsTemplate { get; set; }

        #region Accessibility 

        [CultureSpecific]
        [Display(Name = "Module Title", Description = "If not managed, Heading will be used instead.", GroupName ="Accessibility", Order = 10)]
        public virtual string ModuleAriaLabel
        {
            get
            {
                var title = this.GetPropertyValue(p => p.ModuleAriaLabel);

                return !string.IsNullOrWhiteSpace(title)
                    ? title
                    : Heading;
            }
            set => this.SetPropertyValue(p => p.ModuleAriaLabel, value);
        }

        #endregion

        public override void SetDefaultValues(ContentType contentType)
        {
            base.SetDefaultValues(contentType);

            Heading = "Recommended Content";
        }
    }
}

We will also add a new view model which will have this block plus the recommendations we get from the Api.

using System.Collections.Generic;
using Foundation.Cms.Models.Idio;

namespace Foundation.Cms.Features.Common.Blocks.IdioRecommendedContent
{
    public class IdioRecommendedContentBlockViewModel
    {
        public IdioRecommendedContentBlock CurrentBlock { get; set; }

        public List<IdioRecommendation> Recommendations { get; set; }

        public IdioRecommendedContentBlockViewModel(IdioRecommendedContentBlock currentBlock)
        {
            CurrentBlock = currentBlock;
        }
    }
}

The recommendations are going to be passed to the view using the IdioRecommendation POCO model which can be anything that you want. In our case we wanted to show the title, description, Url and the page type. To be able to get that information from your Api you should configure your pages to send that information to Idio. In order to do that you can follow the instructions here.

namespace Foundation.Cms.Models.Idio
{
    public class IdioRecommendation
    {
        public string PageTypeIcon { get; set; }
        public string OgTitle { get; set; }
        public string OgDescription { get; set; }
        public string Url { get; set; }
    }
}

Now, we work on the controller. This code will call our helper class which will get the recommendations from Idio and add them to the view model we just created. Then transform the POCO models from the Idio response to the ones we are going to use in the view. Pay special attention to the information we retrieve from the response. In this case we do not get the title nor the description directly, we get it from the metadata tags from open graph instead. This can change in your implementation.

using EPiServer.Framework.DataAnnotations;
using EPiServer.Web.Mvc;
using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
using Foundation.Cms.ExternalApis.Idio;
using Foundation.Cms.Features.Common.Blocks.IdioRecommendedContent;
using Foundation.Cms.Models.Idio;

namespace Foundation.Website.Features.Common.Blocks.IdioRecommendedContent
{
    [TemplateDescriptor(Default = true)]
    public class RecommendedContentBlockController : BlockController<IdioRecommendedContentBlock>
    {
        public override ActionResult Index(IdioRecommendedContentBlock currentContent)
        {
            var model = new IdioRecommendedContentBlockViewModel(currentContent)
            {
                Recommendations = IdioApiHelper.GetRecommendations(currentContent, HttpContext.Request)
                    ?.content.Select(x => new IdioRecommendation()
                    {
                        OgTitle = x?.metadata?.tags?.og?.title ?? string.Empty,
                        OgDescription = x?.metadata?.tags?.og?.description ?? string.Empty,
                        PageTypeIcon = x?.metadata?.tags?.idio?.page_type_icon ?? string.Empty,
                        Url = x?.link_url ?? string.Empty
                    })?.ToList() ?? new List<IdioRecommendation>()
            };

            return this.PartialView("~/Features/Common/Blocks/IdioRecommendedContent/IdioRecommendedContentBlock.cshtml", model);
        }
    }
}

The central part of all of this is the IdioApiHelper class which will call the Idio Api to get the recommendations. For this we use a variable which is setup in the web.config file, app settings section so if we need to change the Url in other environments we can do it without complications. After getting the recommendations we transform them to the POCO objects we have created for the response called IdioResponse. There is some manual manipulation we had to do before deserializing from json because the response by default starts with idio.r0( and ends with , 200) so we replaced the content with empty strings.

using Newtonsoft.Json;
using System;
using System.IO;
using System.Net.Http;
using System.Text;
using System.Web;
using Foundation.Cms.Features.Common.Blocks.IdioRecommendedContent;
using System.Configuration;
using Foundation.Cms.Models.Idio;

namespace Foundation.Cms.ExternalApis.Idio
{
    public static class IdioApiHelper
    {
        private static readonly string _url = ConfigurationManager.AppSettings["episerver:personalization.content.Environment"];

        public static IdioResponse GetRecommendations(IdioRecommendedContentBlock currentBlock, HttpRequestBase request)
        {
            try
            {
                var httpClient = new HttpClient();
                var ivCookie = HttpContext.Current?.Request.Cookies["iv"]?.Value;

                var result = httpClient.GetAsync(
                    $"https://api.{_url}/1.0/users/idio_visitor_id:{ivCookie}/content?include_topics&callback=idio.r0&key={currentBlock.DeliveryAPIKey}&session[]={HttpUtility.UrlEncode(request.Url.Host)}%2F&session[]={HttpUtility.UrlEncode(request.Url.Host)}%2F&rpp={currentBlock.NumberOfRecommendations}").Result;

                if (result.IsSuccessStatusCode)
                {
                    string resultStr;
                    using (var sr = new StreamReader(result.Content.ReadAsStreamAsync().Result, Encoding.GetEncoding("iso-8859-1")))
                    {
                        resultStr = sr.ReadToEnd();
                    }

                    resultStr = resultStr.Replace("idio.r0(", string.Empty)
                    .Replace(", 200)", string.Empty);

                    var data = JsonConvert.DeserializeObject<IdioResponse>(resultStr);
                    return data;
                }

                return null;
            }
            catch (Exception)
            {
                return null;
            }
        }
    }
}

The Idio response POCO models are the following:

using System.Collections.Generic;

namespace Foundation.Cms.Models.Idio
{
    public class IdioResponse
    {
        public int total_hits { get; set; }
        public List<Content> content { get; set; }
        public string group { get; set; }
        public bool model { get; set; }
        public string recommendation_id { get; set; }
        public string next_page { get; set; }
    }

    public class Idio
    {
        public string page_type_icon { get; set; }
    }

    public class Og
    {
        public string url { get; set; }
        public List<string> image { get; set; }
        public string description { get; set; }
        public string type { get; set; }
        public string locale { get; set; }
        public string title { get; set; }
    }

    public class Article
    {
        public string published_time { get; set; }
        public string content_type { get; set; }
        public string modified_time { get; set; }
    }

    public class Tags
    {
        public Idio idio { get; set; }
        public Og og { get; set; }
        public Article article { get; set; }
    }

    public class Metadata
    {
        public Tags tags { get; set; }
        public string language { get; set; }
    }

    public class Topic
    {
        public string id { get; set; }
        public string title { get; set; }
        public string full_details_url { get; set; }
    }

    public class MainImage
    {
        public int width { get; set; }
        public int height { get; set; }
    }

    public class Source
    {
        public string id { get; set; }
        public string full_details_url { get; set; }
        public string title { get; set; }
        public string display { get; set; }
    }

    public class Content
    {
        public string id { get; set; }
        public string title { get; set; }
        public string @abstract { get; set; }
        public bool featured { get; set; }
        public string approved { get; set; }
        public bool read { get; set; }
        public object published { get; set; }
        public string original_url { get; set; }
        public Metadata metadata { get; set; }
        public List<Topic> topics { get; set; }
        public string link_url { get; set; }
        public string full_details_url { get; set; }
        public object main_image_url { get; set; }
        public MainImage main_image { get; set; }
        public Source source { get; set; }
        public object author { get; set; }
    }
}

Finally, we will implement the view that is going to show the Idio recommendations. This can be any html that you want and you can of course use razor syntax because it is a normal asp.net MVC view. In our case it is a slider with all the recommendations being displayed inside a for each loop.

@model Foundation.Cms.Features.Common.Blocks.IdioRecommendedContent.IdioRecommendedContentBlockViewModel

@if (Model?.Recommendations == null || !Model.Recommendations.Any())
{
    return;
}

<section class="cards-slider cards-slider--recommended container" aria-label="@Model.CurrentBlock.ModuleAriaLabel">
    <div class="cards-slider__inner">
        <div class="cards-slider__content" @Html.EditAttributes(x => x.CurrentBlock.Heading)>
            <h2 class="cards-slider__heading">
                @Html.DisplayFor(x => x.CurrentBlock.Heading)
            </h2>
        </div>
    </div>
    <div class="cards-slider__cards" data-module="content-cards-slider">
        <div class="cards-slider__cards-container swiper-container">
            <div class="swiper-wrapper">
                @foreach (var recommendation in Model.Recommendations)
                {
                    <div class="swiper-slide">
                        <a href="@recommendation.Url" aria-label="Recently Viewed Slide" class="content-card  ">
                            <img src="@recommendation.PageTypeIcon" alt="Document icon" width="20" height="25">
                            <div role="presentation" class="icon-divider"></div>
                            <strong class="content-card__heading">
                                @recommendation.OgTitle
                            </strong>
                            <p class="content-card__copy content-card__copy-slide">
                                @recommendation.OgDescription
                            </p>
                            <span class="content-card__faux-button">
                                <svg viewBox="0 0 13 13"
                                     role="presentation">
                                    <use xlink:href="#arrow-right" />
                                </svg>
                            </span>
                        </a>
                    </div>
                }
            </div>
        </div>
        <div class="slider-nav container ">
            <p class="slider-nav__pager"></p>
            <button class="slider-nav__arrow slider-nav__arrow--prev" aria-label="Previous">
                <svg viewBox="0 0 320 202"
                     width="25"
                     height="16"
                     role="presentation">
                    <use xlink:href="#prev" />
                </svg>
            </button>
            <button class="slider-nav__arrow slider-nav__arrow--next" aria-label="Next">
                <svg viewBox="0 0 320 202"
                     width="25"
                     height="16"
                     role="presentation">
                    <use xlink:href="#next" />
                </svg>
            </button>
        </div>
    </div>
</section>

And that is it, You can now have your own Idio content recommendations block that gets the content from the API from the back end instead of using the default implementation which only allows mustache templates and gets the content from the front end. If you have any question let me know. I hope it will help someone and as always keep learning !!!

Written by:

Jorge Cardenas

Developer with several years of experience who is passionate about technology and how to solve problems through it.

View All Posts

Leave a Reply