How significant is technical debt on your current web project, and how much stress does it cause? Can a composable approach to websites help us avoid the most common issues and keep developers out of maintenance hell?
Ondrej PolesnyPublished on Aug 2, 2023
In this article, I’ll first explain the difference between headless and traditional CMS. Then, I’ll touch on composable architecture and digital maturity before listing the most typical causes of technical debt on modern website projects. For each of them, I’ll share the lessons learned and tips for avoiding them in the future.
Headless CMS is not a CMS in cloud
Let’s start with the basics. Headless CMS and traditional CMS have only one thing in common - they are used to manage content. Headless CMS is not a facelift of a traditional CMS; it’s not a new version, it’s not an upgrade. It serves a different purpose.
Why a different purpose? We’re still building websites, after all, aren’t we?
Correct. But is a website really what your client needs? These days, we’re heavily investing in SEO, machine-readable content, generative AI, online marketing, external commerce platforms, social networks, and other channels to make sure our content is where our clients are looking for it. How many times has Google given you an answer without you having to visit a 3rd party website?
We are making a huge effort to make our websites as machine-readable as possible. When we are building websites, we’re actually also integrating Google, Facebook, Instagram, Amazon, Twitter, LinkedIn, ERPs, PIMs, DAMs, and the list goes on. And even that is all before our client decides to explore mobile apps, smartwatch apps, voice assistants, AI, and so on.
And here comes the difference. Headless CMS is designed from the ground up to help us structure content. That content we need on all those mentioned channels. For headless CMS, as my friend Mike Wills would say, content is king.
On the other hand, traditional CMS is designed to build websites. They typically provide a lot of freedom to content editors as they want to enable the best website-building experience.
So, what’s more important to your client, content, or website? The answer is never easy and heavily depends on the client’s digital maturity. But choosing the wrong approach from the start inevitably creates a huge (not only) technical debt in the future.
Why do we let technical debt appear?
For the rest of this article, I’ll assume your client is ready for headless and wants to use a composable approach for their front end(s). Let’s list the most common approaches that lead to growing technical debt:
- Underestimating content modeling
- Underestimating maintenance
- Underestimating microservices’ development speed
- Handling problems code-first
- Sticking to website defaults
I’ll explain each in greater detail.
1. Underestimating content modeling
Content modeling is not new among CMSs, but it really got critical importance when we put content at the center of everything. Let’s start with developers:
Content model is often created and maintained by developers
When you sign up for a headless CMS, a content model is the first thing you need to create. And it’s usually developers or other tech personas who do that step. The problem is—they always project their technical skills into the content model. I’ll illustrate with examples.
Our own website at Kontent.ai was originally created by a front-end developer who wanted full flexibility for editors and kept creating a new content type for every new page. This approach allowed him to make every aspect of each page as dynamic as needed but also created a tight coupling between the website and the CMS. A developer was always needed to create new pages, and the content was not structured, thus not reusable, which led to a lot of duplicates. This is a very common architecture for people used to traditional CMSs and structures.
Over time as the project grew, the situation became unbearable and led to an expensive remodeling of half of the landing pages. To this day, we still have a few content type-website page couples that we’re working on eliminating.
Lessons learned: The content model should reflect real-life objects, not pages. Do you list events on your website? Model the event, not the event page.
It’s likely that your front end will need exceptions from that rule, or you at least need to allow some of your editors to compose landing pages. In those cases, create special content types for visual components that don’t contain data but only visual elements and link the structured content items. That way, you don’t lose reusability and consistency among content.
Note: On this screenshot, the heading is purely a visual element. It’s intended for website use only and does not represent any reusable value. On the other hand, people are real-life entities that must be reusable across multiple channels.
Another example came from Mike Wills, the VP of Technology at BlueModus, who was tasked with creating a content model for his Fortune 500 client. Being a developer, he wanted to keep every bit of content reusable and broke the content types into pieces that made sense for his code. He kept testing only small widgets and components and didn’t find out until later that the complicated structure led to an appalling editor experience. Editors had to create content items for each page, tab, and other little pieces of their website.
“Don’t let your developers forget that the most beautiful code is pointless if it creates a bad experience for users.”
His team found out that, in reality, a lot of content is single-purpose, and insisting on the ability to reuse everything every time didn’t work. The key assets for his client’s company were the authors, so the priority shifted to them feeling comfortable when working with content.
Lessons learned: Prioritize author usability. We developers love elegant code, but the truth is the code must serve the project’s purpose. Don’t sacrifice editors’ experience to make code simpler.
Note: The full article “The Only Way to Guarantee a Perfect Content Model” by Mike Wills was published by Content Marketing Institute here.
It’s near impossible to get the content model right the first time
We, technical people, tend to look at the content model as if it was a database. Naturally, we try to model it right the first time as we believe that in case of changes, the later we introduce them, the more expensive they’ll be.
That’s not true. Content model is a living mechanism and the first draft is only as good as your client’s ability to explain the business problem and your ability to understand it. So usually not very good. And let’s not forget that clients’ requirements are always subject to change.
Lessons learned: Try not to overthink the first draft of the content model. You will need to adjust it later, anyway. Start small and keep building it iteratively whilst checking the editor and developer experience. If possible, use tools that let you manage the content model from code (CLI).
The line between reusability and editor experience is different for every project
Earlier, I mentioned that composable architecture will only work in case your client is digitally mature enough. But not all of them are, so headless CMS vendors are trying to implement features like visual website editing that keep the experience of managing content similar to traditional CMSs. But they come at a cost—you must adjust the content model to allow for visual and unstructured data.
Lessons learned: Work with your client to improve their digital maturity. It’s the best investment. The more sacrifices you make in content modeling, the more maintenance and tech debt you’re creating for the future.
Now, look at your own organization and consider how digitally mature you are. Many digital agencies that work with headless CMSs don’t have a high level of experience with content modeling, yet clients fully rely on them with their future (not an overstatement) and budget. Investing time and effort into content modeling will eliminate a huge amount of technical debt.
2. Underestimating maintenance
The project is finished, celebrations started, and it’ll work on its own for a few years now, right? We may find this funny, but the truth is this is arguably the most common problem of all projects.
“With every project, we estimate the amount of maintenance to 20-30% of the initial cost per year.”
Every project requires maintenance, and neglecting it inevitably leads to technical debt. The composable architecture partly removes the burden of maintaining the backend parts. That’s a huge benefit, mainly in terms of security which is a growing concern these days. You still need to maintain the content model, though, and some effort will need to go into the front-end channels that developers must implement on their own.
This graph illustrates what we found out when working on our own project a couple of years ago. The focus on adding new features and pushing problems to the future actually locked us in a state when bug fixes and operational agenda exhausted all development resources. This is also the point when your client is fed up with funding a project that doesn’t bring any new features and starts looking for another CMS, agency, or both.
Lessons learned: Insist on good code quality (and tests) and look at new features from the perspective of a whole project. Composable architecture brings many benefits but also requires more experience. Forbid yourself to say, “We’ll fix it later.”
3. Underestimating microservices’ development speed
How many tools do you have in your project stack? And how often do vendors of those tools release new features?
The releases are frequent. They typically have agile processes and short development cycles, and it sometimes feels likes npm packages. Every other day, there’s an upgrade available. The problem is - end clients can hardly use them. The features either need to be implemented on the front end side, or they require structural changes of the content model or other configuration of the CMS project, or people simply need to be trained to understand how to use them.
When none of that happens, it’s only a matter of time until people who work on the project start feeling frustrated that other systems have features that would help them streamline their work and their stack is no longer good enough. At that point, it’s too late to start implementing the new features and training people as they are already convinced that the grass is greener on the other side and that they need to start over.
Lessons learned: Be attentive to people who work on the project on a daily basis. Find out what their struggles are and optimize the project by implementing newly released features of used tools.
4. Handling problems code-first
We, developers, are very good and quick at solving problems in code. The problem is, if you’re fixing a broken content model or mistakes in content management this way, you’re creating technical debt, and you’re doing it for all relevant front-end channels. I’ll explain using a few examples.
Duplicated logic in the codebase
The first one happened when we added AB testing. Each page that would be subject to the AB test was duplicated, and editors would prepare the content of the B variant. Other parameters of each test were added to the front-end implementation, and the test was launched. This process was repeated every time the team wanted to add a new AB test, so we ended up having a lot of extra pages with duplicated AB testing logic in the codebase.
Lessons learned: When there’s a potential a functionality may scale, find a sustainable solution. In this case, we created a special content type that held all AB test configuration data, including the content of the B variant, and allowed editors to AB test any page without the need for a developer.
Note: The full article about AB testing a Next.js site is published here.
Constantly working around the same problem
Our website is built with Next.js, and the framework needs serialized data to render (hydrate) each pre-generated page. On some content-heavy pages where we needed to fetch a lot of data, we were experiencing circular references in the pre-generated objects. At the time, we aimed to fix the problem by manually excluding problematic references in the code, but the problem kept resurfacing every now and then.
Lessons learned: Every workaround has an expiration date. We ended up preparing a migration script and adjusting the content model to avoid the circular reference—which was the problem in the first place.
Adding new code instead of reusing existing components
The third example is about extending components. As I described above in the section about content model challenges, in the past, we converted many landing pages from the 1-1 couples with content types to a common template using a single content type and linked visual components. Over time as we were adding all the needed components for the landing pages, we found out that the majority of components were duplicated in the codebase. They only had different markups as they were used in different contexts.
Lessons learned: Build a habit of resistance against adding new code. It makes it easier to convince yourself to check for functional/markup duplicates.
Adding redundant logic
The last example looks very innocent. A component that we used on many places throughout the website had the label “Learn more.” We needed that label to be configurable from the CMS, so we added a text box element to the respective component content type and, in the code, added a condition that whenever the label was empty, it should default to “Learn more.” This custom logic looks like a good solution to avoid writing migration scripts and retrospectively filling out all relevant content items. However, these “workarounds” become a nightmare over time, and you don’t even see it coming.
Lessons learned: Don’t implement functionality that the CMS can handle. Try to add as little logic as possible to your code.
5. Sticking to website defaults
And finally, yes, we’re mostly building websites, but the content is not website-exclusive. Many of the examples above are from the same bucket, so here I’ll list the solutions specific to linking and URLs that will give you headaches over time.
Handling URLs in a web-centric way
Did you ever have to fix a broken link on your website? Let me rephrase that: did you have to fix a broken link on your website this week? Or today?
Take a look at one of the historical components on our website and how it handles the URL target:
It has the relative path of the item stored as text. That leads to multiple problems and creates a huge technical debt:
- It can’t handle URL changes - if the URL slug of the target content item or URL path of any parent page changes, the link stops working
- It creates a tight couple between the headless CMS and one website channel
- It relies on the website implementation to process the link correctly
Lessons learned: URLs are a web-only thing. That’s why good headless CMSs only let you work with URL slugs and let you define links between content items using codenames or IDs, as they are the same across all channels. Any other way of handling URLs creates a technical debt and makes broken link checks very complicated.
Note: I wrote a full article on handling URLs in Next.js, so feel free to check it out if you’re interested in this topic.
Links to other items using a domain
Now, if we take the described URLs problem to the next level when we don’t store only the relative path to a specific content item, but also include the top-level domain, we’re making the problem even worse. Then, we can’t easily change the top-level domain, as none of the links would work anymore. And while changing the production domain doesn’t happen very often, having a different and unique top-level domain or subdomain for testing and preview environments happens very frequently.
Funny enough, what applies to links within a single headless CMS project is the complete opposite of how you should handle external URLs. Look at this YouTube component:
It’s one of the items on our tech debt list too. Originally, it required the editor to provide a YouTube video ID, but over time editors needed a way to select a video within a specific list, so they needed to manage the full URL. We added a next field—YouTube link—to accommodate that request and ensure backward compatibility (see Handling problems code-first above). Here we even had to describe the logic to editors (see Content model is often created and maintained by developers above).
Lessons learned: For external links, always use full external URLs. If you need to check their validity, use one of the integrations touchpoints of your CMS or run a broken link-checking tool on your frontend channels.
Handling redirects on top-most level
These days, many front-end frameworks are using client-side routing. It brings many benefits, mainly in terms of performance and visitor experience, but comes with a little hidden condition: You need to let the framework handle all URLs on its own. The reason is that the client-side router is not making a network request if you’re only navigating between pages.
And why is that a problem? If you’re handling redirects outside of the website, for example, on a hosting provider level, they won’t work for links within the website.
You probably wouldn’t design the handling of redirects on the top-most level in the first place, but in most cases, it’s a type of tech debt you’re carrying historically.
Lessons learned: Handle both URLs and redirects within the web application.
In this article, I explained the fundamental difference between traditional and headless CMS. Then, I listed the most common causes of technical debt in modern projects that use headless CMS and explained the lessons learned to make sure you can avoid the same mistakes in your projects. The list is, of course, not complete, there are many more possible causes of technical debt. If you experienced something that’s missing or want to discuss solving technical debt overall, make sure to join the community on our Discord server.