Integrating ContentKing in Episerver

Recently, I found out about the ContentKing CMS API that allows the user to trigger priority auditing of a page through an API. Basically telling ContentKing that a page has changed, and requesting re-evaluation of the SEO score.

In this blog I will explain how I integrated the CMS API in an episerver project.

Publish event

Firstly I needed to hook up to the Episerver publish event. This I did with an Initializable Module.

    [InitializableModule]
    [ModuleDependency(typeof(EPiServer.Web.InitializationModule))]
    public class PublishEventInitializationModule : IInitializableModule
    {
        public void Initialize(InitializationEngine context)
        {
            var contentEvents = ServiceLocator.Current.GetInstance<IContentEvents>();
            contentEvents.PublishingContent += contentEvents_PublishingContent;
        }

        public void Uninitialize(InitializationEngine context)
        {
            var contentEvents = ServiceLocator.Current.GetInstance<IContentEvents>();
            contentEvents.PublishingContent -= contentEvents_PublishingContent;
        }

        void contentEvents_PublishingContent(object sender, EPiServer.ContentEventArgs e)
        {
            var contentKingService = ServiceLocator.Current.GetInstance<IContentKingService>();
            contentKingService?.TriggerPageAudit(e.Content);
        }
    }

When a page is published now the ContentKingService is called to trigger page auditing. The service reads the ContentKing API key needed using a settingsservice, and uses the UrlHelper and ISiteDefinitionResolver to retrieve the full url of the page published. Then does a POST to the ContentKing API using an HttpClient.

[ServiceConfiguration(ServiceType = typeof(IContentKingService), Lifecycle = ServiceInstanceScope.HttpContext)]
    public class ContentKingService : IContentKingService
    {
        private readonly UrlHelper _urlHelper;
        private readonly ISiteDefinitionResolver _siteDefResolver;
        private readonly ISettingsService _settingsService;

        private string EndPointUrl = "https://api.contentkingapp.com/";

        public ContentKingService()
        {
            _urlHelper = ServiceLocator.Current.GetInstance<UrlHelper>();
            _siteDefResolver = ServiceLocator.Current.GetInstance<ISiteDefinitionResolver>();
            _settingsService = ServiceLocator.Current.GetInstance<ISettingsService>();
        }

        public void TriggerPageAudit(IContent content)
        {
            var settings = _settingsService.GetSettings();

            try
            {
                if (settings != null &&
                    settings.TriggerAnalyzeOnPublish &&
                    !string.IsNullOrEmpty(settings.CmsApiKey))
                {
                    var fullPageUrl = ResolveUrl(content);

                    var url = EndPointUrl + "v1/check_url";
                    var requestHeaders = new NameValueCollection { { "Authorization", $"token {settings.CmsApiKey}" } };
                    var json = JsonConvert.SerializeObject(new
                    {
                        url = fullPageUrl
                    });

                    var result = PostObject<object>(url, json, requestHeaders);
                }
            }
            catch
            {
                //ToDo Do something with exceptions..
            }
        }

        public string ResolveUrl(IContent content)
        {
            var contentUrl = _urlHelper.ContentUrl(content.ContentLink);
            if (contentUrl.Contains("//"))
                return contentUrl;

            return new Uri(_siteDefResolver.GetByContent(content.ContentLink, true, true).SiteUrl, contentUrl).AbsoluteUri;
        }

        private static T PostObject<T>(string url, string jsonContent, NameValueCollection requestHeaders = null)
        {
            var httpContent = new StringContent(jsonContent, Encoding.UTF8, "application/json");
            using (var client = new HttpClient())
            {
                if (requestHeaders != null && requestHeaders.Count > 0)
                {
                    foreach (string key in requestHeaders.Keys)
                    {
                        client.DefaultRequestHeaders.Add(key, requestHeaders[key]);
                    }
                }

                client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

                var httpResponse = client.PostAsync(url, httpContent).Result;

                if (!httpResponse.IsSuccessStatusCode)
                    throw new Exception($"{httpResponse.StatusCode} | {httpResponse.ReasonPhrase}");

                return httpResponse.Content.ReadAsAsync<T>().Result;
            }
        }
    }

CMS Admin Page

