Making Umbraco headless

EDIT: I have uploaded the source code to Github: https://github.com/nickfrederiksen/HeadlessUmbracoTest

Umbraco HQ offers a paid hosted headless implementation of Umbraco called ”Heartcore”. This is good for some, but for others that wants more than Umbraco Heartcore can offer, there is not much help from the Umbraco Core, since Heartcore more or less runs a custom build / highly modified version of Umbraco that we cannot utilize.

So, we are essentially forced to build our own headless implementation on top of Umbraco to get the same functions.

In this post, I am going to share my experience with building a headless API on top of Umbraco and the issues I have encountered.

My implementation will not be compatible with the way Umbraco has chosen to do Heartcore, but it would be interesting if, at some point, the client libraries that is used for Heartcore, could be used for on premise installs as well.

But, as this is just a PoC based upon own experiences on requirements, it will not be compliant or feature complete. At all. But it will work.

I expect you know about route hijacking, composers and components, web api and other rather advanced stuff.

I will upload my code to GitHub. That code will be more complete than this post.

Routing

First things first, we need to setup some custom routes. Since we are, essentially, building a RESTful API, I think the routes should be prefixed with “/api”. This way we distinguish between our API’s and other endpoints.

I have identified at least 4 routes:

  • /api/content/{contentGuid}
  • /api/media/{mediaGuid}
  • /api/sitemap/{parentGuid}
  • /api/dictionary

The “parentGuid” parameter is optional.

These 4 endpoints add a few problems that needs to be resolved:

  • Finding correct content page and variation from guid.
  • Setting the Umbraco context up with the correct content, as it would have been done on a normal request.
  • Routing into custom doctype controllers, as one would do with a normal Umbraco site.

NB: One might argue about using RouteTable.Routes.MapUmbracoRoute and implement an IContentFinder instead of the way shown here. But that is for MVC routes and controllers, and that gave me a lot of headaches…

Finding the correct Umbraco page

Finding content by id has always been easy in Umbraco, prior to v8. The introduction of variations adds an awesome editor experience but for what we are doing, it adds a bit of complexity.

When Umbraco receives a request, it finds a matching domain and its corresponding culture code, (simplified). It then uses that culture code to setup the variation context. This context is then used to select the correct variation whenever a content is requested.

But since we are running on a completely different server, with a completely different domain, we cannot rely on this method. So, we need the client to tell us witch variation is needed. And the only way to do that is with one of two methods: query string og header.

Looking at the Heartcore documentation, I found that they support both:

                Query: ?culture=en-US

                Header: Accept-Language: en-US

That we can work with.

So now we have the content id, and the culture code, now we need to find the content.

As you will see later, this code is going to be reused and we need to call it before the request hits our controller action to setup the Umbraco context before we will be using it. So, we need an ActionFilter attribute.

I have called mine “UmbracoPageFilterAttribute”. This action filter finds a content variation by id and culture code and sets up the Umbraco context. It also returns a 404 Not Found response if no content was found.

In the action filter class, we have to override the OnActionExecutingAsync method. This method is executed just before our action itself. This way, we can setup everything before we need it.

First thing, is to get the controller instance, that is being executed, it is as easy as this:

if (actionContext.ControllerContext.Controller is UmbracoApiController controller)

Notice that I test for the controller being an Umbraco API controller. This is because I need access to the Umbraco context, and this is the simplest way.

The second thing we need to do, is to get the culture code from the request and set the variation context.

var request = actionContext.Request;
var cultureCode = request.GetQueryNameValuePairs().FirstOrDefault(q => q.Key.Equals("culture", StringComparison.InvariantCultureIgnoreCase)).Value;

if (string.IsNullOrWhiteSpace(cultureCode))
{
	cultureCode = request.Headers.AcceptLanguage.FirstOrDefault()?.Value;
}

This code tries to get the culture code, first from the query string, then from the Accept-Language header.

We are then going to use this value to set the VariationContext. And that turns out to be really simple:

controller.UmbracoContext.VariationContextAccessor.VariationContext = new VariationContext(cultureCode);

 Just set the variation context to a new instance with the culture code from the request, and you are golden.

