Digiarjessa-blogi

How to integrate Episerver to HubSpot – blog page and landing page integration

cms-integrointi

Sometimes one CMS isn't enough to cover everything that needs to be done. In these cases it is also crucial to get information flowing between systems. In this blog post Digia's senior developer Ville Laukkonen provides a solution for a specific integration implementation between Hubspot and Episerver.

In this blog post I will present a solution to integrate HubSpot Page data to the Episerver CMS. The post is scoped mainly for Episerver developers including several code examples of implementation, but it might also be relevant for Episerver administrators and content producers by offering information about possibilities of HubSpot and Episerver integration.

Table of contents:

Introduction
Landing page integration
Blog Page integration
Epilogue

Introduction

About HubSpot

HubSpot is a web platform offering software for marketing, sales, and customer service, with a CRM at its core. These features are named as Marketing Hub, Sales Hub, Service Hub and so on. Integration described in this document is made to the Marketing Hub feature.

There are existing tools for HubSpot integration such as Episerver Connect for HubSpot add-on and HubSpot-Episerver Connector. These however are more high-level tools and they don’t offer features described in this blog.

Goals of the integration

Our event pages have been published as HubSpot Landing Pages, because they offer an easy way to include registration form and handling of the registrations. HubSpot also provides statistics and workflow features helpful in tracking and contacting registered users. As our main web site is on the Episerver platform, and there has been a need to display event data also on main web site, event pages has been created also in Episerver to redirect users to the event pages hosted in HubSpot.

First goal of the integration project was to fetch event data created in HubSpot to the Episerver and display this data in Episerver without the creation of duplicate event pages in both systems. Second goal of the integration was to supplement Episerver search’s blog page result data. HubSpot Blog pages are indexed using Episerver Find Connectors feature, but it is unable to index some important values needed in search results.

Implementation and code structure

Methods needed for calling HubSpot APIs and handling integration data are written to HubSpotIntegration class implementing IHubSpotIntegration interface. This class is injected to Scheduled Jobs and other classes using HubSpotIntegration methods.

An own scheduled job is implemented for fetching Landing Page and Blog Page data.
Scheduled jobs are separated as they call their own HubSpot APIs, and fetch different data for different purposes. Also the need of scheduling is different. Blog pages can be updated once per day, while the Landing Page update should be much more frequent.

Unless otherwise stated, code examples in this document are from HubSpotIntegration class.

Reference to injected HubSpotIntegration class is named as _hubSpotIntegration.

HubSpot API and Authentication

HubSpot offers wide range of Rest APIs for developers. API’s allow two kinds of authentication, OAuth and API keys. API key can be used during development, but OAuth authentication should be set up for production implementations.

Before starting development, a HubSpot Developer Account and Development portal must be created. After Development portal creation, add an App to the portal using these instructions.
Now you can build OAuth authentication for your application as described here.

In our implementation, information needed for OAuth authentication is saved to Settings page in Episerver. Own property is needed for Cliend ID, Client Secret and Refresh Token.
Using these properties, OAuth authentication can be accomplished from Episerver Scheduled Job tasks.

Example code 1.

Method for calling HubSpot authentication API. clientID, clientSecret and refreshToken values are fetched from Settings Page properties. Asynchronous method returns deserializable json object containing access_token needed for accessing HubSpot REST APIs.

public async Task<string> UpdateHubSpotToken(string clientId, string clientSecret, string refreshToken)
{
   _ = clientId ?? throw new ArgumentNullException(nameof(clientId));
   _ = clientSecret ?? throw new ArgumentNullException(nameof(clientSecret));
   _ = refreshToken ?? throw new ArgumentNullException(nameof(refreshToken));

   var url = "https://api.hubapi.com";
   var path = "/oauth/v1/token";
   string responseValue = string.Empty;
   using (var client = new HttpClient())
   {
      client.BaseAddress = new Uri(url);
     var content = new FormUrlEncodedContent(new[]
     {
        new KeyValuePair<string, string>("grant_type", "refresh_token"),
         new KeyValuePair<string, string>("client_id", clientId),
         new KeyValuePair<string, string>("client_secret", clientSecret),
         new KeyValuePair<string, string>("refresh_token", refreshToken)
     });

     HttpResponseMessage response = await client.PostAsync(path, content);
     if (response.IsSuccessStatusCode)
     {
        responseValue = await response.Content.ReadAsStringAsync();
     } else {
         _log.Error("Reading " + url + path + " failed");
         _log.Error("response.StatusCode: " + response.StatusCode);
         throw new HttpResponseException(response.StatusCode);
     }
   }
   return responseValue;
}

