Headless - Not Hovedløst
Kaspar Boel KjeldsenUmbraco has many virtues; I’ll be the first to sing them. And while the Content Delivery API is a great enabler for headless implementations, there are pitfalls and gotchas to be aware of — as well as (in my opinion) features that could be more fleshed out to better support headless development.
Now that I’ve come out swinging, allow me to soften the blow: this isn’t all Umbraco’s fault. Going headless by definition means going custom, and how do you generalize the ability to customize something that is inherently unknown?
Still, there are places in the core CMS, and even in Umbraco’s paid products, where the implementation feels like it hasn’t quite been followed all the way to the doorstep.
Which brings me back to the title. It’s mostly meant in jest, a soft jab at Umbraco (and all of us implementing headless on top of it), but as a Dane I’ve always found the concept of headless funny. We have the same word (hovedløs), but you’d never use it in this context... although perhaps you should?

To go headless into something in Danish means to charge ahead without thinking through the consequences. It means going forward without a solid plan. Put plainly: you didn’t think before you acted.
And if I’m being honest, that description fits most headless projects I’ve been part of. “Headless got added to the contract last minute with no extra time.” “It’s headless because the client wanted it.” Cue the “This could have been an MVC” version of “This could have been an email.”
More often than not, those projects are built on assumptions or shortcuts that lead to cleanup jobs, performance issues, or just plain janky sites. Headless is hard, especially the first two or three times you do it. And if your goal is reusability, you’d better have a pipeline of similar projects and a steady team in place.
Perhaps we are all doing headless, hovedløst.

I’m not claiming to have all the answers or to be a headless expert (unless we’re using the Danish definition). But I do know a bad implementation when I see one, and it makes me think we should be talking about how to do headless with purpose, instead of doing it hovedløst. We need to put the brain somewhere when the head is gone.
Before I get too ranty, I should say this: I have the deepest respect for the Umbraco developers and the community, and I am not calling Umbraco a bad implementation, quite the contrary. My criticism comes from a place of wanting Umbraco to succeed everywhere, also headlessly.

Headless gripe number 1: Performance
Note to reader: I’m assuming you’re not creating a completely static site. If you are, this isn’t your problem.
Most tutorials for headless CMS setups, especially for Umbraco, will have you hooking up Next, Nuxt, Astro or something similar to the Content Delivery API. They might even show you how to auto-generate your models and clients through Swagger using tools like openapi-typescript-codegen, heyapi, or Orval. Then they’ll spin up a few components, hit “run,” and call it a day.
That’s all fine for a small website. But if you’re building a small website, why go headless? You’ve just doubled the number of servers and endpoints you need to manage. And if that’s not your case, you’ve now ensured that every single visit to your site hits the Content Delivery API, whether there’s new content or not. That gets expensive fast in terms of performance.
You can also create some pretty heavy queries against the API, and heavy or not, a fetch will always be slower than something that’s already there.

The obvious answer is caching. Which brings me to my first gripe with Umbraco in a headless context.
Let’s say you want to cache your site’s queries—or even the entire page, I’m not your boss—based on the path. A very normal pattern. So how does Umbraco help with that?
It doesn’t really. Sure, there are webhooks, and you could make an API on your backend to receive a ping from Umbraco and react to it. But the content you’re sent only tells you something about the page where the change happened. That might seem useful, but let’s say your page is a news article that has a writer attached. You go and change the writer’s email or photo on another content node. Your news article won’t update, because Umbraco only notifies you about the writer node, not the related article.
Many teams end up invalidating everything on publish, but if your site has thousands of pages and a steady stream of visitors, that’s a lot of unnecessary cache churn just because someone fixed a comma somewhere.
My own solution to this problem is my package Cache Keys. It won’t solve everything for everyone, but it adds a set of cache “keys” to every response from the Content Delivery API, giving the returned object an added layer of context: not just “I am this,” but also “I’m related to these other things.”
That lets you define caching logic like: “This page is cached with its own key, plus the key of the writer, and the key of every piece of media on the page.” On publish, Umbraco can then send a cache invalidation request to your endpoint with all the related keys of the content.

Since I’m using Nuxt, I handle the frontend side of caching with Nuxt Multi Cache, but similar solutions exist for Next and other frameworks. I’ve also successfully used it for targeted cache purges in Azure Front Door.
The Content Delivery API should ideally include related-content context out of the box. Caching isn’t an edge case; it’s a fundamental part of running a headless implementation well.

Headless gripe number 2: Security
Did you know that Umbraco’s Content Delivery API is public by default when you set it up? Or that you can only set one API key?
What if I have ten different “heads” I want to serve content to? Do I just hand out the same key to all of them?
As it stands, yes — that’s exactly what you do.
In a world where we already have API users in Umbraco for the Management API, it’s odd that the Delivery API is still public by default and limited to a single shared key. The assumption seems to be that we completely trust everyone asking for our content. Not only that, but that they should have access to all content across all sites in the solution.
As with most things in Umbraco, if you don’t like it, you can rip it out and replace it with your own. And that’s exactly what I recommend if you need to share a key with someone who isn’t you.
What I haven’t shown here is a system for scoping API key access — something I haven’t had to implement yet, but which really should be part of Umbraco itself. Why should a key automatically grant access to every single piece of content on every domain?
The default setting for the Content Delivery API should not be public. At the very least, it should only be public if the request origin matches the same domain (for SPA scenarios, for example).
Also, “Api-Key” should really be “Api-Keys”, supporting multiple keys. Even better would be to integrate with Umbraco’s existing API users, letting us create Delivery API users the same way we do for the Management API. That way, we could scope access through member groups and define exactly which content each key can access.

