Skip to main content

Optimizely

Overriding Optimizely’s Content Recommendations Block to Implement Custom Recommendations

Introduction

The Content Recommendations add-on for Optimizely CMS dynamically recommends content from your site tailored to the interests of each user. Installing the NuGet Package will give you access to Optimizely’s Content Recommendations Block which uses Mustache Templating to render the data from the Content Recommendations API on a page.

Although Mustache Templates provide a great way of inserting recommendation data into a webpage, content authors who don’t have prior experience with HTML may have difficulties setting up recommendations. I’ll demonstrate how to override the default Content Recommendations Block so you can leverage the Content Recommendations API within your block’s controller to give developers greater control over recommendations and improve the editing experience.

Overriding the Content Recommendations Block

The first step is to create a new block that we can reference as a property of other Pages/Blocks. This block will allow editors to access content recommendations without the need for a user-defined template. We can simplify this process by inheriting the ContentRecommendationsBlock already provided in the NuGet Package and hiding the template field from the CMS.

public class RecommendationsApiBlock : ContentRecommendationsBlock
{

    [ScaffoldColumn(false)]
    [Ignore]
    public override string RecommendationsTemplate { get => string.Empty; }

}

Now we can add a RecommendationsApiBlock as a nested block to any new or existing content so content authors can choose a Delivery Widget and set the Number of Recommendation Items while we handle how to display the data in-code.

Understanding Content Recommendations API

With our simplified RecommendationsApiBlock we can query the Content Recommendations API from a block or page controller and pass that data into its view. In order to do this, we need to understand how Recommendations API functions.

The default ContentRecommendationsBlock which we inherited from works by passing a visitor id, callback function, and API key as well as session and pagination data to the Idio endpoint. The JavaScript callback handles the response and renders data on the page based on the specified mustache template.

https://api.usea01.idio.episerver.net/1.0/users/idio_visitor_id:243e64eb‑a408-6e29‑ab25‑cc5642a0bdd6/content?include_topics&callback=idio.r0&key=ZE9AEWGWG2AI26PD871C&session[]=https%3A%2F%2Fwww.website.com&rpp=10

Building the API URL

Inside of our block’s controller we are going to create a request to the Content Recommendations API which we use to map the JSON response into a view model to be rendered on page. There are a variety of ways to achieve this, but to keep things brief I will be using RestSharp v106.X to make the request and Newtonsoft.Json 12.X to parse the response.

private static string _ApiUrl = ConfigurationManager.AppSettings["episerver:personalization.apiBase"];

public override ActionResult Index(RecommendationBlock current)
{
    current.Recommendations = GetRecommendations(current.RecommendationsSettings);

    return PartialView("_RecommendationsBlock", current);
}

In order to build out the API call we first need to be able to retrieve the base URL, which I’ve chosen to store in AppSettings. In our Index method we then set the current block’s Recommendations ViewModel to the result of our GetRecommendations helper method that we’ll define later in our controller. A reference of the RecommendationsApiBlock named RecommendationsSettings is passed into the function so the specified delivery widget and number of recommendations can be included in our API request.

Parsing API Response for Content ViewModel

…
public List<RecommendationViewModel> GetRecommendations(RecommendationsApiBlock recommendationsBlock)
{
    //return null if delivery isn't selected
    if (string.IsNullOrEmpty(recommendationsBlock.DeliveryAPIKey))
    {
        return null;
    }

    //pull visitor id from iv token
    System.Web.HttpCookie userCookie = HttpContext.Request.Cookies["iv"];
    var cookieVal = userCookie?.Value;

    //build url
    var url = $"{_ApiUrl}{(cookieVal != null && !string.IsNullOrEmpty(cookieVal) ? "idio_visitor_id:" + cookieVal : "")}" +
        $"/content?include_topics&callback=" +
        $"&key={recommendationsBlock.DeliveryAPIKey}" +
        $"&session[]={HttpUtility.UrlEncode(Request.Url.Host)}" +
        $"&rpp={recommendationsBlock.NumberOfRecommendations}";

    var client = new RestClient(url);
…

Inside of the GetRecommendations method we need to first check to see if a Delivery Widget (stored as DeliveryAPIKey) has been selected in the dropdown. Since the delivery widget is necessary to interfacing with the API, we return null and handle any empty recommendations list within the view.

In order to personalize recommendations we need to specify the user’s visitor token which is stored as a cookie. If you look ahead to the where the URL is defined you’ll notice that when the cookie’s value is not set we can omit the idio_visitor_id parameter which will return impersonalized recommendations.

Now that we have the required parameters, we can build out the request URL and instantiate our RestClient. We’ve left the callback parameter blank since we won’t be using JS callbacks to render recommendations.

…
var request = new RestRequest(Method.GET)
IRestResponse response = client.Execute(request);

//strip callback function out of response
var responseBody = Regex.Replace(response.Content, @"^\(", string.Empty);       // match callback '('
responseBody = Regex.Replace(responseBody, @"(\,\s*\d\d\d\))$", string.Empty);  // match status code ', ###)'

//parse response into viewmodel
JObject jsonBody = JObject.Parse(responseBody);
List<RecommendationViewModel> recommendations = new List<RecommendationViewModel>();
foreach (var content in jsonBody["content"]?.Children())
{
    recommendations.Add(new RecommendationViewModel
    {
        Title = content["title"]?.ToString(),
        Eyebrow = content["metadata"]?["tags"]?["idio"]?["eyebrow"]?.ToString(),
        Url = content["metadata"]?["tags"]?["og"]?["url"]?.ToString(),
        AccentColor = content["metadata"]?["tags"]?["idio"]?["accent_color"]?.ToString(),
        Image = content["metadata"]?["tags"]?["og"]?["image"]?.ToString(),
        Description = content["metadata"]?["tags"]?["og"]?["description"]?.ToString()
    });
}

return recommendations;

With our RestClient created we can now execute a GET request against the Content Recommendations API. Because the API was originally intended to be used with the JS Callback, we need to strip the JavaScript out of the response body before we can parse the JSON and map it into our ContentRecommendations view model.

The response is usually preceded by “callback(“, but since we have left the callback parameter empty we only need to replace the preceding “(“. The response body also ends with the response code, typically seen as “, 200)” which we also need to strip out before parsing JSON. It is important to use regex to strip this last piece out as it can vary depending on the HTTP Status returned from the API.

Now we can parse the remaining string as JSON and map it to our view model to render recommendations.

Results

Our simplified RecommendationsApiBlock can now be nested in any block like a slider or carousel to pull content from the Recommendations API. Now, content authors can simply choose a delivery widget and number of recommendations without needing to write any HTML.

Thoughts on “Overriding Optimizely’s Content Recommendations Block to Implement Custom Recommendations”

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Aidan Britt, Sr. Technical Consultant

Aidan Britt is an Optimizely CMS Certified Developer with over 6 years of experience developing Web Applications using .Net and React. He loves using the latest technology to find creative & innovative solutions that help bring our client’s visions to life.

More from this Author

Follow Us
TwitterLinkedinFacebookYoutubeInstagram