Then we need to get the content guid from the route data and use that to find the content:

string routeContentGuid = actionContext.Request.GetRouteData().Values["contentGuid"] as string;

if (string.IsNullOrWhiteSpace(routeContentGuid) || !Guid.TryParse(routeContentGuid, out var contentGuid))
{
	return null;
}
 
var content = controller.UmbracoContext.Content.GetById(contentGuid);

There it is. The correct page in the correct variation.

When we have our content, we need to verify that content and its cultures. We do not want to return content that does not have the cultures being requested:

if (content == null || !content.Cultures.Any(c => c.Value.Culture.Equals(cultureCode, StringComparison.InvariantCultureIgnoreCase)))
{
	return null;
}

Next up, is setting up the Umbraco Context with this newly found, valid, content. This part is quite simple and heavily “inspired” by the way Umbraco does it.

var router = GlobalConfiguration.Configuration.DependencyResolver.GetService(typeof(IPublishedRouter)) as IPublishedRouter;
 
var contentRequest = router.CreateRequest(controller.UmbracoContext, new Uri(content.Url, UriKind.RelativeOrAbsolute));
 
contentRequest.PublishedContent = content;
 
if (!router.PrepareRequest(contentRequest))
{
	router.UpdateRequestToNotFound(contentRequest);
}

First, we need a reference to Umbraco own IPublishedRouter. We will use that router to create a content request, telling Umbraco that a request has been made to a URL that points to a content page.

We then go on to set the PublishedContent property on that request. This tells Umbraco that the request has found a valid content page.

We then call the “PrepareRequest” method on the router. This is where the magic happens. This method sets up the Umbraco context as if this request were any other normal request to an MVC controller. And if that fails, we escalate the request to be a not found request. I have yet to figure out what would cause it to fail, but Umbraco does it this way, so I figured I should to.

The final step is to tell our controller about this content request, and the content we have found:

controller.UmbracoContext.PublishedRequest = contentRequest;
controller.Umbraco.AssignedContentItem = content;

That is how you find a content page variation and setup the Umbraco Context for custom API’s using an Action Filter.

Content route

Having found our content, we can now move on the next part. Routing.

Umbraco has a “catch all route” that maps all requests, that doesn’t have any other routes, into the “RenderMvcController”. This controller first tries to find a controller by document type, for Route Hijacking, then an action by template. If a controller and action is found, it will execute said action and return the result. Otherwise, it will just return a view result, pointing at a view with the template name, passing in the content as a model.

We will do some of this as well. Only difference here is, we do not want to return the content directly since the serialization of IPublishedContent could become a recursive nightmare. And we do not care about templates. Since we only return json, and do not care about how the frontend wants to present the data.

Let us start by mapping a route:

HttpConfiguration configuration = GlobalConfiguration.Configuration;

configuration.Routes.MapHttpRoute(
	name: "contentPathApi",
	routeTemplate: "api/content/{contentGuid}",
	defaults: new
	{
		action = "Get",
		controller = "ContentApi",
		area = AreaNames.HeadlessUmbraco,
	});

This maps a route to a web api controller called “ContentApi”. I have chosen to add the controller to an area for better isolation, you do not have to do this.

Before building the controller, we need a way to identify all our custom controllers. An easy way is to create an interface, IHeadlessPageController.

The interface is quite simple. At the moment, it has no members but that can change.

Then we need to identify all controllers that implements that interface and extends the UmbracoApiController. This way we can ignore any implementation that does not have the contexts we need.

private static readonly Type PageControllerType = typeof(IHeadlessPageController);
public static readonly Type UmbracoApiControllerType = typeof(UmbracoApiController);

internal static readonly Lazy<Dictionary<string, HttpControllerDescriptor>> ControllerMappings = 
	new Lazy<Dictionary<string, HttpControllerDescriptor>>(
		() =>
		{
			IHttpControllerSelector httpControllerSelector = GlobalConfiguration.Configuration.Services.GetHttpControllerSelector();
			IDictionary<string, HttpControllerDescriptor> controllerMappings = httpControllerSelector.GetControllerMapping();
			return controllerMappings.Where(c => PageControllerType.IsAssignableFrom(c.Value.ControllerType) && UmbracoApiControllerType.IsAssignableFrom(c.Value.ControllerType)).ToDictionary(c => c.Key.ToLower(), c => c.Value);
		},
		true);