To make the integration more flexible I wanted to create a page in the cms-admin section of Episerver to manage the ContentKing API key, and enable or disable the triggering. To create a page in cms-admin section I created a controller for the page and added the EPiServer.PlugIn.GuiPlugIn attribute.

    [EPiServer.PlugIn.GuiPlugIn(
        Area = EPiServer.PlugIn.PlugInArea.AdminMenu,
        Url = "/ContentKingAdmin/Index",
        DisplayName = "ContentKing Admin")]
    public class ContentKingAdminController : Controller
    {
        private readonly ISettingsService _settingsService;

        public ContentKingAdminController()
        {
            _settingsService = ServiceLocator.Current.GetInstance<ISettingsService>();
        }

        public ActionResult Index()
        {
            var model = _settingsService.GetSettings();

            return View(model);
        }

        [System.Web.Mvc.HttpPost]
        public ActionResult Save(SettingsModel model)
        {
            _settingsService.SaveSettings(model);

            return RedirectToAction("Index");
        }
    }

Then I created a view to create a ui for the settings. (I stole some styling from another episerver admin page)

@{
        Layout = null;
    }

@model AlloyContentKing.Models.SettingsModel

<!DOCTYPE html>
<html>
<head>
    <title></title>
    <!-- Mimic Internet Explorer 7 -->
    <meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7">
    <link rel="stylesheet" type="text/css" href="/EPiServer/Shell/11.19.1/ClientResources/epi/themes/legacy/ShellCore.css">
    <script type="text/javascript" src="/EPiServer/Shell/11.19.1/ClientResources/ShellCore.js"></script>
    <link rel="stylesheet" type="text/css" href="/EPiServer/Shell/11.19.1/ClientResources/epi/themes/legacy/ShellCoreLightTheme.css">
    <script type="text/javascript" src="/EPiServer/CMS/11.19.1/ClientResources/ReportCenter/ReportCenter.js"></script>
    <link href="../../../App_Themes/Default/Styles/system.css" type="text/css" rel="stylesheet">
    <link href="../../../App_Themes/Default/Styles/ToolButton.css" type="text/css" rel="stylesheet">
</head>
<body>
    <div class="epi-contentContainer epi-padding">
        <div class="epi-contentArea">
            <h1 class="EP-prefix">ContentKing Settings</h1>
            <p class="EP-systemInfo">
                Settings for ContentKing module.
            </p>
        </div>

        <div class="epi-formArea">
            <form id="cmsApiSettings" method="POST" action="\ContentKingAdmin\Save">
                <strong>ContentKing CMS Api</strong>
                <div class="epi-size20">
                    <div>
                        <label>Trigger Page Analize on Publish</label>
                        @Html.EditorFor(x => Model.TriggerAnalyzeOnPublish)
                    </div>
                    <div>
                        <label>ContentKing CMS Api Key</label>
                        @Html.EditorFor(x => Model.CmsApiKey)
                    </div>
                </div>
                <div class="epi-buttonContainer">
                    <span class="epi-cmsButton">
                        <input class="epi-cmsButton-text epi-cmsButton-tools epi-cmsButton-Save"
                               type="submit"
                               name="ctl00$FullRegion$MainRegion$ImportFile"
                               id="FullRegion_MainRegion_ImportFile"
                               value="Save"
                               title="Save">
                    </span>
                </div>
            </form>
        </div>
    </div>
</body>
</html>

The Admin page looks like this:

Dynamic Data Store

To store the settings I use Episerver Dynamic Data Store (DDS). To store data in the DDS you first need to create a model that Implements IDynamicData

    public class SettingsModel : IDynamicData
    {
        public Identity Id { get; set; }

        public bool TriggerAnalyzeOnPublish { get; set; }

        public string CmsApiKey { get; set; }
    }

The SettingsService reads or writes the settings from and to the DDS.

    [ServiceConfiguration(ServiceType = typeof(ISettingsService), Lifecycle = ServiceInstanceScope.Singleton)]
    public class SettingsService : ISettingsService
    {
        private Guid _settingsId = new Guid("05663fcf-9f39-4fc2-af49-bddfb76953e0");

        public SettingsModel GetSettings()
        {
            var store = DynamicDataStoreFactory.Instance.CreateStore(typeof(SettingsModel));
            return store.Items<SettingsModel>().FirstOrDefault(x => x.Id.ExternalId == _settingsId);
        }

        public void SaveSettings(SettingsModel model)
        {
            model.Id = _settingsId;

            var store = DynamicDataStoreFactory.Instance.CreateStore(typeof(SettingsModel));
            store.Save(model);
        }
    }

This concludes my first ever blog. Let me know what you think or if you have any questions!

Mark Prins

Mark Prins

Senior Microsoft .Net / Optimizely Developer @ Arlanet (part of Conclusion)