One of our recent projects called for a feature to allow editors to manage and publish content. In other words, we needed to choose a headless CMS.

We already had a React-based web app, and a React Native mobile app in the works where we wanted to present that content. So we needed to choose a Headless CMS. Unlike Wordpress or other traditional CMS solutions, a headless CMS does not come with a web interface where readers will see published content. Instead, a headless system acts more like a database: it hosts content, and your app uses an API to fetch content to display. As a result, your choice of app architecture is not tied to your choice of CMS. This can be especially helpful if you want to incorporate managed content into an app that is not primarily a publishing platform, or if you want to present the same content differently in different contexts as was the case with our web vs mobile apps.

The biggest challenge is to choose a headless CMS system that is right for you. The field is wide open, and headless systems have not been around long enough for a clear leader to stand out. During my research I came across a comparison spreadsheet that lists 20 solutions. That sheet is not comprehensive — for example it excludes Contentstack which was one of the solutions we considered.

choosing a headless cms

It’s also not completely up-to-date with features that have been recently added to the solutions that it does list. But it provides a valuable overview of what features are available, and roughly how the solutions available compare. The overall score in the last row is a quick way to size up different options. But keep an eye out for specific features that are important to you.

What we looked for in Choosing a Headless CMS

Our use case had some specific requirements. The app we built publicizes events, and we wanted to use the CMS to store event details. We also wanted to migrate content from a legacy source. So we looked for a solution with support for:

  • Custom fields that we could use to enter start times, agendas, photos, descriptions, number of seats available, and other details.
  • Content types so that we could set up an event document type with the aforementioned custom fields plus distinct document types for other purposes including unstructured content pages.
  • A read-write API for migrating content. Some solutions offer a bulk import feature instead of a write API but after trying one of those we found that we really needed the flexibility of an API.

We also considered the price of each solution when we choose a headless cms.

We looked most closely at ButterCMSCloud CMSContentfulContentstackGhostPrismic, and Sanity. This list was a result of suggestions from our client, experience related by colleagues, and online research. We were able to eliminate some options quickly: Contentful was deemed too expensive; Ghost does not appear to provide the support we needed for content types or custom fields; colleagues told us that based on their experience they think ButterCMS does not provide adequate support for those features either. The same colleagues had had a good experience with Prismic, so we chose that for our first iteration.

Prismic

Prismic’s support for custom fields and content types did indeed suit our needs.

prismic cms

But Prismic is one of the solutions that provides a bulk-import feature instead of a read-write API, and we ran into problems migrating content. The bulk import involves bundling content into zip files which are uploaded via a form. It’s a slow process, and we found the lack of ability to update individual documents programmatically to be frustrating. During development, we realized that we would like to be able to automatically update individual event documents in certain cases. We could have found another way to do what we wanted, but at that point we were concerned about locking ourselves into a platform that would limit our ability to interact with content. We decided it was time to pull the plug and move on to the next candidate.

Sanity

Our next pick was Sanity because I was able to verify that it has good support for the features that we needed, including a read-write API.

sanity cms

It also comes with some nice-to-have features including schema migration and a customizable UI for editors which came in very handy. What makes Sanity stand out from other solutions is that it is configured and customized in code instead of in a web interface. Content types and custom fields (the “schema”) are defined in TypeScript files. The Studio (the web interface that editors use to write content) comes in the form of a React app that we customize and deploy ourselves. Getting the base Studio set up only takes a couple of commands. Creating custom input components for custom fields is a matter of dropping in new React components, and importing them in the schema.

To me as an engineer configuring a CMS in code seems ideal. We get to keep the configuration in version control, apply our code review process to ensure the quality of changes we make, and we can write migrations in code to automatically update existing documents when we make changes to custom fields. But using code does mean that it would be difficult to make configuration changes without bringing in engineers. In our case the custom fields in event documents in the CMS must be kept in sync with app code that displays events; so we want engineers involved in CMS configuration changes regardless. In other situations where engineering resources are limited web-based configuration might be preferable.

Customizing the editing UI

The ability to customize Sanity’s editing UI ended up saving us a lot of trouble. I mentioned that events in our app have a start time and an agenda. We want times for both of those to be entered according to the time zone where the event will take place. Sanity’s built-in date-time input shows times in the time zone that the editor’s computer is set to. We thought that would lead to confusion if an editor is working in one time zone but is working on an event in another time zone. To compensate for this we introduced a custom date-time input that reads a time zone value from the selected location for the event, and applies that time zone to the start time and agenda time inputs. The code looks basically like this:

import styles from "part:@sanity/base/theme/forms/text-input-style"
import FormField from "part:@sanity/components/formfields/default"
import { withDocument } from "part:@sanity/form-builder"
import PatchEvent, { set, unset } from "part:@sanity/form-builder/patch-event"

import * as React from "react"
import Datetime from "react-datetime"

class DatetimeInputRaw extends React.Component {
render() {
// `document` is the event we are editing
const { document, markers, readOnly, type, value } = this.props

// Reference the location entered for the event (if one has been selected)
// to get it's time zone.
const timezone = document.location?.tz ?? FALLBACK_TZ

const selectedDatetime = value && new Date(value)

return (
<FormField
label={type.title}
description={type.description}
markers={markers}
>
<Datetime
value={selectedDatetime}
displayTimeZone={timezone}
onChange={(datetime) => this.onSelectDate(datetime)}
inputProps=
/>
</FormField>
)
}

private onSelectDate(date: Date | null) {
const patch = PatchEvent.from(date ? set(date.toISOString()) : unset())
this.props.onChange(patch)
}
}


// Wrap `DatetimeInputRaw` with the `withDocument` higher-order component to
// inject the `document` prop.
export const DatetimeInput: React.ComponentType<DatetimeInputProps> = withDocument(
DatetimeInputRaw
)

Dropping in React components helped us in a few places. We didn’t have to modify any of Sanity’s existing code — the customizations we made are purely additive. As a result we don’t have to do any tricky code merging when Sanity pushes updates.

If I have one complaint about Sanity it’s that its TypeScript support could be better. The Studio and API client libraries ship with type definitions, but in many cases those definitions apply the blanket any type to exports. I would especially like to be able to automatically generate types from the schema files that we wrote to apply to data fetched from Sanity in our user-facing apps. For the time being we have to keep type definitions in app code in sync with the CMS schema by hand.

Conclusion

We were able to choose a headless CMS that was right for our situation but there are lots of variables to consider when you choose a headless CMS. We had a need for specifically structured content, custom time zone handling, and the ability to create and update CMS documents via an API. We found that Sanity meets those needs well given that we have engineering resources available to manage CMS configuration and to coordinate interaction between the CMS and our user-facing apps. I would recommend Sanity to anyone looking for a headless CMS unless you want to be able to set up a system without pulling in engineers.

Jesse Hallett is a Senior Software Engineer at Originate, a concept to growth acceleration partner for digital products.