Blog

Blogging on programming and life in general.

  • Published on
    -
    1 min read

    Cache Busting Kentico

    When developing a website that is quick to load on all devices, caching from both a data and asset perspective is very important. Luckily for us, Kentico provides a comprehensive approach to caching data in order to minimise round-trips to the database. But what about asset caching, such as images, CSS and JavaScript files?

    A couple days ago, I wrote an article on the Syndicut Medium publication on how I have added cache busting functionality in our Kentico CMS builds. I am definitely interested to hear what the approaches other developers from the Kentico network take in order to cache bust their own website assets.

    Take a read here: https://medium.com/syndicutstudio/cache-busting-kentico-cf89496ffda0.

  • When building MVC websites, I cannot get through a build without using a method to convert a partial view to a string. I have blogged about this in the past and find this approach so useful especially when carrying out heavy AJAX processes. Makes the whole process of maintaining and outputting markup dynamically a walk in the park.

    I've been dealing with many more ASP.NET Core builds and migrating over the RenderPartialViewToString() extension I developed previously was not possible. Instead, I started using the approach detailed in the following StackOverflow post: Return View as String in .NET Core. Even though the approach was perfectly acceptable and did the job nicely, I noticed I had to make one key adjustment - allow for views outside controller context.

    The method proposed in the StackOverflow post uses ViewEngine.FindView(), from what I gather only returns a view within the current controller context. I added a check that will use ViewEngine.GetView() if a path of the view ends with a ".cshtml" which is normally the approach used when you refer to a view from a different controller by using a relative path.

    public static class ControllerExtensions
    {
        /// <summary>
        /// Render a partial view to string.
        /// </summary>
        /// <typeparam name="TModel"></typeparam>
        /// <param name="controller"></param>
        /// <param name="viewNamePath"></param>
        /// <param name="model"></param>
        /// <returns></returns>
        public static async Task<string> RenderViewToStringAsync<TModel>(this Controller controller, string viewNamePath, TModel model)
        {
            if (string.IsNullOrEmpty(viewNamePath))
                viewNamePath = controller.ControllerContext.ActionDescriptor.ActionName;
    
            controller.ViewData.Model = model;
    
            using (StringWriter writer = new StringWriter())
            {
                try
                {
                    IViewEngine viewEngine = controller.HttpContext.RequestServices.GetService(typeof(ICompositeViewEngine)) as ICompositeViewEngine;
    
                    ViewEngineResult viewResult = null;
    
                    if (viewNamePath.EndsWith(".cshtml"))
                        viewResult = viewEngine.GetView(viewNamePath, viewNamePath, false);
                    else
                        viewResult = viewEngine.FindView(controller.ControllerContext, viewNamePath, false);
    
                    if (!viewResult.Success)
                        return $"A view with the name '{viewNamePath}' could not be found";
    
                    ViewContext viewContext = new ViewContext(
                        controller.ControllerContext,
                        viewResult.View,
                        controller.ViewData,
                        controller.TempData,
                        writer,
                        new HtmlHelperOptions()
                    );
    
                    await viewResult.View.RenderAsync(viewContext);
    
                    return writer.GetStringBuilder().ToString();
                }
                catch (Exception exc)
                {
                    return $"Failed - {exc.Message}";
                }
            }
        }
    
        /// <summary>
        /// Render a partial view to string, without a model present.
        /// </summary>
        /// <typeparam name="TModel"></typeparam>
        /// <param name="controller"></param>
        /// <param name="viewNamePath"></param>
        /// <returns></returns>
        public static async Task<string> RenderViewToStringAsync(this Controller controller, string viewNamePath)
        {
            if (string.IsNullOrEmpty(viewNamePath))
                viewNamePath = controller.ControllerContext.ActionDescriptor.ActionName;
                
            using (StringWriter writer = new StringWriter())
            {
                try
                {
                    IViewEngine viewEngine = controller.HttpContext.RequestServices.GetService(typeof(ICompositeViewEngine)) as ICompositeViewEngine;
    
                    ViewEngineResult viewResult = null;
    
                    if (viewNamePath.EndsWith(".cshtml"))
                        viewResult = viewEngine.GetView(viewNamePath, viewNamePath, false);
                    else
                        viewResult = viewEngine.FindView(controller.ControllerContext, viewNamePath, false);
    
                    if (!viewResult.Success)
                        return $"A view with the name '{viewNamePath}' could not be found";
    
                    ViewContext viewContext = new ViewContext(
                        controller.ControllerContext,
                        viewResult.View,
                        controller.ViewData,
                        controller.TempData,
                        writer,
                        new HtmlHelperOptions()
                    );
    
                    await viewResult.View.RenderAsync(viewContext);
    
                    return writer.GetStringBuilder().ToString();
                }
                catch (Exception exc)
                {
                    return $"Failed - {exc.Message}";
                }
            }
        }
    
    }
    

    Quick Example

    As you can see from my quick example below, the Home controller is using the RenderViewToStringAsync() when calling:

    • A view from another controller, where a relative path to the view is used.
    • A view from within the realms of the current controller and the name of the view alone can be used.
    public class HomeController : Controller
    {
        public async Task<IActionResult> Index()
        {
            NewsListItem newsItem = GetSingleNewsItem(); // Get a single news item.
    
            string viewFromAnotherController = await this.RenderViewToStringAsync("/Views/News/_NewsList.cshtml", newsItem);
            string viewFromCurrentController = await this.RenderViewToStringAsync("_NewsListHome", newsItem);
    
            return View();
        }
    }
    
  • It seems whenever I work on an ASP.NET Core website, I always seem to get the most unhelpful error when deploying to production:

    HTTP Error 502.5 - Process Failure

    I have no problem running the ASP.NET Core site whilst developing from within a local environment.

    From past experience, the HTTP 502.5 error generally happens for the following reasons:

    1. The ASP.NET Core framework is not installed or your site is running the incorrect version.
    2. Website project incorrectly published.
    3. Potential configuration issue at code level.

    Generally when you successfully publish a deployable version of your site, you'd expect it to just work. To get around the deployment woes, the solution is to modify your .csproj file by adding the following setting:

    <PropertyGroup>
       <PublishWithAspNetCoreTargetManifest>false</PublishWithAspNetCoreTargetManifest>
    </PropertyGroup>
    

    Once this setting has been added, you'll notice when your site is re-published a whole bunch of new DLL files are now present, forming part of all the dependencies a site requires. It's strange a normal publish does not do this already and what's even stranger is I have a different .NET Core site running without having to take this approach.

    For any new .NET Core sites I work on, I will be using approach going forward.

    Useful Links

  • Every few weeks, I check over the health of my site through Google Search Console (aka Webmaster Tools) and Analytics to see how Google is indexing my site and look into potential issues that could affect the click-through rate.

    Over the years the content of my site has grown steadily and as it stands it consists of 250 published blog posts. When you take into consideration other potential pages Google indexes - consisting of filter URL's based on grouping posts by tag or category, the number of links that my site consists is increased considerably. It's to the discretion of Google's search algorithm to whether it includes these links for indexing.

    Last month, I decided to scrutinise the Search Console Index Coverage report in great detail just to see if there are any improvements I can make to alleviate some minor issues. What I wasn't expecting to see is the large volume of links marked as "Crawled - Currently not indexed".

    Crawled Currently Not Indexed - 225 Pages

    Wow! 225 affected pages! What does "Crawled - Currently not indexed" mean? According to Google:

    The page was crawled by Google, but not indexed. It may or may not be indexed in the future; no need to resubmit this URL for crawling.

    Pretty self-explanatory but not much guidance on the process on how to lessen the number of links that aren't indexed. From my experience, the best place to start is to look at the list of links that are being excluded and to form a judgement based on the page content of these links. Unfortunately, there isn't an exact science. It's a process of trial and error.

    Let's take a look at the links from my own 225 excluded pages:

    Crawled Currently Not Indexed - Non Indexed Links

    On initial look, I could see that the majority of the URL's consisted of links where users can filter posts by either category or tag. I could see nothing content-wise when inspecting these pages for a conclusive reason for index exclusion. However, what I did notice is that these links were automatically found by Google when the site gets spidered. The sitemap I submitted in the Search Console only list out blog posts and content pages.

    This led me to believe a possible solution would be to create a separate sitemap that consisted purely of links for these categories and tags. I called it metasitemap.xml. Whenever I added a post, the sitemap's "lastmod" date would get updated, just like the pages listed in the default sitemap.

    I created and submitted this new sitemap around mid-July and it wasn't until four days ago the improvement was reported from within the Search Console. The number of non-indexed pages was reduced to 58. That's a 74% reduction!

    Crawled Currently Not Indexed - 58 Pages

    Conclusion

    As I stated above, there isn't an exact science for reducing the number of non-indexed pages as every site is different. Supplementing my site with an additional sitemap just happened to alleviate my issue. But that is not to say copying this approach won't help you. Just ensure you look into the list of excluded links for any patterns.

    I still have some work to do and the next thing on my list is to implement canonical tags in all my pages since I have become aware I have duplicate content on different URL's - remnants to when I moved blogging platform.

    If anyone has any other suggestions or solutions that worked for them, please leave a comment.

  • I've been doing some personal research into improving my own JavaScript development. I decided to get more familiar with the new version of JavaScript - ES6. ES6 is filled to the brim with some really nice improvements that make JavaScript development much more concise and efficient. After having the opportunity to work on React and React Native projects, I had a chance in putting my new found ES6 knowledge to good use!

    If I had to describe ES6 in a sentence:

    JavaScript has gone on a diet and cut the fat. Write less, do more!

    I have only scratched the surface to what ES6 has to offer and will continue to add more to the list as I learn. If you are familiar with server-side development, you might notice some similarities from a syntax perspective. That in itself shows how far ES6 has pushed the boundaries.

    Arrow Functions

    Arrow functions are beautiful and so easy on the eye when scrolling through vast amounts of code. You'll see with arrow functions, you'll have the option to condense a function that consists of many lines all the way down to single line.

    The traditional way we are all familiar with:

    // The "old school" way..
    function addSomeNumbers(a, b) {
        return a + b;
    }
    
    console.log(addSomeNumbers(1, 2));
    // Output: 3
    

    ES6:

    // ES6.
    const addSomeNumbers = (a, b) => {
        return a + b;
    }
    
    console.log(addSomeNumbers(1, 2));
    // Output: 3
    

    The traditional and ES6 way can still be used in the same way to achieve our desired output. But we can condense out arrow function further:

    // Condensed ES6 arrow function.
    const addSomeNumbers = (a, b) => a + b;
    
    console.log(addSomeNumbers(1, 2));
    // Output: 3
    

    Default Function Parameters

    When developing using server-side languages, such as C# you have the ability to set default values on the parameters used for your functions. This is great, since you have more flexibilty in using a function over a wider variety of circumstances without the worry of compiler errors if you haven't satisfied all function parameters.

    Lets expand our "addSomeNumbers()" function from our last section to use default parameters.

    // Condensed ES6 arrow function with default parameters.
    const addSomeNumbers = (a=0, b=0) => a + b;
    
    console.log(addSomeNumbers());
    // Output: 0
    

    This is an interesting (but somewhat useless) example where I am using "addSomeNumbers()" function without passing any parameters. As a result the value 0 is returned and even better - no compiler error.

    Destructuring

    Destructuring sounds scary and complex. In its simple terms, destructuring is the process of adding values to an object or array to an existing variable more straightforward. Lets start of with a simple object and how we can output these values:

    // Some info on my favourite Star Trek starship...
    const starship = {
      registry: "NCC-1701-E",
        captain: "Jean Luc Picard",
        launch_date: "October 30, 2372",
        spec: {
          max_warp: 9.995,
          mass: "3,205,000 metric tons",
          length: "685.7 meters",
          width: "250.6 meters",
          height: "88.2 meters"
      }
    };
    

    We would normally output the these values in the following way:

    var registry = starship.registry; // Output: NCC-1701-E
    var captain = starship.captain; // Output: Jean Luc Picard
    var launchDate = starship.launch_date; // Output: October 30, 2372
    

    This works well, but the process of returning those values is a little repetitive and spread over many lines. Lets get a bit more focus and go down the ES6 route:

    const { registry, captain, launch_date } = starship;
    
    console.log(registry); // Output: NCC-1701-E 
    

    How amazing is that? We've managed to select a handful of these fields on one line to do something with.

    My final example in the use of destructuring will evolve around an array of items - in this case names of starship captains:

    const captains = ["James T Kirk", "Jean Luc Picard", "Katherine Janeway", "Benjamin Sisko"]
    

    Here is how I would return the first two captains in ES5 and ES6:

    // ES5
    var tos = captains[0];
    var tng = captains[1];
    
    // ES6
    const [tos, tng ] = captains;
    

    You'll see similarities to our ES6 approach for getting the values out of an array as we did when using an object. The only thing I need to look into is how to get the first and last captain from my array? Maybe that's for a later post.

    Before I end the destructuring topic, I'll add this tweet - a visual feast on the basis of what destructuring is...

    Destructuring. Courtesy of @NikkitaFTW pic.twitter.com/j8OX3VyrTL
    — Burke Holland (@burkeholland) May 31, 2018

    Spread Operator

    The spread operator has to be my favourite ES6 feature, purely because in my JavaScript applications I do a lot of data manipulation. If you can get your head around destructuring and the spread operator, you'll find working with data a lot easier. A spread operator is "...". Yes three dots - ellipsis if you prefer. This allows you to copy the values of an object to be used as a basis of a new object.

    In its basic form:

    const para1 = ["to", "boldly", "go"];
    const para2 = [...para1, "where", "no", "one"];
    const para3 = [...para2, "has", "gone", "before"];
    
    console.log(para1); // Output: ["to", "boldly", "go"]
    console.log(para2); // Output: ["to", "boldly", "go", "where", "no", "one"]
    console.log(para3); // Output: ["to", "boldly", "go", "where", "no", "one", "has", "gone", "before"]
    

    As you can see from my example above, the spread operator used on variables "para1" and "para2" creates a shallow copy of the array values into our new array. Gone are the days of having to use a for loop to get the values.

  • This is a relatively simple pagination that will only be shown if there are enough items of data to paginate through. The user will have the ability to paginate by either clicking on the "Previous" and "Next" links as well as clicking on the individual page numbers from within the pagination.

    I created a PaginationHelper.CreatePagination() method that carries out all the paging calculations and outputs the pagination as an unordered list. The method requires the following parameters:

    • currentPage - the current page number being viewed.
    • totalNumberOfRecords - the total count of records from your dataset in order to determine how many pages should be displayed.
    • pageRequest - the current request from by passing in "HttpContext.Request" to get the page URL.
    • noOfPageLinks - the number of page numbers that should be shown. For example "1, 2, 3, 4".
    • pageSize - the number of items will be shown per page.
    using Microsoft.AspNetCore.Http;
    using System;
    using System.Text;
    
    namespace MyProject.Helpers
    {
        public static class PaginationHelper
        {
            /// <summary>
            /// Renders pagination used in listing pages.
            /// </summary>
            /// <param name="currentPage"></param>
            /// <param name="totalNumberOfRecords"></param>
            /// <param name="pageRequest">Current page request used to get the URL path of the page.</param>
            /// <param name="noOfPagesLinks">Number of pagination numbers to show.</param>
            /// <param name="pageSize"></param>
            /// <returns></returns>
            public static string CreatePagination(int currentPage, int totalNumberOfRecords, HttpRequest pageRequest, int noOfPagesLinks = 5, int pageSize = 10)
            {
                StringBuilder paginationHtml = new StringBuilder();
    
                // Only render the pagination markup if the total number of records is more than our page size.
                if (totalNumberOfRecords > pageSize)
                {
                    #region Pagination Calculations
    
                    int amountOfPages = (int)(Math.Ceiling(totalNumberOfRecords / Convert.ToDecimal(pageSize)));
    
                    int startPage = currentPage;
    
                    if (startPage == 1 || startPage == 2 || amountOfPages < noOfPagesLinks)
                        startPage = 1;
                    else
                        startPage -= 2;
    
                    int maxPage = startPage + noOfPagesLinks;
    
                    if (amountOfPages < maxPage)
                        maxPage = Convert.ToInt32(amountOfPages) + 1;
    
                    if (maxPage - startPage != noOfPagesLinks && maxPage > noOfPagesLinks)
                        startPage = maxPage - noOfPagesLinks;
    
                    int previousPage = currentPage - 1;
                    if (previousPage < 1)
                        previousPage = 1;
    
                    int nextPage = currentPage + 1;
    
                    #endregion
    
                    #region Get Current Path
    
                    // Get current path.
                    string path = pageRequest.Path.ToString();
    
                    int pos = path.LastIndexOf("/") + 1;
    
                    // Get last route value.
                    string lastRouteValue = path.Substring(pos, path.Length - pos).ToLower();
    
                    // Removes page number from end of path if path contains a page number.
                    if (lastRouteValue.StartsWith("page"))
                        path = path.Substring(0, path.LastIndexOf('/'));
    
                    #endregion
    
                    paginationHtml.Append("<ul>");
    
                    if (currentPage > 1)
                        paginationHtml.Append($"<li><a href=\"{path}/Page{previousPage}\"><span>Previous page</span></a></li>");
    
                    for (int i = startPage; i < maxPage; i++)
                    {
                        // If the current page equals one of the pagination numbers, set active state.
                        if (i == currentPage)
                            paginationHtml.Append($"<li><a href=\"{path}/Page{i}\" class=\"is-active\"><span>{i}</span></a></li>");
                        else
                            paginationHtml.Append($"<li><a href=\"{path}/Page{i}\"><span>{i}</span></a></li>");
                    }
    
                    if (startPage + noOfPagesLinks < amountOfPages && maxPage > noOfPagesLinks || currentPage < amountOfPages)
                        paginationHtml.Append($"<li><a href=\"{path}/Page{nextPage}\"><span>Next page</span></a></li>");
    
                    paginationHtml.Append("</ul>");
    
                    return paginationHtml.ToString();
                }
                else
                {
                    return string.Empty;
                }
            }
        }
    }
    

    The PaginationHelper.CreatePagination() method can then be used inside a controller where you would like to list your data as well as render the pagination. A simple example of this would be as follows:

    /// <summary>
    /// List all news articles.
    /// </summary>
    /// <param name="page"></param> 
    /// <param name="pageSize"></param>
    /// <returns></returns>
    [Route("/Articles")]
    [Route("/Articles/Page{page}")]
    public ActionResult Index(int page = 1, int pageSize = 10)
    {
        // Number of articles to skip.
        int skip = 0;
        if (page != 1)
            skip = (page - 1) * pageSize;
    
        // Get list of articles from my datasource.
        List<NewsArticle> articles = MyData.GetArticles().Skip(skip).Take(pageSize).ToList();
    
        //Render Pagination.
        ViewBag.PaginationHtml = PaginationHelper.CreatePagination(page, articles.Count, HttpContext.Request, pageSize: pageSize);
    
        return View(articles);
    }
    

    The pagination will be output to a ViewBag that can be called from within your view. I could have gone down a different route and developed Partial View along with the appropriate model. But for my use the method approach offers most flexibility, as I could have the option to either use this from within a controller or view.

  • Memories are what give life purpose. They allow us to go back to the past, into a time that shall forever be stateless. Most importantly memories are experiences that mould us into the person we are today.

    For some reason, when I think about the word memories the first thing that come to mind are pictures... Photos to be exact. I only started thinking how important photos are to me whilst I was having a conversation with my cousin Tajesh. Tajesh popped over this weekend gone by and like always has many fascinating stories to tell. One story in particular got my attention. He told me about an amazing trip he had in Australia many many years ago and how he lost all the photos he had taken after recently damaging his hard drive. On hearing his predicament, I was profoundly moved and imagined how I'd feel if I was in his position.

    Even though our brains are wired to remember events and experiences, memories seem to somehow fade away over time and we start forgetting the little detail of images until it forms into a hazy recall. We remember enough to transport us back to a time or a place, but the brain has a strange way of patching together what we once saw. As if they are pieces of a larger puzzle. If your brain is anything like mine where you can only selectively retrieve one piece of the puzzle that is most meaningful, we're missing a vast array of information.

    I decided I'd make an attempt to try and recover my cousins lost photos. He handed over his Western Digital Caviar edition hard drive carefully enclosed in an old VHS box, entrusting I'll have it's best interests at heart and keeping whatever memories that maybe locked away safe inside... A damaged hard drive is in some ways like our brains selective recall. The data is stored somewhere but we sometimes have problems accessing them.

    I'm no hard disk recovery expert and I am hoping some off the shelf software will help me in getting at least some photos back from his holiday. So what's the game plan?

    I'll start with using a piece of software I blogged about back in 2011 - EaseUs. EaseUs provides a line of software ranging from backup to recovery. It helped me then and (fingers crossed) it'll help me now. I'll also need a 3.5 inch disk caddy to allow the hard drive to be connected via USB and start the recovery process.

    As it stands, my cousins Western Digital Caviar disk doesn't seem to have any visible damage and there are no noises when run. It just doesn't boot.

    Stay tuned for future posts on how I get on.

    To be continued...

  • Published on
    -
    4 min read

    My Time At Melia Bali Hotel

    Family. Family is what comes to mind when I think of my time at Melia Bali. I only happen to come to this conclusion as I checked out on the calm and (strangely) cool evening before having to depart back to the UK.

    I don't generally write about my travels (or lack of!). But my time at Melia has energised me to write something and as a result, I scribble away madly trying to make sense of processing my erratic thoughts and feelings during my long flight back. Just so I can write this post.

    Melia Bali Coconut On The Beach

    I find it ironic I started writing this post about "family", when I happened to visit Bali with people to whom I deem most dear: mum, dad and sister. I feel that family forms centre place to the services they provide.

    If you happen to have the privilege of staying at the Melia Bali Hotel for long enough, you'd get a sense of familiarity of the people working there. To some extent I hope I am perhaps familiar to them - The Indian guy with the ridiculously frizzy hair (a result of the climate ;-) ).

    These people truly are the back bone of the hotel and make it what it is. Yes, Melia is a pretty place on the surface but it's the people that make it truly shine when compared to the other hotels staggered along the beach shoreline. They are without a doubt amazing at what they do. From the lady who cooks me the most delicious fluffy omelette in the morning down to the lobby personnel who are willing to help with any query or concern.

    From the moment I wake up and make my way to the breakfast hall to the moment I enter the lobby at the end of a long day gallivanting, I am greeted with many smiles. You can't help but be infected with a sense of positivity and happiness, something I don't think I've ever come across when holidaying elsewhere.

    The lobby statues and wondrous ceiling mural makes for a welcome sight at any time, setting the ambience and standard for the hotel. If you're lucky to be at the lobby during the evening, you'd be greeted by two classically trained balinese dancers dressed from an era of time gone by. As they dance with delicate intricacy to the tune of the rindik, I am reminded of similarities when compared to classical Indian dances - a lost art and cultural heritage slowly eroding with time, making for a visceral experience and something you can't help but appreciate.

    Melia Bali Dancers

    Melia offers around five restaurants to cater for the guests varying palettes - each with their own theme and cuisines. We found ourselves venturing outside to nearby restaurants as after a few nights as we found the prices a little dear based on the portion sizes of the main meals. Even though the food was very tasty, my western belly expected something more sizeable. When taking into consideration the 21% combined tax and service charge on top of the prices on the menu - not so cheap. There are many fine eateries at the Bali Collection for consideration, just a 5-10 minute walk away.

    When there are issues, it is family that are there to support at time of need. We happened to experience some very loud noises from the room above us at an unsightly hour. Spoiling the serenity we become accustomed to. Now this went on for a couple nights. We just happened to make a remark of our problem in passing to one of the workers whilst feasting on the morning breakfast buffet and within a a short period of time this was communicated to the customer service representative who apologised and organised a room change swiftly.

    The rooms themselves are all very well maintained, clean and provided nice views from the balcony. Based on our room change, you can expect subtle differences in terms of the what the rooms offer. For example, our first room just had a shower, but the second room had a shower/bath. My only quibble is the shower head position - not a deal breaker. There are generous bathroom amenities, consisting of toothbrush, toothpaste, vanity kit, shampoo, conditioner, shower gel, shaving kit and body lotion. All fully restocked daily. The crazy thing is that you get a new toothbrush every day! As much as I like a new toothbrush, I sometimes have to question the environmental impact.

    Melia Bali - View from Lobby

    Even though our noisy neighbour was no fault of thier own, Melia took us under their wing to ensure our holiday was perfect. We were even given a fruit platter for our troubles. I think what struck a chord with me is before we departed, is that the management seem to know everything about a guests stay to a granular level. The hotel manager hoped we'd comeback again to visit even with the minor inconvenience we experienced.

    If I do get another opportunity to visit again, I will consider paying a little extra for "The Level" experience. I have to admit, I was quite envious of all the things I heard about this upgrade when talking to other guests on the beach. I felt like a pauper. I like the idea of having a little more privacy in terms of accomodation and your own space on the beach. More importantly it's adults only. No noisy kids! :-)

    Melia Bali - Entrance At Night

    I look back at my time at Melia Bali with fond memories that will be permanently etched into my memory. For me, Melia compliments Bali as the wondrous land of Bali compliments Melia.

  • 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);
    
  • If you have many sites running on your installation of Windows Server, you will soon find that there will be an accumulation of logs generated by IIS. Through my niavity, I presumed that there is a default setting in IIS that would only retain logs for a specific period of time. It is only when I started noticing over the last few weeks the hard disk space was slowly getting smaller and smaller.

    Due to my sheer embaressment, I won't divulge how much space the logs had taken up. All I can say, it was quite a substantial amount. :-)

    After some Googling online, I came across a Powershell script (which can be found here), that solved all my problems. The script targets your IIS logs folder and recusively looks for any file that contains ".log" for deletion. Unfortunately, the script did not run without making some minor modifications to the original source. This is due to changes in versions of Powershell since the post was written 3 years ago.

    $logPath = "C:\inetpub\logs\LogFiles" 
    $maxDaystoKeep = -5
    $cleanupRecordPath = "C:\Log_Cleanup.log" 
    
    $itemsToDelete = dir $logPath -Recurse -File *.log | Where LastWriteTime -lt ((get-date).AddDays($maxDaystoKeep)) 
    
    If ($itemsToDelete.Count -gt 0)
    { 
        ForEach ($item in $itemsToDelete)
        { 
            "$($item.FullName) is older than $((get-date).AddDays($maxDaystoKeep)) and will be deleted." | Add-Content $cleanupRecordPath 
            Remove-Item $item.FullName -Verbose 
        } 
    } 
    Else
    { 
        "No items to be deleted today $($(Get-Date).DateTime)." | Add-Content $cleanupRecordPath 
    }    
    
    Write-Output "Cleanup of log files older than $((get-date).AddDays($maxDaystoKeep)) completed!" 
    
    Start-Sleep -Seconds 10
    

    If you're ever so inclined, hook this script up to a Scheduled Task to run on a daily basis to keep your log files in order.