Building a blog in Kentico Cloud - Part 3

Building a blog in Kentico Cloud - Part 3

Friday, December 9, 2016

This third post in my series that explores some of the core capabilities of Kentico Cloud based on short pomodoro-size bursts of effort. This post explores RSS feeds and sitemaps as well as correcting the mime type of media files.

Article Building a blog in Kentico Cloud - Part 3

So this week I was able to find time to do three more things with my blog. Namely that I managed to:

  • Change the content into markdown
  • Add an RSS feed and google sitemap
  • Correct the mime type of images served from the cloud

I wasn't really expecting to get quite so far with so little time to spend; I fully anticipated my lack of MVC knowledge to give me more issues than it did. That being said - I'm sure that there are many better way to achieve what I have here in MVC.

Moving to markdown

Quite early on, I realised that I wanted more control than the rich text field allowed - especially when trying to add code snippets. I had come up with an over-engineered method of including text files and resolving some secret markup on the fly to inject my code. Then I realised that I was drafting up my posts in Trello using markdown and that it is pretty flexible. Using markdown would make my life simpler so I could just use the text field and then parse it inside the web application.

I used Markdown 2.1.0 as my markdown processor and added a simple function in my application to prepare the content as follows:

public static string GetHtmlFromMarkdown(string markdown)
{
    if (String.IsNullOrWhiteSpace(markdown)) 
    {
        return String.Empty;
    }

    Markdown parser = new Markdown();
    string html = parser.Transform(markdown);

    return html;
} 

My reason for doing this is to give me a little more control over the content that I can enter. I don't doubt that the Kentico Cloud team will continue to enhance the platform, but for now, what I need/want to be able to do is add richer content than the current toolbar allows.

Also, I'm considering moving to Markdig, but i prefer the maturity of the package I'm using.

Adding RSS and Google Sitemap

These guys were pretty straight forward from the Kentico Cloud viewpoint. I just needed to go grab all of my content that I wanted in the feed. This gave me a simple RSS controller as follows:

public async Task<ActionResult> Index()
{
    var filter = new List<IFilter>
    {
        new EqualsFilter("system.type", "blog_post"),
        new Order("elements.post_date",OrderDirection.Descending),
        new ElementsFilter("summary", "post_date")
    };

    var posts = await client.GetItemsAsync(filter);
    SyndicationItem[] items = new SyndicationItem[posts.Items.Count];

    for (int i = 0; i < posts.Items.Count; i++)
    {
        items[i] = new SyndicationItem(posts.Items[i].System.Name, posts.Items[i].Elements.summary.value.ToString(), new System.Uri($"http://www.mattnield.co.uk/posts/show/{posts.Items[i].System.Codename}"));
        items[i].Id = posts.Items[i].System.Id;
        items[i].PublishDate = Convert.ToDateTime(posts.Items[i].Elements.post_date.value);
    }

    return new RssResult("Matt Nield", "Matt is the development manager at Ridgeway - an Oxfordshire based digital agency. These are his ramblings as he learns new things. The views in this blog are his own.", items);
}

The RSSResult was a custom FileResult that simply serializes the supplied SyndicationFeed as follows:

public class RssResult : FileResult
{
    private readonly SyndicationFeed _feed;

    public RssResult(SyndicationFeed feed)
        : base("application/rss+xml")
    {
        _feed = feed;
    }

    public RssResult(string title, string description, IEnumerable<SyndicationItem> feedItems)
        : base("application/rss+xml")
    {
        _feed = new SyndicationFeed(title, description, HttpContext.Current.Request.Url) { Items = feedItems };
    }

    protected override void WriteFile(HttpResponseBase response)
    {
        using (XmlWriter writer = XmlWriter.Create(response.OutputStream))
        {
            _feed.GetRss20Formatter().WriteTo(writer);
        }
    }
}

Ta-dah! That's it really; the sitemap works in the same way. What's nice here from the Kentico Cloud view is using the API to filter down to the content that I care about easily

Correcting the media mime types

Before you read this bit - you should know that the Kentico Cloud team have been in touch to say that they are correcting this in a future release

