Building a Blog with Umbraco
This post was originally written for the AWH Blog. You can read the original post there.
Umbraco is an incredible open-source CMS. It鈥檚 powerful, flexible, and what we use to power the AWH website. One of Umbraco鈥檚 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鈥檒l 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 were 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鈥檚 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鈥檛 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鈥檙e 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
</a>
<h1 class="blog-post__title">
@title
</h1>
<p class="blog-post__date">
@date
</p>
<div class="blog-post__body">
@Html.Raw(body ?? string.Empty)
</div>
</article>
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鈥檛 use the Models Builder feature of Umbraco. I鈥檝e 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"
name="search"
value="@Model.SearchQuery"
class="blog-search__input"
@(string.IsNullOrWhiteSpace(Model.SearchQuery) ? "" : "autofocus") />
<button type="submit" class="blog-search__button">
Search
</button>
</form>
<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
</a>
</li>
}
</ul>
<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">
Previous
</a>
</li>
}
@if (Model.CurrentPage < Model.TotalPages)
{
var url = $"{Model.Url()}?page={Model.CurrentPage + 1}";
<li class="blog-list-pager__item">
<a href="@url">
Next
</a>
</li>
}
</ul>
What鈥檚 happening here is, on the surface, quite simple. We鈥檙e 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鈥檚 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));
else
{
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)
.Take(PAGE_SIZE);
model.IsPaged = true;
model.CurrentPage = currentPage;
model.TotalPages = (int)Math.Ceiling((double)allPosts.Count() / PAGE_SIZE);
model.Posts.AddRange(pagedPosts);
}
return CurrentTemplate(model);
}
}
There鈥檚 a lot happening here and all will be explained.
First, let鈥檚 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鈥檙e using a RenderController
(which is what the BaseController
type inherits from).
This post is already overflowing with code samples so I didn鈥檛 want to add the base controller that we鈥檙e using. You can find that here. The base controller constructor handles retrieving services needed for
RenderController
and exposesPublishedValueFallback
. It鈥檚 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鈥檚 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
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鈥檛 actually slow. Umbraco caches aggressively. As long as you stick to using the standard APIs to access content and don鈥檛 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.
Searching
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
.CreateQuery(IndexTypes.Content)
.ParentId(listPageId)
.And()
.ManagedQuery(searchTerm, _blogSearchFields)
.Execute()
.Select(x => _umbracoHelper.Content(x.Id))
.WhereNotNull()
.ToList();
}
}
A few things to note here:
.ParentId(listPageId)
limits our search to only posts under the current blog. Everything we鈥檝e 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.
- Adding more values to
RSS Feed
The last path is an RSS feed. Ok, technically we鈥檙e providing an Atom feed and not RSS. But clients are typically called RSS readers and basically all readers can ingest both formats, so let鈥檚 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))
.Select(GetSyndicationItem)
.Take(10)
.ToList();
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),
lastUpdatedTime,
items);
var output = new StringBuilder();
await using var writer = XmlWriter.Create(output, new XmlWriterSettings { Async = true });
feed.SaveAsAtom10(writer);
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鈥檝e 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鈥檚 nicely integrated and doesn鈥檛 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 https://awh.net/blog
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鈥檚 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.
Photo by Csabi Elter on Unsplash.