Back to Blog

Building a Blog with Umbraco

Andrew Moscardino | August 29th, 2023

Umbraco is an incredible open-source CMS. It’s powerful, flexible, and what we use to power the AWH website. One of Umbraco’s strengths is that you get complete control over the HTML that it renders. The flip side of that approach is that a new Umbraco site is very empty. Setting up something like a blog requires some planning to get right since you have to handle everything yourself.

We recently redesigned and rebuilt our website from the ground up and part of that involved rebuilding the blog. We had a blog before, and it was fine. But this was a chance to really get it right and build it in a scalable and maintainable way. In this post, I’ll show you how to build a blog using Umbraco. The example code is all based on the new blog on the AWH website, though simplified to better show off the concepts.

A Quick Intro to Umbraco

Umbraco is an open-source CMS based on ASP.NET Core. Older versions of Umbraco were built on the older, Windows-only .NET Framework, but modern versions are based on newer .NET and come with all of the flexibility that modern .NET provides.

Umbraco releases new versions multiple times per year. They keep an LTS version that matches the .NET LTS version. At the time of writing Umbraco 10 and .NET 6 are the LTS versions and what this post is using.

In Umbraco, everything starts with the document types. A document type is like a class in C#. It defines the fields that a piece of content will populate with values. A piece of content is an instance of a document type in the same way that an object in C# is an instance of a class.

A template connects to a document type and provides a way to generate HTML given the values in the content. Templates in Umbraco are Razor views like you would find in any ASP.NET Core application.

There’s lots more to Umbraco, but that is not what this post is trying to cover. The Umbraco docs are very thorough and an invaluable tool for building Umbraco sites. If you want to learn Umbraco, start there.

The Basic Document Types

For this example, we will only need two document types: Blog Post and Blog List. It may seem counter-intuitive, but creating the Post document type first can make this process a little easier.

The Post document type should be created with a matching template. For this example, we will only need a few fields:

  • Title (data type: Text String)
  • Date (data type: Date Picker)
  • Body (data type: Richtext Editor)

For the List type, it should also be created with a matching template and should allow the Post page as a child node. Enabling List View on the page is also a good idea, though that only affects the Umbraco backoffice. For this example, we don’t actually need any extra fields on the List type.

You will probably want more fields on both types depending on the design you want to implement. For this example, we’re keeping it simple.

How you handle templates and layouts is something I skipped over. This is a complex subject and one that mostly comes down to preference. I like to use a Master document type for properties needed by every page, though a composition works just as well. I also like to create a master template that can be used as the layout for all pages.

The Post Page Template

The Post template is very simple. All it needs to do is pull the values from the content and render some basic HTML.

@inherits UmbracoViewPage
    Layout = "Layout.cshtml";

    var parentPage = Model.Parent!;
    var backToListUrl = parentPage.Url() + Context.Request.QueryString;
    var title = Model.Value<string>("title", fallback: Fallback.ToDefaultValue, defaultValue: Model.Name);
    var body = Model.Value<string>("body");
    var date = Model.GetDateTime("date").ToFriendlyString();

<article class="blog-post">
    <a href="@backToListUrl" class="blog-post__list-link">
        Back to List

    <h1 class="blog-post__title">

    <p class="blog-post__date">

    <div class="blog-post__body">
        @Html.Raw(body ?? string.Empty)

The strangest thing that this template does is pull the query string for the current page to use as the query string for the link to the list page. This allows the list page to pass query string values when linking to posts, and for the post to send those values back to the list, thus keeping a simple form of state. For instance, this can maintain the pagination or search values when clicking into and out of posts.

I don’t use the Models Builder feature of Umbraco. I’ve had bad experiences with Models Builder in the past so I prefer to pull values from content manually. This is entirely my preference and I probably should give ModelsBuilder another try.

The List Page Template

The template for the List page is a bit more complex and requires some explaining:

