Single Sourced

Turn your Git repo into a CMS with Tina

Wilfred Springer
East Pole
8 min readApr 17, 2024

--

Last week, I moved my tiny eastpole.nl web site over to Next.js, for a couple of reasons. First of all, I needed to eat my own dog food. I have been telling customers to move to Next.js for years now, and actually helped quite a few of them getting there, but my own site was still based on a single CoffeeScript file, some Pug files and a bunch of Metalsmith plugins, so that had to change.

Another reason for moving to Next.js is that I wanted to understand TinaCMS a bit better. Now, the eastpole.nl website is hardly so complicated that it absolutely needs a headless content management system. However, I’m planning a few websites for some stories that must be told, and for those websites, having a headless content management system actually does make a lot of sense. And if I moved my own tiny eastpole.nl website over to Next.js, that would give me an awesome opportunity to try a headless CMS that I have been eyeing for a while now.

Before TinaCMS

Before TinaCMS, I have been pushing StoryBlok a lot. StoryBlok was one of the first headless content management systems that actually allowed you to be fully headless and at the same time have something close to an in-place visual editing user experience. Where many other content management systems pushed really hard to claim control over your entire presentation layer (Adobe Experience Manager), or basically required re-rendering the entire page to get a visual a preview (Contentful), StoryBlok allowed you to stay in control over your presentation layer, and at the same time offered an almost visual editing experience to the content editors.

I have always loved the approach StoryBlok took, and I think it’s fair to say many CMS providers started copying their approach. At the same time, there were also some disadvantages with StoryBlok, which mainly evolved around keeping changes in your codebase aligned with changes in your CMS; this applied both to content definitions as well as actual content itself.

With different repositories, coordinating code and content releases is hard
  1. Content: In many cases, you want new features to be accompanied with new content. A new feature should be accompanied with UI content explaining those new features. Those explanations should not be released before releasing the actual feature. Since the copy covering these new features and the features themselves live in different systems, releasing them in lockstep tends to be really complicated.
  2. Content Definitions: Not having your content being released in lockstep with the features might be confusing to the customer, but it gets worse when the content definitions in both systems are misaligned. Your rendering code might assume something about the content definitions that is not guaranteed by your CMS yet, or your CMS might introduce some changes in content definitions that are not understood by your rendering code yet. In both cases, it potentially breaks your code in a horrible way.

Enter TinaCMS

Because of this, I was really happy to find there now was a CMS that actually solved this problem by keeping your content and the content definition in your repository! As a consequence, your content and content definitions would always be aligned with the features introduced on a feature branch. Misalignment will break the build, and a broken build will never land on main and therefore in production.

I was even happier to find out that it still supported the exact same visual editing experience StoryBlok offered. And where StoryBlok relied on Vue based custom components that had to be uploaded to the CMS, custom components in TinaCMS are based on React and Tailwind, and are being sourced directly from your repository as well.

Last, but not least, I found out that it seemed to be possible to use TinaCMS without even using a TinaCMS account. but TinaCMS itself is just a toolkit that also works if you only run it in development, without connecting to the central CMS system at all. (In that case, changes will be comitted to your local file system. Admittedly, this is not the way to go if you have content editors that are not developers, but for my purpose, this works just fine.)

As consequence, you can run it for free. Now, again, I do thing it’s worth the money, but nothing beats free. In fact, a couple of years ago, I tried helping an artist with a system to be run at an exhibition on large displays. She needed an option to upload content herself, so I offered her the entire solution based on StoryBlok, and because of her limited funds, we couldn’t go ahead. Free, in some cases, is really the only option, as it seems.

How TinaCMS works (in my case)

With TinaCMS, you define the structure of your content in JavaScript or TypeScript. Tina then takes these definitions and creates GraphQL API for storing and retrieving data snippets conforming to your definitions. The data it self is stored on your file system, somewhere in the directory structure of your repository; the same repository that also hosts your code.

