Thoughts On Moving To A Headless CMS

I’ll get right to it. Should I be making the move to a headless content management platform? I am no stranger to the Headless CMS sector after the many years of being involved in using different providers for client-based projects, so I am well-versed on the technology to make a judgement. But any form of judgment gets thrown out the window when making a consideration from a personal perspective.

Making the move to a Headless CMS is something I’ve been thinking for quite some time now as it would streamline my website development considerably. I can see my web application build footprint being smaller compared to how it is at the moment by running on Kentico 12.

This website has been running on Kentico CMS for around 6 years ever since I was first introduced to the Kentico platform, which gave me a very good reason to move from BlogEngine. I wanted my web presence to be more than just a blog that would give me the flexibility to be something more. I do not like the idea of being restricted to just one feature-base.

As great as it is running my website on Kentico CMS, it’s too big of an application for my needs. Afterall, I am just using the content-management functionality and none of the other great features the platform offers, so it’s good time to start thinking of downsizing and reduce running costs. Headless seems the most suitable option right?

I won’t be going into detail on what headless is. The internet contains information on the subject matter detailed in a more digestable manner over the years suitable for varied levels of technical expertise. “Headless CMS” is the industry buzz-word that clients are aware of. You can also take a read of a Medium post I wrote last year about one type of headless platform - Kentico Cloud (now named Kontent) and the market.

So why haven’t I made the move to Headless CMS? I think it comes down to following factors:

  • Pricing
  • Infrastructure and stability
  • Platform changes
  • Trust

Pricing

First and foremost, it’s the price. I am aware that all Headless CMS providers have a free or starter tier, each with their own defined limitations whether that be the number of API requests or content limits. I like to look into the future and see where my online presence may take me and at some point, I would need to consider the cost of a paid tier. How does that fit into my yearly hosting costs?

At the moment, I am paying £80 yearly. If I were to jump onto headless, the cheapest price I’ve seen equates to £66 a year and I haven’t factored in hosting costs yet. I could get away with low-cost hosting as my web build will be on a smaller scale and I plan my next build using the .NET Core framework.

If I had my own company or product where I was looking for ways to deliver content across multiple channels, I would use headless in a heartbeat. I could justify the cost as I know I would be getting my money’s worth and if I were to find myself exceeding a tiers limit I could just move onto the next.

Infrastructure and Stability

Infrastructure and stability of a headless service all come down to how much you’re willing to pay. The API uptime is the most important part after the platform features. I’ve noticed that some starter and free tiers do not state an uptime, for example, 99.9% or 99.5%. Depending on the technology stack, this might not be an issue where a constant connection to the API is required, for example, Gatsby.

I do think in this area where Headless CMS wins, is the failover and backup procedures in place. They would more than likely surpass the infrastructure in place from a personally hosted and managed site.

Platform Changes

It’s natural for improvements and changes to be made throughout the lifespan of a product. The only thing with headless you don’t have a choice on whether you want those changes as what works for one person may not necessarily work for another. You are locked into the release cycle.

I remember back in the early days when Headless CMS’s started to gain traction, releases were being made in such a quick turnaround at the expense of the editors who had to quickly adapt to the subtle changes in features. The good thing now is the dust has settled as the platform has gotten to the point of maturity.

The one area I still have difficulty getting over is the rich-text area. Each headless CMS provider seems to have their restrictions and you never really get full control over HTML markup unless a normal text area is used. There are ways around this but some restrictions still do exist.

Where do you as an individual fit into this lifecycle? That’s the million-dollar question. However, there is one headless platform that is very involved with feedback from their users, Kentico Kontent, where all ideas are put under consideration, voted on and (if successful) submitted into the roadmap. I haven’t seen this approach offered by other Headless CMS platforms and maybe this is something they should also do.

Trust

There is a trust aspect to an external provider storing your content. Data is your most valuable asset. Is there any chance in the service being discontinued at some-point? If I am being totally honest to myself, I don’t think this is a valid point as long as the chosen platform has proven it’s worth and cemented itself over a lengthy period of time. Choose the big players (in no particular order), such as:

  • Kontent
  • Contentful
  • Prismic
  • DatoCMS
  • ButterCMS

There is also another aspect to trust that draws upon a further point I made in the previous section regarding platform changes. In the past, I’ve seen content features getting deprecated. This doesn’t break your current build, just causes you to rethink things when updating to the newer version API interface.

Conclusion

I think moving to a Headless CMS requires a bigger leap than I thought. I say this purely from a personal perspective. The main piece of work would be to carry out content modelling for pages, migrate all my site content and media into the new platform and apply page redirects. This is before I have made a start in developing the new site.

I will always be in two minds on whether I should use a Headless CMS. If I wasn’t such a control-freak when it comes to every aspect of my application and content, I think I could make the move. Maybe I just need to learn to let go.

My First Butter CMS JavaScript Implementation

For one of my side projects, I was asked to use Butter CMS to allow for basic blog integration using JavaScript. I have never heard or used Butter CMS before and was intrigued to know more about the platform.

Butter CMS is another headless CMS variant that allows a developer to utilise API endpoints to push content to an application via an arrange of approaches. So nothing new here. Just like any headless CMS, the proof is in the pudding when it comes to the following factors:

  • Quality of features
  • Ease of integration
  • Price points
  • Quality of documentation