As I mentioned in my last post on the topic, the mime type for all of my media files seems to be application/octetstream. For the act of serving a page to a web browser, this appears to be fine. If you're trying to implement something like twitter cards, then this is more of an issue.

In addition, if you - like me - want to use something like CoudFlare CDN, then you need to serve the images from your own domain. So I want to figure this out anyway. Keep in mind that the entirety of Kentico Cloud sits behind a CDN, meaning that the API and all of the assets are already available through a global CDN to keep response times to a minimum.

All in, this is more about exploring options in MVC than Kentico Cloud at this point. The process behind deliving the image though my domain is fairly simple:

  1. Modify the media/asset URL's when rendering the page
  2. Create a new controller/handler to deal with the module URLs and routing
  3. Create a content result that streams the asset from Kentico Cloud through to a new FileResult with the correct mime type.

The first thing was to create a FileResult, that would take in the image URL and mime type as follows:

public class MediaResult : FileResult
{
    private readonly string _originUrl;

    public MediaResult(string mediaUrl, string mimeType)
        : base(mimeType)
    {
        _originUrl = mediaUrl;
    }

    protected override void WriteFile(HttpResponseBase responseStream)
    {
        WebRequest request = WebRequest.CreateHttp(_originUrl);

        using (WebResponse response = request.GetResponse())
        {
            using (Stream image = response.GetResponseStream())
            {
                byte[] buffer = new byte[4096];
                for (int read = image.Read(buffer, 0, buffer.Length); read > 0; read = image.Read(buffer, 0, buffer.Length))
                {
                    responseStream.OutputStream.Write(buffer, 0, read);
                }
            }
        }
    }
}

Then, I can create a controller to intercept the image URLs:

public class MediaController : Controller
{
    // GET: Media
    [OutputCache(Duration = 3600)]
    public ActionResult Index(string mediaStem)
    {
        string path = mediaStem;
        string ext = System.IO.Path.GetExtension(path);
        string cloudHost = ConfigurationManager.AppSettings["CloudMediaHost"];
        string originUrl = $"{cloudHost}{path}";
        string mimeType = "application/octetstream";
        switch (ext.ToLowerInvariant())
        {
            case ".jpg":
            case ".jpeg":
                mimeType= "image/jpg";
                break;
            case ".gif":
                mimeType = "image/gif";
                break;
            case ".png":
                mimeType = "image/png";
                break;
            case ".svg":
                mimeType = "image/svg+xml";
                break;
        }

        return new MediaResult(originUrl, mimeType);
    }
}

I also needed to add this new route to RouteConfig.cs:

routes.MapRoute(
    name: "Media",
    url: "media/{*mediaStem}",
    defaults: new { mediaStem = @".*\.(css|js|gif|jpg|png)", controller = "Media", action = "Index" }
    );

and also runAllManagedModulesForAllRequests="true" in the web.config.

Finally, just a couple of methods/tweaks to allow the image URLs to be modified before the page is served:

public static string GetHtmlFromMarkdown(string markdown)
{
    if (String.IsNullOrWhiteSpace(markdown)) return String.Empty;

    MarkdownOptions options = new MarkdownOptions();
    Markdown parser = new Markdown();

    string html = parser.Transform(markdown);

    html = html.Replace(ConfigurationManager.AppSettings["CloudMediaHost"], ConfigurationManager.AppSettings["LocalMediaHost"]);

    return html;
}

and

public static string LocalizeMediaUrl(string cloudUrl)
{
    if (String.IsNullOrEmpty(cloudUrl))
    {
        return cloudUrl;
    }

    return cloudUrl.Replace(ConfigurationManager.AppSettings["CloudMediaHost"], ConfigurationManager.AppSettings["LocalMediaHost"]);
}

In hindsight, this might be better as some kind of output filter on the views, but I had 25 minutes to bash this part together - so it'll do for now. I certainly wouldn't call it production ready.

Summary

This week's efforts really were more about a basic tidy-up in order to give me more control of my content and close off some issues that I had found.

What's next?

There are a couple of things on my list to look at next:

  1. Start using module content to create related pages/posts and to links series of posts together.
  2. Pushing content up into Azure Search in order to provide a search capability across the site for blog posts.

Post tags

Kentico Kentico Cloud Web development