Inside your Next.js pages (or React Server Components), you interact with the GraphQL API it provided. Inside these components, you use some path information provided with the request to retrieve relevant content from the GraphQL API. In a typical setup, you will have some type of “page” content type that exists of different “blocks”. Every block type will have a corresponding React component. It’s up to you, the developer, to retrieve the “blocks” for a page to create instances of these React components based on the data provided for every block.

Once you do that, you can access the “admin” page (in development mode hosted together with your Next.js server), open a page and get an editor that allows you to add new blocks, move blocks around, and edit the details of a block.

In my case, when I’m happy with the results, I will run next build with the GraphQL API running in the background. That will generate the static version of the site. If the build succeeds and my branch is merged to main, then this static site will be uploaded to S3.

How I feel about it

You will probably have already guessed by now that I’m pretty happy with the results:

The good

First of all, integrating TinaCMS into the codebase of my web site turned out to be pretty easy. My landing page already consisted of a container with a bunch of React components for the various sections. In most of those cases, the data was embedded inside those components. Moving that data out of the components and into a JSON file was fairly simple.

And with my content now living in the repository, I can now rely on standard Github pipeline code to push my updated static site to S3. What’s more important: I can have as many branches as I want, and there might be conflicting content definitions on each of them without getting in each others way. I can work with TinaCMS on each of these branches, and rely on normal Git conflict resolution mechanisms to eventually harmonize those different definitions.

The content sections of my web page were already rendered from Markdown. For that, I was using React Markdown. Tina is offering its own markdown rendering component, called TinaMarkdown. At first, I thought you could use it as a drop-in replacement, but then it turns out that the GraphQL API providing access to your markdown already parses it and turns it into a structured document model. This prevents you from needing Markdown parsing capabilities in your front-end, but it does force your to redefine the content model of your components.

One of the things I wanted to avoid is to have my “block” components be overly aware of TinaCMS. It turns out that makes the whole setup perhaps a bit too complicated. For “block” type components, I would now recommend accepting that they are TinaCMS aware. In fact, their props definitions should ideally be the types TinaCMS generates from the GraphQL definitions.

The bad

There are a couple of things that have been bothering me a little:

First of all, in order to support visual editing, I had to add use client; to the React component representing my page. I still get server side rendering, but the amount of JavaScript code required to make my page editable inside TinaCMS grew considerably as a result.

Another thing that TinaCMS does not offer is the asset management capabilities you would typically need. Sure, you can upload an image and it will be stored correctly on your file system, but in reality, you will always need something that scales your images to the right sizes and caches the results, ideally on some sort of edge-side cache.

TinaCMS doesn’t offer that. It does offer some integration with other media services, but I haven’t really tried any of that yet, so it’s hard to judge if it’s any good. (This is where I was missing StoryBlok for a second.)

The ugly

According to the documentation, all I had to do for the static build is add this to my package.json file

"build": "tinacms build && next build"

Turns out that resulted in horrible errors. Error reporting in this case is less than helpful. After some fiddling around, it turns out I could actually get the build to run by:

  1. Start the TinaCMS dev server with tinacms dev
  2. Run the build with next build
  3. Kill the TinaCMS dev server once next build was done

I’m still not entirely sure if there is something in my setup that causes the documented setup to fail. It might be that the documented setup is assuming that I run my build through TinaCMS Cloud, but the documentation here is a little limited.

The final verdict

I feel confident enough about the entire setup to start implementing it in a new web site. There are some things that I yet have to work out, like how a setup with TinaCMS Cloud would work, but the experience so far has been largely a walk in the park. I’ll accept the fact that TinaCMS does not have asset management capabilities that I was hoping for, but I feel that I might actually be able to resolve that by using next/image and Cloudflare Images.

Check the video below for an overview of the current setup.

--

--

Wilfred Springer
East Pole

Double bass playing father of three, hacker and soul searcher