Blog

Posts written in December 2021.

  • Published on
    -
    5 min read

    Year In Review - 2021

    I haven't met any of the tasks I set myself based on my last year in review. But as life springs up random surprises, you see yourself shifting to a moment in time that you never thought was conceivable.

    If someone were to tell me last year that 2021 would be the year I'd find someone and finally settle down, I'd say you've been drinking too much of the finest Rioja.

    When such a shift in one's life happens, this takes utmost priority and as a result, my blogging has taken a backseat. After April things have been a little sporadic - a time when the new stage in my life kicked up a notch.

    Even though blogging this year hasn't been a priority, it's not a result of a lack of learning. I've just been focusing on learning some new life skills during this new stage in my life as well as keeping on top of new technological advances within a work environment on a daily basis.

    2021 In Words/Phrases

    Coronavirus, Covid-19, Omicron, Hubspot, Wedding, No Time To Die, Money Heist, Tailwind CSS, Prismic, Gatsby Prismic, Beard, Azure, Back The Gym, Blenheim Light Show, Camping, Abingdon Fireworks, New Family/Friends

    My Site

    Believe it or not, I have been working on an updated version by completely starting from scratch. This involved updating to the latest Gatsby framework and redoing the front-end. I came across a very good tried and tested CSS framework called Tailwind CSS.

    Tailwind is a utility CSS framework that allows for a quick turnaround in building blocks of markup to create bespoke designs based on a library of flexible predefine CSS classes. The main benefits I found so far is that it has a surprisingly minimal footprint when building for production and many sites have pre-developed HTML components you can customise and implement on your site. Only time will tell whether this is the correct approach.

    Beard Gains

    Growing some facial hair wasn't an outcome to living like a hermit during these Covid times, but a requirement from my wife. My profile picture is due for an update to reflect such a change in appearance. Even I don't recognise myself sometimes.

    Statistics

    When it comes to site statistics, I tend to lower my expectations so I'm not setting myself up for failure when it comes to checking Google Analytics. I wasn't expecting much from this year's stats due to my lack of contribution, but suffice to say I haven’t faired too badly.

    2020/2021 Comparison:

    • Users: +41.09%
    • Page Views: +45.56%
    • New Users: +42.03%
    • Bounce Rate: -3.06%
    • Search Console Total Clicks: +254%
    • Search Console Impressions: +295%
    • Search Console Page Position: -8.3%

    I'm both surprised and relieved that existing content is still getting traction resulting in more page views and users. The bounce rate has decreased a further 3.05% over last year. Out of all the statistics listed above, I believe the Google Page Position is the most important and quite disheartened that I've slipped up in this area.

    To my surprise, the site search implemented earlier this year using Algolia was getting used by visitors. This was very unexpected as the primary reason why I even added a site search is mainly for my use.

    One can only imagine how things could have been if I managed to be more consistent in the number of posts published over the year.

    Things To Look Into In 2022

    NFT and Crypto

    The main thing I want to look into further is the realms of Cryptocurrency and NFT’s. I’ve been following the likes of Dan Petty and Paul Stamatiou on Twitter and has opened my eyes to how things have moved on since I last took a brief look at this space.

    Holiday

    I haven’t been on a holiday since my trip to the Maldives in 2019 and I’m well overdue on another one - preferably abroad if I feel safe enough to do so and COVID allowing.

    Lego Ford Mustang

    I purchased a Lego Creator Series Ford Mustang near the end of last year as an early Christmas present to myself and I’m still yet to complete it. I’ve gone as far as building the underlying chassis and suspension. It doesn’t even resemble a car yet. How embarrassing. :-)

    On completion, it’ll make a fine centre-piece in my office.

    Azure

    Ever since I worked on a project at the start of the year where I was dealing with Azure Functions, deployment slots and automation I’ve been more interested in the services Azure has to offer. I’ve always stuck to the hosting related service setup and search indexing. Only ventured very little elsewhere. I’d like to keep researching in this area, especially in cognitive services.

    Git On The Command-Line

    Even though I’ve been using Git for as long as I’ve been working as a developer, it has always been via a GUI such as TortoiseGit or SourceTree. When it comes to interacting with a Git repo using the command-line, I’m not as experienced as I’d like to be when it comes to the more complex commands. In the past when I have used complex commands without a GUI, it’s been far more straightforward when compared to the comfort of a GUI where I naturally find myself when interacting with a repository.

    Twitter Bot

    For some reason, I have a huge interest in creating a Twitter bot that will carry out some form of functionality based on the contents of a tweet. At this moment in time, I have no idea what the Twitter bot will do. Once I have thought of an endearing task it can perform, the development will start.

    Final Thoughts

    If you thought 2021 was bad enough with the continuation of the sequel no one wanted (Covid part 2), we are just days away from entering the year 2022. The year grim events took place from the fictitious film - Soylent Green.

    Soylent Green Poster (1973)

    Luckily for me, 2021 has been a productive year filled with personal and career-based accomplishments and hoping for this to continue into the new year. But I do feel it's time I pushed myself further.

    I’d like to venture more into technologies that don’t form part of my existing day-to-day coding language or framework. This may make for more interesting blog posts. But to do this, I need to focus more over the next year and allocate time for research and writing.

  • Published on
    -
    5 min read

    Umbraco: Programmatically Add/Update A Content Page

    I decided to write this post to primarily act as a reminder to myself when dealing with programmatically creating content pages in Umbraco and expanding upon my previous post on setting a dropdownlist in code. I have been working on a piece of functionality where I needed to develop an import task to pull in content from a different CMS platform to Umbraco that encompassed the use of different field-types, such as:

    • Textbox
    • Dropdownlist
    • Media Picker
    • Content Picker

    It might just be me, but I find it difficult to find solutions to Umbraco related problems I sometimes face. This could be due to results returned in search engines reference forum posts for older versions of Umbraco that are no longer compatible in the version I'm working in (version 8).

    When storing data in the field types listed (above), I encountered issues when trying to store values in all field types except “Textbox”. The other fields either required some form of JSON structure or Udi to be parsed.

    Code

    My code contains three methods:

    1. SetPost - to create a new blog post, or update an existing blog post if one already exists.
    2. GetAuthorIdByName - uses Umbraco Examine Search Index to get back an Author document and return the Udi.
    3. GetUmbracoMedia - uses the internal Examine Search Index to return details of a file in a form that will be acceptable to store within a Media Picker content field.

    The SetPost method consists of a combination of fields required by my Blog Post document, the primary ones being:

    • Blog Post Type (blogPostType) - Dropdownlist
    • Blog Post Author (blogPostAuthor) - Content Picker
    • Image (image) - Media Picker
    • Categories (blogPostCategories) - Tags
    /// <summary>
    /// Creates or updates an existing blog post.
    /// </summary>
    /// <param name="title"></param>
    /// <param name="summary"></param>
    /// <param name="postDate"></param>
    /// <param name="type"></param>
    /// <param name="imageUrl"></param>
    /// <param name="body"></param>
    /// <param name="categories"></param>
    /// <param name="authorId"></param>
    /// <returns></returns>
    private static PublishResult SetPost(string title, 
                                        string summary, 
                                        DateTime postDate, 
                                        string type, 
                                        string imageUrl, 
                                        string body, 
                                        List<string> categories = null, 
                                        string authorId = "")
    {
        PublishResult publishResult = null;
        IContentService contentService = Current.Services.ContentService;
        ISearcher searchIndex = ExamineUtility.GetIndex().GetSearcher();
    
        // Get blog post by it's page title.
        ISearchResult blogPostSearchItem = searchIndex.CreateQuery()
                                        .Field("pageTitle", title.TrimEnd())
                                        .And()
                                        .NodeTypeAlias("blogPost")
                                        .Execute(1)
                                        .FirstOrDefault();
    
        bool existingBlogPost = blogPostSearchItem != null;
    
        // Get the parent section where the new blog post will reside, in this case Blog Index.
        IContent blogIndex = contentService.GetPagedChildren(1099, 0, 1, out _).FirstOrDefault();
    
        if (blogIndex != null)
        {
            IContent blogPostContent;
    
            // If blog post doesn't already exist, then create a new node, otherwise retrieve existing node by ID to update.
            if (!existingBlogPost)
                blogPostContent = contentService.CreateAndSave(title.TrimEnd(), blogIndex.Id, "blogPost");
            else
                blogPostContent = contentService.GetById(int.Parse(blogPostSearchItem.Id));
    
            if (!string.IsNullOrEmpty(title))
                blogPostContent.SetValue("pageTitle", title.TrimEnd());
    
            if (!string.IsNullOrEmpty(summary))
                blogPostContent.SetValue("pageSummary", summary);
    
            if (!string.IsNullOrEmpty(body))
                blogPostContent.SetValue("body", body);
                    
            if (postDate != DateTime.MinValue)
                blogPostContent.SetValue("blogPostDate", postDate);
    
            // Set Dropdownlist field.
            if (!string.IsNullOrEmpty(type))
                blogPostContent.SetValue("blogPostType", JsonConvert.SerializeObject(new[] { type }));
    
            // Set Content-picker field by parsing a "Udi". Reference to an Author page. 
            if (authorId != string.Empty)
                blogPostContent.SetValue("blogPostAuthor", authorId);
    
            // Set Media-picker field.
            if (imageUrl != string.Empty)
            {
                string umbracoMedia = GetUmbracoMedia(imageUrl);
    
                // A stringified JSON object is required to set a Media-picker field.
                if (umbracoMedia != string.Empty)
                    blogPostContent.SetValue("image",  umbracoMedia);
            }    
    
            // Set tags.
            if (categories?.Count > 0)
                blogPostContent.AssignTags("blogPostCategories", categories);
    
            publishResult = contentService.SaveAndPublish(blogPostContent);
        }
    
        return publishResult;
    }
    
    /// <summary>
    /// Gets UDI of an author by fullname.
    /// </summary>
    /// <param name="fullName"></param>
    /// <returns></returns>
    private static string GetAuthorIdByName(string fullName)
    {
        if (!string.IsNullOrEmpty(fullName))
        {
            ISearcher searchIndex = ExamineUtility.GetIndex().GetSearcher();
    
            ISearchResult authorSearchItem = searchIndex.CreateQuery()
                                            .Field("nodeName", fullName)
                                            .And()
                                            .NodeTypeAlias("author")
                                            .Execute(1)
                                            .FirstOrDefault();
    
            if (authorSearchItem != null)
            {
                UmbracoHelper umbracoHelper = Umbraco.Web.Composing.Current.UmbracoHelper;
                return Udi.Create(Constants.UdiEntityType.Document, umbracoHelper.Content(authorSearchItem.Id).Key).ToString();
            }
        }
    
        return string.Empty;
    }
    
    /// <summary>
    /// Gets the umbracoFile of a media item by filename.
    /// </summary>
    /// <param name="fileName"></param>
    /// <returns></returns>
    private static string GetUmbracoMedia(string fileName)
    {
        if (!string.IsNullOrEmpty(fileName))
        {
            ISearcher searchIndex = ExamineUtility.GetIndex("InternalIndex").GetSearcher();
    
            ISearchResult imageSearchItem = searchIndex.CreateQuery()
                                            .Field("umbracoFileSrc", fileName)
                                            .Execute(1)
                                            .FirstOrDefault();
    
            if (imageSearchItem != null)
            {
                List<Dictionary<string, string>> imageData = new List<Dictionary<string, string>> {
                        new Dictionary<string, string>() {
                            { "key", Guid.NewGuid().ToString() },
                            { "mediaKey", imageSearchItem.AllValues["__Key"].FirstOrDefault().ToString() },
                            { "crops", null },
                            { "focalPoint", null }
                    }
                };
    
                return JsonConvert.SerializeObject(imageData);
            }
        }
    
        return string.Empty;
    }
    

    Usage Example - Iterating Through A Dataset

    In this example, I'm iterating through a dataset of posts and parsing the field value to each parameter of the SetPost method.

    ...
    ...
    ...
    SqlDataReader reader = sqlCmd.ExecuteReader();
    
    if (reader.HasRows)
    {
        while (reader.Read())
        {
            SetPost(reader["BlogPostTitle"].ToString(),
                    reader["BlogPostSummary"].ToString(),
                    DateTime.Parse(reader["BlogPostDate"].ToString()),
                    reader["BlogPostType"].ToString(),
                    reader["BlogPostImage"].ToString(),
                    reader["BlogPostBody"].ToString(),
                    new List<string>
                    {
                            "Category 1",
                            "Category 2",
                            "Category 3"
                    },
                    GetAuthorIdByName(reader["BlogAuthorName"].ToString()));
        }
    }
    ...
    ...
    ...
    

    Use of Umbraco Examine Search

    One thing to notice is that when I’m retrieving the parent page to where the new page will reside or checking for a page or media file, Umbraco Examine Search Index is used. I find querying the search index is the most efficient way to return data without consistently hitting the database - ideal for when carrying out a repetitive task like an import.

    In my code samples, I'm using a custom ExamineUtility class to retrieve the search index in a more condensed and tidy manner:

    public class ExamineUtility
    {
        /// <summary>
        /// Get Examine search index.
        /// </summary>
        /// <param name="defaultIndexName"></param>
        /// <returns></returns>
        public static IIndex GetIndex(string defaultIndexName = "ExternalIndex")
        {
            if (!ExamineManager.Instance.TryGetIndex(defaultIndexName, out IIndex index) || !(index is IUmbracoIndex))
                throw new Exception("Examine Search Index could not be found.");
    
            return index;
        }
    }
    

    Conclusion

    Hopefully, the code I have demonstrated in this post will give a clearer idea on how to programmatically work with content pages using a combination of different field types. For further code samples on working with different field types, take a look at the "Built-in Umbraco Property Editors" documentation.

  • After working on all things Hubspot over the last year whether that involved building and configuring site instances to developing API integrations, I have seemed to have missed out on CMS development-related projects. Most recently, I have been involved in an Umbraco site build where pages needed to be dynamically created via an external API.

    Programmatically creating CMS pages is quite a straight-forward job as all one needs to do is:

    • Select the parent node your dynamically created page needs to reside
    • Check the parent exists
    • Create page and set field values
    • Publish

    From past experience when passing values to page fields, it's been simple as passing a single value based on the field type. For example:

    myNewPage.SetValue("pageTitle", "Hello World");
    myNewPage.SetValue("bodyContent", "<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>");
    myNewPage.SetValue("hasExpired", true);
    myNewPage.SetValue("price", 9.99M);
    

    Umbraco has something completely different in mind if you plan on setting the value of type "Dropdown". Simply sending a single value will not work even though it is accepted during runtime. You will need to send a value as a Json array:

    string type = "Permanent";
    myNewPage.SetValue("jobType", JsonConvert.SerializeObject(new[] { type }));
    

    This is approach is required regardless of whether you've set the "Dropdown" field type in Umbraco as single or multiple choice.