Usage in Blog Page Scheduled Job

var result = _hubSpotIntegration.UpdateHubSpotToken(_settingsPage.HubSpotClientId, _settingsPage.HubSpotClientSecret, _settingsPage.HubSpotRefreshToken);
var resultString = result.Result != null ? result.Result : null;
string token = null;
if (resultString != null)
{
   dynamic resultJsonObject = JsonConvert.DeserializeObject(resultString);
   token = resultJsonObject.access_token;
   …

Additionally a static Javascript file was used to test Authentication and APIs returning Blog Posts and Landing Page data.

Landing Page integration

Landing pages in our HubSpot are used mostly for Events and Webinars. Goal of the Landing Page integration was to fetch needed data for displaying Event feeds in Episerver from HubSpot Landing Page data. HubSpot data is read using Episerver’s Scheduled Job functionality and saved as DDS data to Episerver. Dedicated Episerver Block was created for listing Event data in DDS.

HubSpot API

Address: https://api.hubapi.com/content/api/v2/pages

API description: https://developers.hubspot.com/docs/methods/pages/get_pages

Parameters used

Parameter

Value

Description

subcategory

landing_page

Read only Landing Page data.

limit

1000

In API specification, there is no note about maximum amount of returned items. 1000 returned items was enough for our needs, and unlike with Blog Pages no paged API calls was needed.

is_draft

false

Return only published Landing Pages

 

Landing Pages are also filtered by HubSpot folder ID saved in Episerver’s Settings Page data. “folder_id” data is returned from REST API, but it cannot be used as a filter.

Reading data

A custom class was defined for setting parameters of the API call.

Example code 2. HubSpotApiCall class definition

public class HubSpotApiCall {
   public string Token; // HubSpot access_token
   public string ApiUrl; // HubSpot API url
   public string UrlParams; // API call url parameters
}

This allows us to use one method (ReadHubSpotPages(HubSpotApiCall callParams)) for different HubSpot API calls.

Parsing data

Data returned from REST API is deserialized to System.Dynamic.ExpandoObject using Newtonsoft.Json.Converters.ExpandoObjectConverter, and saved in List<dynamic> object.

Example code 3. Parsing data returned from HubSpot REST API

var converter = new ExpandoObjectConverter();
ExpandoObject jsonObjects = new ExpandoObject();
jsonObjects = JsonConvert.DeserializeObject<ExpandoObject>(jsonString, converter);

List<dynamic> resultObjects = new List<dynamic>();

foreach (KeyValuePair<string, object> kvp in jsonObjects)
{
   // Set resultObjects value from jsonObjects KeyValuePair<string, object> object
   // containing key value “objects”
   if(kvp.Key.Equals("objects")) {
     resultObjects = (List<dynamic>)kvp.Value;
   }
}

Reading OOTB Landing Page Property values

Once the pages have been parsed to List<dynamic> element, OOTB page properties located under item’s root node can be easily read.

Example code 4. Reading OOTB Landing Page data from List<dynamic> object

foreach (var page in resultObjects)
{
   pageName = page.name;
   pageUpdated = (page.updated != null) ? page.updated.ToString() : string.Empty;
   …

Reading custom Widget property values

In HubSpot each Widget module added to page template has “id” property with value “module_xxxxxxxxxxxxxxxx” (for example "module_1460634696146958"). This should be unique and permanent value, and suitable for detecting the Widgets of template when parsing page data fetched from HubSpot API. Since we were able to use custom template with Widgets using custom labels, the “label” property is used as unique identifier of the Widget. This property was used as its value is editable in HubSpot template, and “label” values can be set more descriptive than values randomly generated to “id” property.

Screenshot 1. Json data returned from https://api.hubapi.com/content/api/v2/pages API.

epi-screenshot1

In the code example below, page.widgets object and label of the widget data to fetch is passed to GetWidgetsContent method. All items in containers parameter are looped through and the one with the same label value as passed in label parameter defines which data will be returned.

Example code 5. Reading data from Widget

public string GetHubSpotLandingPageWidgetsContent(dynamic containers, string label)
{
   _ = containers ?? throw new ArgumentNullException(nameof(containers));
   _ = label ?? throw new ArgumentNullException(nameof(label));   string widgetContent = string.Empty;

     // Loop through all properties in containers object
   foreach (dynamic property in containers)
   {
     // Get label value searching for “label” key in IDictionary<string, object>
     // property.Value object
     var labelTitle = ((IDictionary<string, object>)property.Value).ContainsKey("label") ?
     property.Value.label :
     null;
     if (labelTitle != null && labelTitle.ToString().Equals(label))
     {
        switch (label)
         {
            case "Start-date":
               // In date typed widget fields, value is saved to body.date_field property
              widgetContent =
               ((IDictionary<string, object>)property.Value.body).ContainsKey("date_field")
               ? property.Value.body.date_field.ToString() :
               string.Empty;
               break;
              // In string typed widget fields, value is saved to body.value property
           case "Header":
              widgetContent =
               ((IDictionary<string, object>)property.Value.body).ContainsKey("value")
               ? property.Value.body.value.ToString() :
               string.Empty;
               break;

            // In Rich Text typed widget fields, value is saved to body.value.html
             // property
           case "Rich Text":
              widgetContent =
               (!string.IsNullOrEmpty(property.Value.body.html)) ?
               property.Value.body.html :
               widgetContent;
               break;
     }
   }
   return widgetContent;
}

Usage

var startDate = GetHubSpotLandingPageWidgetsContent(page.widgets, "Start-date");

DateTime values in HubSpot

In HubSpot DateTime values are saved in millisecond format. To display DateTimes in readable format, they must be reformatted. Below is a method for settings millisecond date format to DateTime? format. addHours parameter is added for situations, when HubSpot returns DateTimes incorrectly for some reason.

Example code 6. Converting millisecond DateTime format to DateTime?

public DateTime? ConvertMillisecondsToDateTime(string datetime, int addHours = 0)
{
   _ = datetime ?? throw new ArgumentNullException(nameof(datetime));

   if (long.TryParse(datetime, out long milliSeconds))
   {
      var convertedDateTime = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)
        .AddMilliseconds(milliSeconds)
        .ToLocalTime().AddHours(addHours);
     return convertedDateTime;
   } else {
      _log.Error("DateTime? ConvertMillisecondsToDateTime(\"" + datetime + "\", " +
     addHours + ")");
     _log.Error("DateTime parsing failed");
     throw new FormatException("DateTime parsing failed");
   }
}

Saving HubSpot Landing Page data to Episerver DDS

As explained in introduction, we didn’t want to save whole new pages for Events in Episerver. Landing Page/Event data was supposed to use only in custom Event list block, and only the data needed for this block was supposed to be saved into Episerver.

Therefore, we decided to save data in Episerver Dynamic Data Store. It might not be the most efficient solution, but since it is very easy to implement and use, and reading of data will be done using Scheduled Jobs, it should be a good solution for the implementation.

DDS Class for saving data

EpiserverDataStore attributes should be added to DDS data class since they help when making changes to class properties or publishing class to a new environment. When  AutomaticallyRemapStore value is set to true, changes made to class are automatically applied after rebuilding. If attribute is not set or value is false, class must be removed and recreated to apply changes.

Identity Id property must be added to the class to ensure that Episerver handles saved DDS data correctly. HubSpotId however is the property that is used to identify HubSpotLandingPage entities in code.

Example code 7. Definition of class for HubSpot Landing Page data

[EPiServerDataStore(AutomaticallyCreateStore = true, AutomaticallyRemapStore = true)]
public class HubSpotLandingPage : IDynamicData
{
   public Identity Id { get; set; }
   public string HubSpotId { get; set; }
   public string Name { get; set; }
   public string PublishDate { get; set; }
   public string Updated { get; set; }
   public string StartDate { get; set; }
   public string EndDate { get; set; }
   public string Header { get; set; }
   public string BackgroundImage { get; set; }
   public string Location { get; set; }
   public string Url { get; set; }
}

Saving DDS data

Save and update operations of HubSpotLandingPage data can be done with the following method.

Example code 8. Method for saving and updating HuSpotLandingPage data

public static void SaveAndUpdateHubSpotLandingPageStore(HubSpotLandingPage page)
{
   _ = page ?? throw new ArgumentNullException(nameof(page));
   var store = DynamicDataStoreFactory.Instance.CreateStore(typeof(HubSpotLandingPage));
   store.Save(page);
}

Saving and updating operations are done with following methods in Scheduled Job updating Episerver HubSpotLandingPage data.

List of parsed data fetched from HubSpot API is given as parameter to CreateAndUpdateHubSpotLandingPageData method.

List of pages to be created (createData) is set by comparing existing pages to parsed HubSpot data. Pages not existing in DDS will be created.

List of pages to be updated (updateData) is set by comparing existing pages to parsed HubSpot data. Pages existing in DDS will be added to list.

CreateHubSpotLandingPageData and UpdateHubSpotLandingPageData methods return strings containing amount of created and updated HubSpotLandingPage entities. This string will be used as return string of Episerver Scheduled Job’s Execute() method return value.

Example code 9. HubSpotLanding Page data save and update

public string CreateAndUpdateHubSpotLandingPageData(List<HubSpotLandingPage> hubSpotPageData)
{
   _ = hubSpotPageData ?? throw new ArgumentNullException(nameof(hubSpotPageData));

   var dataInDDS = GetHubSpotLandingPages();
   var createData =
   hubSpotPageData.Where(x => !dataInDDS.Any(y => y.HubSpotId.Equals(x.HubSpotId)));
   var updateData =
   hubSpotPageData.Where(x => dataInDDS.Any(y => y.HubSpotId.Equals(x.HubSpotId)));
   var dataCreated = CreateHubSpotLandingPageData(createData);
   var dataUpdated = UpdateHubSpotLandingPageData(updateData);
   return dataCreated + "<br />" + dataUpdated + "<br />";
}

public string CreateHubSpotLandingPageData(IEnumerable<HubSpotLandingPage> newPages)
{
   _ = newPages ?? throw new ArgumentNullException(nameof(newPages));

   var pagesCreatedCounter = 0;
   foreach (var page in newPages)
   {
      SaveAndUpdateHubSpotLandingPageStore(page);
     pagesCreatedCounter++;
   }
   return pagesCreatedCounter + " HubSpotLandingPageData items created.\t";
}

public string UpdateHubSpotLandingPageData(IEnumerable<HubSpotLandingPage> updatePages)
{
   _ = updatePages ?? throw new ArgumentNullException(nameof(updatePages));  

   var pagesUpdatedCounter = 0;
   var ddsPages = GetHubSpotLandingPages(); // Read all HubSpotLandingPage data
   foreach (var page in updatePages)
   {
      var ddsPageSearch = ddsPages.Where(x => x.HubSpotId.Equals(page.HubSpotId));
     var ddsPage = (ddsPageSearch.Count() > 0) ? ddsPageSearch.First() : null;
     if(ddsPage != null)
     {
        var savedUpdateTime = ConvertMillisecondsToDateTime(ddsPage.Updated);
         var pageUpdateTime = ConvertMillisecondsToDateTime(page.Updated);

         if(savedUpdateTime != null && pageUpdateTime != null
           && (pageUpdateTime > savedUpdateTime)) {
              ddsPage.Name = page.Name;
             ddsPage.PublishDate = page.PublishDate;
             ddsPage.Updated = page.Updated;
             ddsPage.Header = page.Header;
             ddsPage.StartDate = page.StartDate;
             ddsPage.EndDate = page.EndDate;
             ddsPage.BackgroundImage = page.BackgroundImage;

             SaveAndUpdateHubSpotLandingPageStore(ddsPage);
             pagesUpdatedCounter++;
         }
       }
   }
   return pagesUpdatedCounter + " HubSpotLandingPageData items updated.\t";
}

// Method for reading all HubSpotLandingPage entities from DDS
public IEnumerable<HubSpotLandingPage> GetHubSpotLandingPages()
{
   var store =  
   DynamicDataStoreFactory.Instance.CreateStore(typeof(HubSpotLandingPage));
   return store.Items<HubSpotLandingPage>();
}

Removing DDS Data

Single HubSpotLandingPage entity can be removed using following method.

Example code 10. Removing single HubSpotLandingPage from DDS

public void DeleteHubSpotLandingPage(HubSpotLandingPage page)
{
   _ = page ?? throw new ArgumentNullException(nameof(page));
   var store =
   DynamicDataStoreFactory.Instance.CreateStore(typeof(HubSpotLandingPage));
   store.Delete(page.Id);
}

Whole HubSpotLandingPage DDS store can be removed using the following command.

Example code 11. Removing HubSpotLandingPage store from DDS

DynamicDataStoreFactory.Instance.DeleteStore(typeof(HubSpotLandingPage),
true);

DDS data logging

To view existing HubSpotLandingPage entities in Episerver DDS, a custom Admin tool was created. Detailed instructions to create a custom admin tool can be found for example here.

Admin tool includes list view of all HubSpotLandingData entities in DDS and functionalities to remove single entity and remove whole HubSpotLandingPage store from DDS.

Detailed description of Admin tool implementation is not presented here, but it should be easy to implement using instructions linked for creating Admin tool and using code examples presented in Landing Page integration chapter.

Episerver Block for displaying HubSpotLandingPage entities

This is the block element that is visible for Episerver site users. The block fetches all HubSpotLandingPage entities using GetHubSpotLandingPages() method presented earlier and filters valid entities.

Detailed description of Block implementation is not presented here, but it should be easy to implement using code examples presented in Landing Page integration chapter.

Blog Page integration

Goal of the Blog Page integration was to supplement Episerver Find search results. Episerver Find is using Find Connectors to index our blog site blog.digia.com. However, data like publish date and article tags/topics cannot be indexed, as they are not visible content of the page.

HubSpot offers an xml feed for Blog Pages, but it doesn’t include these properties and the amount of items in xml feed is limited. Therefore, only way to read all HubSpot Blog Pages with publish date and tag/topic data to Episerver, was the use of HubSpot API.

HubSpot data is read using Episerver’s Scheduled Job functionality and saved as DDS data to Episerver.

HubSpot API

Address: https://api.hubapi.com/content/api/v2/blog-posts

API description: https://developers.hubspot.com/docs/methods/blogv2/get_blog_posts

Parameters used

Parameter

Value

Description

limit

300

Amount of returned blog page items. Max value 300.

order_by

-publish_date

Returns items in descending order

offset

0-n

The offset to start returning items from. API returns max. 300 items, total amount of items is calculated using separate API call before reading blog post data and API is called (total / 300) times.

Reading data

Unlike when reading Landing Pages, Blog Page data (HubSpotBlogPage) is removed from DDS every time the Scheduled Job is executed.

As the maximum amount of returned Blog Page items is 300, REST API must be called x times to fetch all blog pages. (Total amount of Blog Pages in our HubSpot is over 600 items)
This is done by executing an extra API call to fetch the amount of Blog Pages.

Like with Landing Pages, HubSpotApiCall class is used for setting API call parameters. HubSpot API calls are made using ReadHubSpotPages(HubSpotApiCall callParams) method.

Example code 12. Reading total amount of Blog Pages in HubSpot

public async Task<string> ReadHubSpotPages(HubSpotApiCall callParams)
{
   _ = callParams ?? throw new ArgumentNullException(nameof(callParams));
   _ = callParams.Token ?? throw new ArgumentNullException(nameof(callParams.Token));
   _ = callParams.ApiUrl ?? throw new ArgumentNullException(nameof(callParams.ApiUrl));

   var token = callParams.Token;
   string responseValue = string.Empty;
   var apiUrl = callParams.ApiUrl;
   var urlParams = callParams.UrlParams;
   var url = apiUrl + urlParams;
   using (var client = new HttpClient())
   {
      client.BaseAddress = new Uri(url);
      client.DefaultRequestHeaders.Authorization
       = new AuthenticationHeaderValue("Bearer", token);
       HttpResponseMessage response = await client.GetAsync(url);
       if (response.IsSuccessStatusCode)
       {
          responseValue += await response.Content.ReadAsStringAsync();
       }
       else {
        _log.Error("response.StatusCode: " + response.StatusCode);
         throw new HttpResponseException(response.StatusCode);
       }
   }
   return responseValue;
}

Usage

var callParams = new HubSpotApiCall
{
   Token = token,
   ApiUrl = "https://api.hubapi.com/content/api/v2/blog-posts",
   UrlParams = "?limit=1"
};
var total = _hubSpotIntegration.ReadHubSpotPages(callParams);
var converter = new ExpandoObjectConverter();
ExpandoObject jsonObject = new ExpandoObject();
jsonObject = JsonConvert.DeserializeObject<ExpandoObject>(total.Result, converter);
// Total number of Blog Pages (“total”) is the fourth node of root element
string totalString = (((IDictionary<string, object>)jsonObject).Values != null && ((IDictionary<string, object>)jsonObject).Values.ToArray()[3] != null) ? ((IDictionary<string, object>)jsonObject).Values.ToArray()[3].ToString() : "0";
double totalDouble = 0;
int pages = 0;
int pageSize = 300;
bool errors = false;

// Get amount of pages as int
if (double.TryParse(totalString, out totalDouble))
{
   double numbers = totalDouble / pageSize;
   pages = (int)Math.Floor(numbers);
}

Example code 13. Read blog pages using offset parameter, scheduled Job code

for (int i = 0; i <= pages; i++)
{
   if(!errors) {
     // Count offset, pageSize = 300
     int offset = i * pageSize;
     var apiCall = new HubSpotApiCall {
         Token = token,
         ApiUrl = "https://api.hubapi.com/content/api/v2/blog-posts",
         UrlParams = "?limit=300&order_by=-publish_date&offset=" + offset
     };
     var jsonData = _hubSpotIntegration.ReadHubSpotPages(apiCall);
     var hubSpotPageData = (jsonData.Result != null) ?
     _hubSpotIntegration.ParseHubSpotBlogPageJson(jsonData.Result) : null;
     // Set scheduledJobMessage shown after Job execution
     if (hubSpotPageData != null) {
         // Call method that updates DDS data and returns amount of updated HubSpotBlogData
           entities for current page
         scheduledJobMessage += "Page " + i + ": " +
         _hubSpotIntegration.CreateHubSpotBlogPageStore(hubSpotPageData);
     } else {
         scheduledJobMessage = "No data returned from HubSpot.";
         errors = true;
     }
   }
}
// Add total number of Blog Pages to scheduledJobMessage
if (!errors)
   scheduledJobMessage += "Total: " + totalString;

Parsing data

Data parsing is done as when reading Landing Page data from HubSpot API.

Saving HubSpot Blog Page data to Episerver DDS

As explained in introduction, we didn’t want to create whole new pages for Blog Pages in Episerver. On Blog pages the need of this integration was to supplement Episerver Find search result data. Blog pages are indexed using Episerver Find External Connector, but unfortanely it is not able to index a couple of needed properties.

We decided to save data to Episerver Dynamic Data Store. As noted in Landing Pages section, it might not be the most efficient solution, but it is a working solution here.

DDS Class for saving data

EpiserverDataStore and AutomaticallyRemapStore attributes and Identity Id property should be added to class as explained in Landing Page integration chapter.

Example code 14. Class for HubSpot Blog Page data

[EPiServerDataStore(AutomaticallyCreateStore = true, AutomaticallyRemapStore = true)]
public class HubSpotBlogPage : IDynamicData
{
   public Identity Id { get; set; }
   public string HubSpotId { get; set; }
   public string Url { get; set; }
   public string PublishDate { get; set; }
   public string[] HubSpotTagIds { get; set; }
}

Saving DDS data

Save and update operations of HubSpotBlogPage data can be done using the following method.

Example code 15. Method for saving and updating HubSpotBlogPage data

public void SaveAndUpdateHubSpotBlogPageStore(HubSpotBlogPage page)
{
   _ = page ?? throw new ArgumentNullException(nameof(page));
   var store = DynamicDataStoreFactory.Instance.CreateStore(typeof(HubSpotBlogPage));
   store.Save(page);
}

Example code 16. Add HubSpotBlogPage data given as parameter. Returns string containing amount of added HubSpotBlogPage data. See usage in Example code 13.

public string CreateHubSpotBlogPageStore(List<HubSpotBlogPage> blogPages)
{
   _ = blogPages ?? throw new ArgumentNullException(nameof(blogPages));
   var updCounter = 0;
   foreach (var page in blogPages)
   {
      var convertedDate = ConvertMillisecondsToDateTime(page.PublishDate);
     if (convertedDate != null)
     {
        page.PublishDate = convertedDate.Value.Year + "-" + convertedDate.Value.Month +
         "-" + convertedDate.Value.Day;
         SaveAndUpdateHubSpotBlogPageStore(page);
         updCounter++;
     }
   }
   return "Amount of added HubSpotBlogPage DDS data: " + updCounter + "<br />";
}

Removing DDS Data

Single HubSpotBlogPage entity can be removed using following method.

Example code 17. Removing single HubSpotBlogPage object from DDS

public void DeleteHubSpotBlogPage(HubSpotBlogPage page)
{
   _ = page ?? throw new ArgumentNullException(nameof(page));

   var store = DynamicDataStoreFactory.Instance.CreateStore(typeof(HubSpotBlogPage));
   store.Delete(page.Id);
}

Whole HubSpotBlogPage DDS store can be removed using the following command.

Example code 18. Removing HubSpotBlogPage store from DDS

DynamicDataStoreFactory.Instance.DeleteStore(typeof(HubSpotBlogPage), true);

DDS data logging

To view existing HubSpotBlogPage entities in Episerver DDS, a custom Admin tool was created. Admin tool includes list view of all HubSpotBlogPage entities in DDS and functionalities to remove single entity and remove whole HubSpotBlogPage store from DDS.

Detailed description of Admin tool implementation is not presented here, but it should be easy to implement using instructions linked for creating Admin tool in Landing Page integration chapter and using code examples presented in Blog Page integration chapter.

Using HubSpotBlogPage data in Episerver Find Search results

HubSpotBlogPage DDS data is used to add Publish date and Categories information to Blog Page WebContent search results. Code example below shows, how this data is fetched in API Controller, and returned as a list of custom PageDataView entities. Json data returned by API Controller is consumed by a React view.

Example code 19. From search result controller


IEnumerable<HubSpotBlogPage> ddsHubSpotPages = null;
// Data returned from Find Search result in list of custom FindItem entities.
foreach (var item in searchResults)
{
   var pageDataViewItem = new PageDataView();
   var pageRef = (item.Id != 0) ? new PageReference(item.Id) : null;
   var pageData = (pageRef != null) ? _contentLoader.Get<SitePageData>(pageRef) : null;
   // If current page has no SitePageData, it is HubSpot Blog Page (WebContent)
var blogPageData = (pageData == null) ? new HubSpotBlogPage() : null;
   if (blogPageData != null)
   {
     // Get all HubSpotBlogPage entities from DDS if not fetched in earlier results
     if(ddsHubSpotPages == null)
     {
        ddsHubSpotPages = _hubSpotIntegration.GetHubSpotBlogPages();
     }
     // Get current HubSpotBlogPage entity
     blogPageData = blogPageData =
     _hubSpotIntegration.GetHubSpotBlogPageData(ddsHubSpotPages, item.Url);
   }
   pageDataViewItem.StartPublish = (item.StartPublish != null) ?  
   (DateTime?)item.StartPublish : GetWebContentPublishDate(blogPageData);
   pageDataViewItem.Categories = (pageData != null) ?
   GetCategoryExplanation(pageData, languageSelector, forcedCulture, categorySearchRootId)
: GetWebContentCategories(blogPageData);

Example code 20. Method to return all HubSpotBlogPage entities from DDS

public IEnumerable<HubSpotBlogPage> GetHubSpotBlogPages()
{
   var store = DynamicDataStoreFactory.Instance.CreateStore(typeof(HubSpotBlogPage));
   return store.Items<HubSpotBlogPage>();
}

Example code 21. Method to fetch HubSpotBlogPage entity by url

public HubSpotBlogPage GetHubSpotBlogPageData(IEnumerable<HubSpotBlogPage> pages, string url)
{
   _ = pages ?? throw new ArgumentNullException(nameof(pages));
   _ = url ?? throw new ArgumentNullException(nameof(url));

var result = pages.FirstOrDefault(x => x.Url.Equals(url));
   return result;
}

Example code 22. Return PublishDate from HubSpotBlogPage entity. Code from search result controller.

public DateTime? GetWebContentPublishDate(HubSpotBlogPage blogPage)
{
   string dateString = (blogPage != null && blogPage.PublishDate != null) ?
   blogPage.PublishDate : null;
   DateTime? dateValueNullable = null;
   if (dateString != null)
   {
      CultureInfo ci = CultureInfo.CurrentCulture;
     bool success = false;
     DateTime dateValue;
     success = DateTime.TryParse(dateString, ci, DateTimeStyles.None, out dateValue);
     if (success)
     {
        dateValueNullable = dateValue;
      }
}
   return dateValueNullable;
}

Integrating HubSpot Tags/Topics to Episerver Categories

An important goal in HubSpot Blog Page integration was to display HubSpot Tag/Topic data in Blog Page search results.

HubSpot API returns Tag/Topic data in array on numeric values (example of single value: 2926024044). These Tags/Topics are linked to Episerver by creating a corresponding Episerver Category. As Episerver Category names are not allowed to start with a number, prefix “hsbt-“ is added to Episerver Category Name.

Prefixed name is added to HubSpotBlogPage entity in Scheduled Job reading HubSpot Blog Page data.

Example code 23. Read HubSpot Blog Page Tag/Topic data from dynamic object and return array of string containing each tag_id with “hsbt-“ prefix

public string[] GetHubSpotBlogPagePropertyValueArray(dynamic container, string propertyName)
{
   _ = container ?? throw new ArgumentNullException(nameof(container));
   _ = propertyName ?? throw new ArgumentNullException(nameof(propertyName));


   List<string> tagIds = new List<string>();
   string tagPrefix = "hsbt-"; // Episerver Category Name cannot start with a number.
   if (((IDictionary<string, object>)container).ContainsKey(propertyName))
   {
     var tagList = container.tag_ids;
      foreach (long tag in tagList)
      {
        tagIds.Add(tagPrefix + tag.ToString());
      }
   }
   return tagIds.ToArray<string>();
}

Example code 24. Return HubSpotBlogPage category names. Code from search result controller.

private string GetWebContentCategories(HubSpotBlogPage blogPage)
{
   StringBuilder catStrings = new StringBuilder();
   var tagArray = (blogPage != null && blogPage.HubSpotTagIds != null) ?
   blogPage.HubSpotTagIds : null;
   if(tagArray != null) {
      foreach(var tag in tagArray) {
        var catDesc = CategorizableExtensions.GetCategoryDescription(tag);
         if(!string.IsNullOrEmpty(catDesc)) {
            catStrings.Append(", " + catDesc);
         }
     }
}
   return catStrings.ToString();
}

Example code 25. Helper method to get Category display name (Description) by Category Name. Code from Episerver Category helper (CategorizableExtensions) method.

public static string GetCategoryDescription(string catName)
{
   var locator = ServiceLocator.Current.GetInstance<CategoryRepository>();
   var rootCategory = locator.GetRoot();
   var searchCategory = rootCategory.FindChild(catName);
   var catDescription = (searchCategory != null) ? searchCategory.Description : null;
   return catDescription;
}

HubSpot Tag/Topic list

As Tag/Topic ids needed to create corresponding Episerver Category are not visible in HubSpot UI, an admin tool to see a list of HubSpot’s Tag/Topic list was created.

This view reads https://api.hubapi.com/blogs/v3/topics API, and lists data needed to create Episerver Categories. View also shows if corresponding Episerver Category exist.

Detailed description of Admin tool implementation is not presented here, but it should be easy to implement using instructions linked for creating Admin tool in Landing Page integration chapter and using code examples presented in Blog Page integration chapter.

Epilogue

As mentioned earlier, HubSpot offers a wide range of API’s and this is a very small example of its integration possibilities. There is also Client Libraries available for various programming languages. These libraries are presented here.

As the methods for reading and handling HubSpot page data are implemented in a class implementing interface, it is easy to change the store containing HubSpot page data. Interface defines the methods needed, only class implementing the interface must be updated to work with new data store.

Instead of using DDS, data fetched from HubSpot could be saved as Episerver IContent objects. These objects could be indexed by Episerver Find, and then read from Episerver Find index.
Third solution would be the use of custom database with custom tables storing data fetched from HubSpot.

These two options would probably be more efficient way to implement the read/write of HubSpot data in Episerver. Saving data to Episerver is done using two Scheduled Jobs, which run very fast with current amount of HubSpot data when using DDS. Also reading of Landing Page data in event block and reading of additional data in search result works fast with current amount of data.

As the alternative implementations would need a lot of extra coding and configuration, specially the use of a custom database, DDS implementation was found suitable for our needs.

Tilaa Digian blogikirjoitukset suoraan sähköpostiisi


verkkoliiketoiminta   digitaaliset palvelut   ohjelmistokehitys   in english


Tilaa blogikirjoitukset sähköpostiisi