I haven't had a chance to properly look into what Butter CMS fully has to offer, but from what I have seen from working on the requirements for this side project I was pleasently surprised. Found it really easy to get setup with minimal amount of fuss! For this project I used Butter CMS's Blog Engine package, which does exactly what it says on the tin. All the fields you need for writing blog posts are already provided.

JavaScript Code

My JavaScipt implementation is pretty basic and provides the following functionality:

  • Outputs a list of posts consisting of title, date and summary text
  • Pagination
  • Output a single blog post

All key functionality is derived from the "ButterCMS" JavaScript file:

/*****************************************************/
/*                    Butter CMS                                 */
/*****************************************************/
var ButterCMS =
{
    ButterCmsObj: null,

    "Init": function () {
        // Initiate Butter CMS.
        this.ButterCmsObj = new ButterCmsBlogData();
        this.ButterCmsObj.Init();
    },
    "GetBlogPosts": function () {
        BEButterCMS.ButterCmsObj.GetBlogPosts(1);
    },
    "GetSinglePost": function (slug) {
        BEButterCMS.ButterCmsObj.GetSinglePost(slug);
    }
};

/*****************************************************/
/*                Butter CMS Data                         */
/*****************************************************/
function ButterCmsBlogData() {
    var apiKey = "<Enter API Key>",
        baseUrl = "/",
        butterInstance = null,
        $blogListingContainer = $("#posts"),
        $blogPostContainer = $("#post-individual"),
        pageSize = 10;

    // Initialise of the ButterCMSData object get the data.
    this.Init = function () {
        getCMSInstance();
    };

    // Returns a list of blog posts.
    this.GetBlogPosts = function (pageNo) {
        // The blog listing container needs to be cleared before any new markup is pushed.
        // For example when the next page of data is requested.
        $blogListingContainer.empty();

        // Request blog posts.
        butterInstance.post.list({ page: pageNo, page_size: pageSize }).then(function (resp) {
            var body = resp.data,
                blogPostData = {
                    posts: body.data,
                    next_page: body.meta.next_page,
                    previous_page: body.meta.previous_page
                };

            for (var i = 0; i < blogPostData.posts.length; i++) {
                $blogListingContainer.append(blogPostListItem(blogPostData.posts[i]));
            }

            //----------BEGIN: Pagination--------------//

            $blogListingContainer.append("<div>");

            if (blogPostData.previous_page) {
                $blogListingContainer.append("<a class=\"page-nav\" href=\"#\" data-pageno=" + blogPostData.previous_page + " href=\"\">Previous Page</a>");
            }

            if (blogPostData.next_page) {
                $blogListingContainer.append("<a class=\"page-nav\" href=\"#\" data-pageno=" + blogPostData.next_page + " href=\"\">Next Page</a>");
            }

            $blogListingContainer.append("</div>");

            paginationOnClick();

            //----------END: Pagination--------------//
        });
    };

    // Retrieves a single blog post based on the current URL of the page if a slug has not been provided.
    this.GetSinglePost = function (slug) {
        var currentPath = location.pathname,
            blogSlug = slug === null ? currentPath.match(/([^\/]*)\/*$/)[1] : slug;

        butterInstance.post.retrieve(blogSlug).then(function (resp) {
            var post = resp.data.data;

            $blogPostContainer.append(blogPost(post));
        });
    };

    // Renders the HTML markup and fields for a single post.
    function blogPost(post) {
        var html = "";

        html = "<article>";

        html += "<h1>" + post.title + "</h1>";
        html += "<div>" + blogPostDateFormat(post.created) + "</div>";
        html += "<div>" + post.body + "</div>";
        
        html += "</article>";

        return html;
    }

    // Renders the HTML markup and fields when listing out blog posts.
    function blogPostListItem(post) {
        var html = "";

        html = "<h2><a href=" + baseUrl + post.url + ">" + post.title + "</a></h2>";
        html += "<div>" + blogPostDateFormat(post.created) + "</div>";
        html += "<p>" + post.summary + "</p>";

        if (post.featured_image) {
            html += "<img src=" + post.featured_image + " />";
        }

        return html;
    }

    // Set click event for previous/next pagination buttons and reload the current data.
    function paginationOnClick() {
        $(".page-nav").on("click", function (e) {
            e.preventDefault();
            var pageNo = $(this).data("pageno"),
                butterCmsObj = new ButterCmsBlogData();

            butterCmsObj.Init();
            butterCmsObj.GetBlogPosts(pageNo);
        });
    }

    // Format the blog post date to dd/MM/yyyy HH:mm
    function blogPostDateFormat(date) {
        var dateObj = new Date(date);

        return [dateObj.getDate().padLeft(), (dateObj.getMonth() + 1).padLeft(), dateObj.getFullYear()].join('/') + ' ' + [dateObj.getHours().padLeft(), dateObj.getMinutes().padLeft()].join(':');
    }

    // Get instance of Butter CMS on initialise to make one call.
    function getCMSInstance() {
        butterInstance = new Butter(apiKey);
    }
}

// Set a prototype for padding numerical values.
Number.prototype.padLeft = function (base, chr) {
    var len = (String(base || 10).length - String(this).length) + 1;

    return len > 0 ? new Array(len).join(chr || '0') + this : this;
};

To get a list of blog posts:

// Initiate Butter CMS.
BEButterCMS.Init();

// Get all blog posts.
BEButterCMS.GetBlogPosts();

To get a single blog post, you will need to pass in the slug of the blog post via your own approach:

// Initiate Butter CMS.
BEButterCMS.Init();

// Get single blog post.
BEButterCMS.GetSinglePost(postSlug);