Headless gripe number 3: Incompleteness
Translations
We have a Content Delivery API, and we have a Media Delivery API (if you enable it). But where are my translation items?
Umbraco has an entire section dedicated to managing multi-lingual content, yet there’s no built-in way to access dictionary items in a headless setup.
In MVC, you’d simply call @Umbraco.GetDictionaryValue, but in a headless site that entire part of Umbraco might as well be dead, unless you build your own implementation.
My solution usually involves setting up an API controller that hands over either all translation items, or every translation item with key starting with a parameter.
Authorization
Authorization in headless Umbraco is another area that feels half-done.
If you enable the Authorization Code Flow in the Delivery API and set your login and logout URLs, you’d think you wouldn’t have to repeat them on every single page you want to protect. But as of my last check, you still do. You basically have to enter a dummy value, because in a headless setup you probably can’t even link directly to your login page.
I don’t like it, so I usually replace Umbraco’s implementation of IRequestMemberAccessService with my own. That lets us handle access control in code through something like:
public Task<PublicAccessStatus> MemberHasAccessToAsync(IPublishedContent content)
The benefit is that you can build a composition your editors can use to allow or deny access to specific content nodes, for everyone or specific groups, without making them fill in login and logout URLs for each page. It also gives you the flexibility to say: “every page of this content type is protected like this.”
Preview
Preview is improving in Umbraco 17, so I won’t lean too hard on it here. But before 17, if you wanted the “Save and Preview” button to actually take your editor to a working preview of their content—without adding “More preview URLs”—you basically had to take over Umbraco’s preview endpoint yourself.
That’s what I ended up doing in my package kraftvaerk.umbraco.headless.preview.

Headless gripe number 4: Friendliness
This one applies to MVC solutions too. I might be influenced by the projects I’ve seen, and maybe everyone else is already doing this, but I almost never see proper previews for blocks.
Editors deserve to see a visual representation of the content they’re creating. Yet nine out of ten Umbraco sites I come across — MVC or otherwise — still leave editors staring at the same boring white blocks and call it “good enough.” It’s not.
Rick Butterfield’s Block Preview package is fantastic for MVC (and my version, Instant Block Preview, is alright too), but I’ve yet to see anyone implement proper block previews in the backoffice of a headless solution, except me.
And we should! Yes, you chose headless, and yes, it’s complicated. But it’s not the editor’s fault, so fix it(❤️).
I built Kraftvaerk.Umbraco.Headless.BlockPreview to solve exactly this. It’s used on this site and several other newer Umbraco projects, and you can see it in action here:

Headless gripe number 5: The products
This one could easily be its own post. The Engage part (and how I’m using it on this site headlessly) probably will be.
For now, let’s just say I disagree with selling a “headless” version of a product at the same price as the full one, and then including a list of unsupported features like this (from the Umbraco Engage headless docs):
Time on page tracking
Video tracking (workaround: use the Events API)
Form tracking
Search term tracking (workaround: use the Events API)
The bridging library for Google Analytics
The bridging library for Google Tag Manager
Google Analytics blocker detection
Heatmap
The Umbraco Engage Cockpit
To my mind, there isn’t a single item on that list that can’t be supported in a headless setup — at least not at an API level.
This site uses Umbraco Engage headlessly and still does time-on-page tracking and heatmaps. Look in your your dev-tools console, if you're curious. I'm being pretty explicit about what I'm doing there.
And before I move on, credit where it’s due: a big thank you to Umbraco for providing my free MVP license of Umbraco Engage. It’s generous, and it’s also what makes it possible for me to dig into it this deeply; hopefully in a way that helps improve the product for everyone.

Rounding Up
Everyone should have a good time doing headless, but let’s not do it hovedløst. Let’s think. Let’s carefully craft experiences that, for editors, developers, and visitors alike, match or exceed what’s possible using the standard MVC approach.
And if we’re building CMSs or products for CMSs, let’s commit fully when we say we support headless. Go all the way, or don’t.
Let’s not just charge in hovedløst, without a plan or a thought for what it means in practice. Sometimes, the right move is not to go headless.
I’ll be following up this article with smaller, tutorial-style posts on how I’ve solved some of the issues I’ve mentioned here. As always, this site is open source and available to explore. But fair warning: I said earlier I know a bad implementation when I see one — and since this is mostly a spare-time project, it’s definitely flirting with that line. Take it as inspiration, not gospel.
Let’s make headless Umbraco thoughtful, not hovedløst.
