< Return to Feed
Mike Vallotton
04.07.2016

Using Personalization To Reorder Content

At Sagepath, we offer a variety of different services to our clients - development, creative, analytics, etc.

strategy, user experience, creative, development, analytics

We showcase these on our website, but we wanted to take advantage of Sitecore's personalization features to more effectively target our users.  As a first step, we simply wanted to reorder the services so that the ones most relevant to the user are presented first.  A developer should see Development first, an Executive should see Strategy first, and so on.

We have five services we want to showcase:

  1. Strategy
  2. User Experience
  3. Creative
  4. Development
  5. Analytics

We also have four different types of users:

  1. Executives
  2. Marketers
  3. Designers
  4. Developers

Let's create a personalization profile accordingly.  We'll create profile keys for each of the services, and we'll create profile and pattern cards for each of the user types.  Once we've created each, we set the appropriate weighting on the profile and pattern cards - Designers are more interested in Creative and User Experience than Strategy, for example.  You can refer to the Sitecore documentation if you want more information on setting up profile keys, profile cards, or pattern cards.

profile keys, profile cards, pattern cards

Once we've set up our personalization profile, we can associate our profile cards with some content items to provide for predictive personalization.  As the user moves through the site, viewing the content items, Sitecore uses the associated profile cards to build a personalized profile for the user.  Again, more information on associating items with profile cards is available in the Sitecore documentation.

profile card association

Now, we just need to personalize the user experience.  We open the page in the Experience Editor, and click the personalize button for our Services component.  And this is where we find our problem.

component personalization

We don't have an option to reorder our datasource.  We don't have an option to set any rendering parameters.  We have only three options:

  1. Don't show the component at all
  2. Show a different component
  3. Use a different datasource for our component

The first option doesn't help us, obviously.  The second option is probably something we could work with, but we'd have to create a new component for each of the different types of users.  It's a solution, but not a very elegant one.  The third option is also something we could probably make work, but would require keeping some amount of duplicate information in our content tree, which isn't ideal either.

We really just want to sort our existing datasource.  As the user browses the site and a predictive profile is built up, Sitecore automatically associates them with a pattern card, and this pattern card tells us what the weighting for each Service is.  So we have everything we need to order our data properly, but no way to configure it in Sitecore.

services content treeInstead of any of these three choices, we're going to modify our component to use the personalization features that are already built in to sort our data.  We're going to try to do this in a simple way that we can then apply to other components, too.

So to begin, we have a Services component, which has a datasource that points to a specific item in the content tree - a "ServicesFolder".  Under that item are each of the Services.


In our component view, we just iterate over the list of services and render them to the screen.  Something like this:

foreach (Service service in Model.ServiceList)
{
    <p><span>@Editable(service, x => x.ServiceHeadline)</span></p>
    <div>
        <h4>@Editable(service, x => x.ServiceHeadline)</h4>
        @Editable(service, x => x.ServiceCopy)
        @Editable(service, x => x.ServiceTasks)
    </div>
}

Model.ServiceList is a custom property in our model.  We take the datasource on the component, and translate it to a typed list so that we can iterate over it easily.

public IList<Service> ServiceList
{
    get
    {
        List<Service> services = new List<Service>();

        // get the datasource
        SitecoreContext glassContext = new SitecoreContext();
        string datasourceId = RenderingContext.Current.Rendering.DataSource;
        if (Sitecore.Data.ID.IsID(datasourceId))
        {
            Item dataSource = glassContext.GetItem<Item>(new Guid(datasourceId));

            if (dataSource != null)
            {
                // if the datasource is pointing to a servicesfolder
                if (dataSource.TemplateName == "ServicesFolder")
                {
                    // add all the services
                    foreach (Item child in dataSource.Children)
                    {
                        services.Add(glassContext.GetItem<Service>(child.ID.Guid));
                    }
                }
            }                    
        }

        return services;
    }
}

So now we want to modify our basic implementation to reorder these services based on the weighting in the pattern card.  We're going to make a generic helper class so that we can do it in a reusable fashion.  Our class is going to take the type of object we want to sort and the name of the profile we're going to use for the weighting.  The constructor will

  1. Find the profile
  2. Make sure the pattern is up-to-date so that the correct pattern card is identified
  3. Build a dictionary of weighted values.
If you refer back to the Designer pattern card we set up earlier, you'll see that we'll end up with a pattern dictionary that looks like this (assuming a designer is viewing the site):
 Creative
 User Experience
 Development
 Strategy
 Analytics

We'll also create a Sort method, which will take a list of objects and the name of the property on those objects that correspond to the profile key.  It will use that to compare the list to the pattern dictionary and sort it for us.

Here's the full class implementation:

public class PersonalizationHelper<T>
{
    private readonly Profile _profile;
    private readonly Item _patternCard;
    private readonly Dictionary<object, float> _patternDictionary;

    public PersonalizationHelper(string profileName)
    {
        // look to see if we have a profile
        if (Tracker.Current == null || !Tracker.Current.Interaction.Profiles.ContainsProfile(profileName)) return;

        _profile = Tracker.Current.Interaction.Profiles[profileName];
            
        // if there has been any activity, force the pattern to update
        if (_profile.Total > 0)
        {
            _profile.UpdatePattern();
        }

        // look to see if there is an active pattern card
        Guid? patternId = _profile.PatternId;
            
        if (patternId == null) return;

        // get the active pattern card
        _patternCard = Context.Database.GetItem(new ID(patternId.Value));

        // get the data for this pattern card
        XmlField xmlData = _patternCard.Fields["Pattern"];

        // get the keys for this pattern card
        XmlDocument xmlDoc = xmlData.Xml;
        XmlNodeList patternKeys = xmlDoc.GetElementsByTagName("key");

        // build a dictionary of the pattern keys
        _patternDictionary = new Dictionary<object, float>();
        foreach (XmlNode patternKey in patternKeys)
        {
            float keyValue = 0;
            if (patternKey.Attributes == null) continue;

            float.TryParse(patternKey.Attributes["value"].Value, out keyValue);

            _patternDictionary.Add(patternKey.Attributes["name"].Value, keyValue);
        }
    }
        
    public IList<T> Sort(IList<T> list, string profileKey)
    {
        if (_patternDictionary != null && _patternDictionary.Count > 0)
        {
            // reorder the list based on the weights of the pattern keys
            return list.OrderByDescending(s => _patternDictionary.ContainsKey(s.GetType().GetProperty(profileKey).GetValue(s)) ? 
                _patternDictionary[s.GetType().GetProperty(profileKey).GetValue(s)] : 0).ToList();
        }

        return list;
    }
}

Now we can very simply sort our list.  Back in our model, once we've built our services list, we call our PersonalizationHelper class to sort the list for the current user.  "ProfileKey" is the property on our Service class that contains the word Analytics, Strategy, Development, etc. - corresponding to the profile key.  This profile key is just defaulted from the name of the content item.

// do a personalized sort for the Services profile
PersonalizationHelper<Service> ph = new PersonalizationHelper<Service>("Services");
services = (List<Service>)ph.Sort(services, "ProfileKey");

So now we have a helper that we can use to execute a personalized sort on any datasource.  The items in the datasource just need a property that corresponds to the profile key to tie the two together.  This is working quite well on our website.  We show the services on our About page, and as you view the different projects and pages on our site, we customize the way those services are displayed to you.

You can also download the PersonalizationHelper directly if you'd like to use it in your own projects.

services content tree
< Return to Feed