A lot is going on here, but it is quite simple. First identify the types, then find all the controllers that is assignable from those types. And we will do it lazily, to only find the controllers when the application is loaded, all controllers have been registered and the controllers are needed.

The way to find the controllers, are quite simple. Just loop through all the registered controllers, filter on type and return a dictionary where the key is the controller name.

The code above, I have chosen to put into a helper class called ControllerHelper. (Again, separation of concern).

Looking at the route, “api/content/{contentGuid}”, registered above, we can see that we need a controller called ContentApiController with an action called Get.

The Get action must first try to find the controller and set the route values so they match what would have been there if the request had been directly to the controller:

private HttpControllerDescriptor GetControllerDescriptor()
{
	var currentPage = this.UmbracoContext.PublishedRequest != null ? this.Umbraco.AssignedContentItem : null; ;
	if (currentPage != null && ControllerHelper.ControllerMappings.Value.TryGetValue(currentPage.ContentType.Alias.ToLower(), out var controllerDescriptor))
	{
		this.ControllerContext.RouteData.Values["action"] = "Index";
		this.ControllerContext.RouteData.Values["controller"] = this.Umbraco.AssignedContentItem.ContentType.Alias;
 
		return controllerDescriptor;
	}
	else
	{
		return null;
	}
}

Nothing much going on, it is mostly null checks. But not line 6-7. We override the route values “action” and “controller”. “Action” is set to “Index”. If we wanted to support templates, as per default Umbraco, we had to test for an action with that name and set the value accordingly. Since we do not want to support templates, we can dictate the action to always be “Index”. I will come back to that later.

The “controller” value is set to the document type alias. This is also the way Umbraco does it.

If we cannot find a matching controller, we will just return not found to the user. Otherwise we will return a 404 not found.

Having found the controller and set the route values, we can now execute the controller and return the result:

var controller = controllerDescriptor.CreateController(this.Request);
var controllerContext = new HttpControllerContext(this.RequestContext, this.Request, controllerDescriptor, controller);
var responseMessage = await controller.ExecuteAsync(controllerContext, cancellationToken);
 
var result = this.ResponseMessage(responseMessage);
 
return result;

This is mostly boilerplate: Create an instance of the controller, setup the controller context, execute the action and return the result.

Page controllers

We now have found the content and built logic to find the controller and execute said controller. Now we must build the page controllers. When doing route hijacking, Umbraco looks for a controller that inherits from “RenderMvcController”. As we are using web api, we cannot use this class so we need to invent our own. I will do this in two steps: HeadlessController and HeadlessPageController.

HeadlessController

This controller is the basis for all headless controllers. Be that page controllers or other types of controllers.

It has a single protected method:

protected IPublishedContent GetCurrentPage()
{
	return this.Umbraco.AssignedContentItem;
}

All this does is returning the content we have found earlier.

HeadlessPageController

This is the base for all page controllers. We are using this base class as a way to ensure we have an Index action that supports HTTP GET requests, and making the developers life a bit easier by setting a current page as the models builder type instead of IPublishedContent:

public abstract class HeadlessPageController<TContent> : HeadlessController, IHeadlessPageController
	where TContent : IPublishedContent
{
	protected TContent CurrentPage => (TContent)this.GetCurrentPage();
 
	[HttpGet]
	public abstract IHttpActionResult Index();
}

Nothing much is going on, but I really like to use the strongly typed models instead of IPublishedContent.

And now we are ready to make our page controllers, using the same formula as we would with a vanilla Umbraco install.

Create a controller named after the document type, make it inherit from the HeadlessPageController and implement the Index method:

public class HomeController : HeadlessPageController<Home>
{
	public override IHttpActionResult Index()
	{
		throw new System.NotImplementedException();
	}
}