@using BlogSample.Models
@inherits UmbracoViewPage<BlogListModel>
    Layout = "Layout.cshtml";

    var feedUrl = Model.Url(mode: UrlMode.Absolute) + "?feed=true";

@section meta {
    <link rel="alternate" type="application/atom+xml" href="@feedUrl" title="AWH Blog" />

<h1 class="page-title">Blog</h1>

<form method="get" class="blog-search">
    <input type="text"
           @(string.IsNullOrWhiteSpace(Model.SearchQuery) ? "" : "autofocus") />

    <button type="submit" class="blog-search__button">

<ul class="blog-list">
    @foreach (var post in Model.Posts)
        var title = post.Value<string>("title", fallback: Fallback.ToDefaultValue, defaultValue: post.Name);
        var date = post.GetDateTime("date").ToFriendlyString();
        var url = post.Url() + Context.Request.QueryString;

        <li class="blog-list-item">
            <a href="@url" class="blog-list-item__link">
                @title - @date

<ul class="blog-list-pager">
    @if (Model.CurrentPage > 1)
        var url = $"{Model.Url()}?page={Model.CurrentPage - 1}";

        <li class="blog-list-pager__item">
            <a href="@url">

    @if (Model.CurrentPage < Model.TotalPages)
        var url = $"{Model.Url()}?page={Model.CurrentPage + 1}";

        <li class="blog-list-pager__item">
            <a href="@url">

What’s happening here is, on the surface, quite simple. We’re rendering a list of posts. Above that is a search form without an action attribute which will cause it to reload the current page with a new query string value. Below the list is a simple pagination control with links for previous and next pages. Near the top we have a <link> tag for an RSS feed.

But the weirdest part is @inherits UmbracoViewPage<BlogListModel>. Where did BlogListModel come from? How did it get populated?

Before we move on, the @section meta {} block corresponds to @await RenderSectionAsync("meta", false) in the layout view. Using sections allows our views to write to different places in the layout view aside from the main @RenderBody() call.

The List Page Model

In Umbraco, your views should always use an @inherits declaration instead of @model and they should inherit from UmbracoViewPage. But UmbracoViewPage comes in two varieties: the non-generic UmbracoViewPage and the generic UmbracoViewPage<T>. The non-generic version is the generic version with IPublishedContent provided as T. Since our layout template uses the non-generic version, we need our generic version to use a type that is compatible with IPublishedContent.

Enter PublishedContentWrapped. Umbraco provides a class you can inherit from to make a model that is interchangeable with IPublishedContent. So we can create a class that inherits from PublishedContentWrapped and use that as our model. When the model gets passed to the layout template, the extra properties are ignored and the template gets a working IPublishedContent instance.

using Umbraco.Cms.Core.Models.PublishedContent;

namespace BlogSample.Models;

public class BlogListModel : PublishedContentWrapped
    public List<IPublishedContent> Posts { get; } = new();
    public string? SearchQuery { get; set; }
    public bool IsPaged { get; set; }
    public int CurrentPage { get; set; }
    public int TotalPages { get; set; }

    public BlogListModel(IPublishedContent content, IPublishedValueFallback publishedValueFallback)
        : base(content, publishedValueFallback) { }

Essentially, this is an easy way to create a derived class without having to implement all of the complexities of IPublishedContent.

We use this class to hold our list of posts to display as well as some properties indicating the page state.

The List Page Controller

Ok, so now we have a model and that model can be used in our view and is compatible with the layout view. But how does that model get populated? Does Umbraco hydrate it for us? No, we intercept the rendering of the page with a custom controller and create the model ourselves!

Here’s the controller:

using BlogSample.Models;
using BlogSample.Services;
using Microsoft.AspNetCore.Mvc;

namespace BlogSample.Controllers;

public class BlogListController : BaseController
    private const int PAGE_SIZE = 6;

    private readonly FeedService _feedService;
    private readonly SearchService _searchService;

    public BlogListController(IServiceProvider serviceProvider, FeedService feedService, SearchService searchService)
        : base(serviceProvider)
        _feedService = feedService;
        _searchService = searchService;

    public async Task<IActionResult> BlogList(int page = 1, string? search = null, bool feed = false)
        if (feed)
            var feedContent = await _feedService.GetBlogFeedAsync(CurrentPage!);
            return Content(feedContent, "text/xml");

        // Search and normal paging share a model and return value
        var model = new BlogListModel(CurrentPage!, PublishedValueFallback) { SearchQuery = search };

        if (!string.IsNullOrWhiteSpace(model.SearchQuery))
            model.Posts.AddRange(_searchService.SearchBlogPosts(CurrentPage!.Id, model.SearchQuery));
            var currentPage = Math.Max(page, 1);
            var allPosts = CurrentPage!.ChildrenOrEmpty();
            var pagedPosts = allPosts
                .OrderByDescending(x => x.GetNullableDateTime("date") ?? DateTime.MinValue)
                .ThenBy(x => x.SortOrder)
                .Skip((currentPage - 1) * PAGE_SIZE)

            model.IsPaged = true;
            model.CurrentPage = currentPage;
            model.TotalPages = (int)Math.Ceiling((double)allPosts.Count() / PAGE_SIZE);

        return CurrentTemplate(model);

There’s a lot happening here and all will be explained.

First, let’s talk controllers. Umbraco is built on ASP.NET Core MVC so controllers are powering things behind the scenes. The Umbraco Docs have a good explanation of the different controller types that you can use. For our example, we’re using a RenderController (which is what the BaseController type inherits from).

This post is already overflowing with code samples so I didn’t want to add the base controller that we’re using. You can find that here. The base controller constructor handles retrieving services needed for RenderController and exposes PublishedValueFallback. It’s extremely helpful to have that base class, especially on projects with many controllers. This strategy also works well for surface controllers.

Render controllers work by intercepting the page rendering. They match the document type alias to the controller name and the template name to the action name. If you name your controller and action to match, you can create a custom model and inject it into the template by using the CurrentTemplate() method provided by RenderController. This is an extremely useful pattern for extracting logic from your views. It’s also the best way to integrate data from external services. This post only scratches the surface of what can be done with controllers in Umbraco.

In our controller, there are three different paths based on the parameters provided. Like any other MVC controller, parameters are populated using the default model binder, so they come from the URL query string.


Pagination is the default path. If no parameters are provided, we return the first page of posts. The code is all contained in the controller and is quite simple. The .ChildrenOrEmpty() method is a custom extension on IPublishedContent that provides a fallback when .Children() is null.

Something important to note is that enumerating all of the children of the list page (which is every blog post) on every load isn’t actually slow. Umbraco caches aggressively. As long as you stick to using the standard APIs to access content and don’t retrieve property values until they are needed, the site will be fast.

Stay away from ContentService and similar as they are much slower. They do have their uses, though not for something like a blog.


The next path is searching, which is similar to pagination in that the same model and return value are used. The difference is how the list of posts is populated.

Searching uses Examine, which is included in Umbraco and provides a wrapper around Lucene.Net. Umbraco includes an index by default that includes all public content that we can easily search. In order to simplify this, the code is extracted into a service which is registered with the default .NET DI container and injected into the controller:

using Examine;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Web.Common;
using Umbraco.Cms.Infrastructure.Examine;

namespace BlogSample.Services;

public class SearchService
    private static readonly string[] _blogSearchFields = new[]
        UmbracoExamineFieldNames.NodeNameFieldName, "title", "body"

    private readonly UmbracoHelper _umbracoHelper;
    private readonly IExamineManager _examineManager;

    public SearchService(UmbracoHelper umbracoHelper, IExamineManager examineManager)
        _umbracoHelper = umbracoHelper;
        _examineManager = examineManager;

    public List<IPublishedContent> SearchBlogPosts(int listPageId, string searchTerm)
        var index = _examineManager.TryGetIndex(Constants.UmbracoIndexes.ExternalIndexName, out var i) ? i : null;

        if (index == null)
            return new List<IPublishedContent>();

        return index.Searcher
            .ManagedQuery(searchTerm, _blogSearchFields)
            .Select(x => _umbracoHelper.Content(x.Id))

A few things to note here:

  • .ParentId(listPageId) limits our search to only posts under the current blog. Everything we’ve done so far has allowed for multiple, independent blogs to be created on the site and this is an important part of that goal.
  • .ManagedQuery(searchTerm, _blogSearchFields) is very important as providing the fields to search prevents us from searching hidden fields that only Umbraco needs.
    • Adding more values to _blogSearchFields will improve your search as long as those fields contain useful values. You can even add fields to your post document type that are only used for searching.
RSS Feed

The last path is an RSS feed. Ok, technically we’re providing an Atom feed and not RSS. But clients are typically called RSS readers and basically all readers can ingest both formats, so let’s just call it a feed.

This path is different from the first two in that it bypasses the template and returns raw XML. The code for the feed path, like search, is complex so the code was extracted out to a DI-compatible service:

using System.ServiceModel.Syndication;
using System.Text;
using System.Xml;
using Umbraco.Cms.Core.Models.PublishedContent;

namespace BlogSample.Services;

public class FeedService
    public async Task<string> GetBlogFeedAsync(IPublishedContent blog)
        var items = blog.ChildrenOrEmpty()
            .OrderByDescending(c => c.GetNullableDateTime("date").GetValueOrDefault(DateTime.MinValue))

        var lastUpdatedTime = items.Select(x => x.PublishDate).DefaultIfEmpty().Max();
        var feed = new SyndicationFeed(
            "Blog Title",
            "A description of this awesome blog",
            new Uri(blog.Url(mode: UrlMode.Absolute)),
            blog.Url(mode: UrlMode.Absolute),

        var output = new StringBuilder();
        await using var writer = XmlWriter.Create(output, new XmlWriterSettings { Async = true });
        await writer.FlushAsync();
        return output.ToString();

    private static SyndicationItem GetSyndicationItem(IPublishedContent post)
        var title = post.Value("title", fallback: Fallback.ToDefaultValue, defaultValue: post.Name);
        var body = post.Value<string>("body");

        var item = new SyndicationItem
            Id = post.Key.ToString(),
            Title = new TextSyndicationContent(title),
            Summary = new TextSyndicationContent(body?.StripHtml().Truncate(200), TextSyndicationContentKind.Plaintext),
            Content = new TextSyndicationContent(body?.StripHtml().Truncate(200), TextSyndicationContentKind.Plaintext),
            PublishDate = post.GetNullableDateTime("date").GetValueOrDefault(DateTime.MinValue)

        item.Links.Add(new SyndicationLink(new Uri(post.Url(mode: UrlMode.Absolute))));

        return item;

This service relies on the System.ServiceModel.Syndication package to simplify creation of the feed and handle serialization.

There are several ways to expose a feed. I’ve used a surface controller on other projects, and an older version of the AWH website used a separate RSS page. I like this approach of using the render controller and a query string parameter because it’s nicely integrated and doesn’t expose any behind-the-scenes stuff in the URL.

The most important part of the feed, though, is linking to it from the list page. Providing that <link> tag with rel="alternate" allows you to enter into your RSS reader and it can automatically find the feed!



Blogs are an important part of any website. They give you a place to write without being dependent on yet another third-party platform. Even if you cross-post somewhere like Medium, it’s good to have somewhere that you fully control.

Umbraco is powerful and can be intimidating, especially when you are looking at a new, empty project. But with only a couple of document types and some simple code, you can build a feature-complete blog.