Now we need to build a model that represents the home page. I have used the starter pack so my properties will match those.

I think the model for the home page should be simple, for this post. I will add only one property:

public class HomeModel
{
	public string Header { get; internal set; }
}

We can now fill the Index action with this logic:

public override IHttpActionResult Index()
{
	var model = new HomeModel()
	{
		Header = this.CurrentPage.HeroHeader,
	};
 
	return this.Ok(model);
}

If you launch Umbraco, log in and selects the home page. Add a domain, I’ve added test.local and selected en-US as language. Find the page guid and call the url:

HTTP GET /api/content/{pageGuid}

Header: Accept-Language: en-US.

Replace {pageGuid} with the guid copied earlier.

You should then get something that looks like this:

{
	"Header": "Umbraco Demo"
}

That is great, but we lack a lot of information about the page. Information like content type, Id, name and possibly other properties. I have chosen to include Create- and UpdateDate as well.

These values are something we want to add to all page results. So, we add a new model: PageData.

The PageData model

To contain the properties, I start creating an interface called ISimplePageData:

public interface ISimplePageData
{
	string ContentType { get; set; }
	Guid Id { get; set; }
	string Name { get; set; }
	DateTime CreateDate { get; set; }
	DateTime UpdateDate { get; set; }
}

This interface is used to build a method that can be used to map these properties in a reusable fashion. That mapping logic will be placed in the HeadlessController:

protected virtual void MapSimplePageData(ISimplePageData model, IPublishedContent page = null)
{
	if (page == null)
	{
		page = this.GetCurrentPage();
	}
	model.ContentType = page.ContentType.Alias;
	model.CreateDate = page.CreateDate;
	model.UpdateDate = page.UpdateDate;
	model.Name = page.Name;
	model.Id = page.Key;
}

Nothing to it really. We then create the page data model:

public class PageData<TPageModel> : ISimplePageData
{
		public string ContentType { get; set; }
 
		public Guid Id { get; set; }
 
		public string Name { get; set; }
 
		public DateTime CreateDate { get; set; }
 
	public DateTime UpdateDate { get; set; }
 
	public TPageModel Page { get; set; }
}

This class is a generic class, that adds the page model as a property.

To ensure every page model is wrapped with this class, we add a couple of new methods to the HeadlessPageController: One that returns OkNegotiatedContentResult and one that wraps the page model in the new PageData model.

protected OkNegotiatedContentResult<PageData<TPageModel>> PageData<TPageModel>(TPageModel pageData)
{
	PageData<TPageModel> model = this.WrapPageData(pageData);
 
	return this.Ok(model);
}
 
protected virtual PageData<TPageModel> WrapPageData<TPageModel>(TPageModel pageData)
{
	var model = new PageData<TPageModel>()
	{
		Page = pageData,
	};
 
	this.MapSimplePageData(model);
 
	return model;
}

Now we can modify the HomeController to return this.PageData(model); instead of this.Ok(model).

Making the call from above, you would get something that looks a little like this:

{
	"ContentType": "home",
	"Id": "ca4249ed-2b23-4337-b522-63cabe5587d1",
	"Name": "Home",
	"CreateDate": "2020-06-25T10:38:35.947Z",
	"UpdateDate": "2020-06-30T10:49:02.88Z",
	"Page": {
		"Header": "Umbraco Demo"
	}
}

You can add or remove properties as you like.

Final thoughts

I will add my code to GitHub as soon as I have cleaned it up a bit. I have a few issues that I would like input to fix.

One of the issues is CORS. Currently my code forces the developer to add the [HttpOptions] attribute to almost anything. That is quite annoying.

Some of the things I have not covered in this article is how to add non-page controllers like site maps, dictionary, and global data or how to handle media. How to set caching headers and setting up Swagger.

I have not investigated how I would implement the preview function, that now actually works in Umbraco.

This method I have presented here, can be used for more than just Umbraco Content. You can add data from third parties, eg. uCommerce, the same way you would have done in a normal Umbraco site.

And there are thousands of other things I have not even thought about yet. This is merely a proof of concept that might / might not, end up with being a package.