Lachlan's avatar@lachlanjc/notebook

Evolve your MDX workflow with a headless CMS

One of my roles at Watershed is to lead our public website’s design & development. Over the last few months, one of the main ways that’s manifested is publishing our blog posts, which include evergreen materials for our customers to read & reference on various topics relating to their climate programs.

Until recently, to publish blog posts we used MDX files stored in Git. The major advantage of these is complete flexibility: since I can mix Markdown with React components, we preserve the presentation-agnostic nature of Markdown, while simultaneously allowing for beautiful, interactive components with React, all while keeping our content in portable plain text. The disadvantage is how every publishing anything—or making nearly any edits to our website—blocks on me sitting down at my computer, which is inefficient for our writer & a drain on my time.

As we ramp up the pace on publishing content, we last month decided to switch to a headless CMS. If you’re not familiar, this guide explains the idea, but essentially it’s a CMS that serves a structured API with your content, leaving you to design/build the end website. We have a JAMstack Next.js website we want to swap out the blog’s data source for, making a headless CMS a perfect fit.

Coming into the project, I’d never used a CMS before, & mentally associated them with templated-feeling landing pages & big media company publishing systems. There’s been surprisingly thoughtful systems design put into a few of the modern CMS products, though, & the transition has been a big success for our site.

Headless CMSs are a surprisingly mature market with dozens of options with majorly overlapping feature sets. It’s difficult to tell a lot of them apart or find the gems. First, I somewhat-arbitrarily narrowed the field down to 3 options:

  1. Contentful is the most popular choice in the arena, via the JAMstack community survey. I heard about them from their sponsorship of the excellent 2019, a coworker had previously used & enjoyed it, & CodeDay wrote about their extensive use of it.
  2. interestingly ranked the highest in satisfaction in the JAMstack survey, if slightly less popular. I'd heard good things on Twitter, but didn’t understand what was unique.
  3. DatoCMS I knew Vercel uses, & I’ve heard about them elsewhere. Their image/video APIs stood out.

Second, I tried to differentiate them. Their marketing sites felt templated & not that helpful—they’re all selling the benefits of a CMS with undifferentiated screenshots. Looking at the Next.js cms-* example projects was instructive for what the APIs look like in practice. I got helpful Twitter replies on the topic. But signing up for all 3 to kick the tires was the most revealing.

DatoCMS has a nice WYSIWYG editor, & if I wanted to use their image & video APIs more it’d be compelling, but between next/image & Vimeo embeds, those aren’t important for this project. Creating components inside the UI & mirroring those as React components felt brittle, like leaving DatoCMS would require reformatting all the content. The editor itself was nice but nothing special.

Screenshot of a Watershed blog post in Contentful

Contentful’s dashboard makes a ton of sense, & I found it easy to set up my models, import content, & be off to the races. The text authoring experience felt solid, & media import was excellent, making it undoubtedly a strong contender.

Sanity was the hardest to figure out. After signing up, you exclusively see configuration settings like access, billing, & API tokens, but nothing for schemas & content. That’s because its “Sanity Studio” you build yourself: you write schemas as JSON with a simple structure, import those into a central schema, which is combined with Sanity’s UI, then built as a static React site you can host yourself (e.g. on Vercel), or can deploy to their free hosting (

$/mo for 10 users$99$489$115
APIsCustom GraphQL, SDKGraphQL, SDKGraphQL, REST
Editor experience3/44/43/4
SchemaWritten in code, combined with Sanity Studio via npm to be deployed as a static React site either on e.g. Vercel or their free hosting.Created in the hosted GUICreated in the hosted GUI
ExtensibilitySince Sanity Studio is a codebase, allows building custom React components right into the editor using their UI components/form management. Deployment/integration is seamless.Mediocre. Can create custom apps & deploy them to Contentful, but requires some configuration.Wouldn't need it, since all the custom blocks can be set up in the UI, & the implementation complexity shifts to the website/consumer. "Legacy Plugins" SDK is available.
ImagesImage assets can be configured to require alt text & offer other fields like captions. Full custom image endpoints & CDN delivery. Markdown editor image uploading doesn’t work out of the box.Easy to upload, hosted on CDN, provides all the metadata we want, inserts Markdown syntax in editor.Easy to upload, hosted on a CDN, wraps Imgix for custom image endpoints.
Text authoringOfficial Markdown editor available, can write MDX inside it. Also offers Portable Text, an open source/more interoperable standard for building custom blocks in your schema & using in editor, which requires a lot more code to consume.Write MDX syntax directly in Markdown editor. Also offers block-based editor with typical drawbacks.Create custom blocks inside text in their admin tools, then write custom code to map those to React components. Seemingly requires client-side JS.
VideosNothing special built in—use Vimeo, Mux (Sanity maintains integration), etc for hosting outside.Same as Sanity. Mux makes an app.Wraps Mux for custom video streaming to an HTML player, providing thumbnail API etc.
CollaborationReal-time editing (Notion-style presence avatars on fields), version history trackingVersion history trackingVersion history tracking

Decisions, decisions

The big decision revolved around how to store formatted text with components. All 3 of these CMSs have extensive capabilities for structured rich text, Notion-block style, and while it’s a much nicer editing experience than writing JSX, you’re then deeply locked into that CMS’s specific structure: the code on our website for presenting blog posts would need to map to the CMS structure, then if we wanted to migrate back to plain text or (more likely) switch to another CMS later, we would have to write a custom migration script than converted their proprietary JSON into the next CMS’s proprietary JSON. To maximize portability, we want to not be locked into any of these in case our needs/the landscape changes, so we’re storing blog post content as MDX in one plain-text field with a Markdown editor on the frontend. This shifts the technical burden off our site into the CMS, the latter of which can be upgraded/replaced without risk of breaking existing content.

After booting up a demo site using Sanity, I decided it was the best fit. I think I could be nearly as happy with Contentful, but the way Sanity allows me to custom-build components means I can guarantee the company we’ll have the editing tools we need—I’ll never be cornered by a missing feature or an arbitrary editing UI.

How I’m using Sanity

I wrote a flexible MDX component for images on the site, which uses next/image for optimization/performant loading, supports full-bleed or inset images, an optional visible caption, alt text, and can link through to another page on the site or an external link. Writing JSX props for the different options isn’t intuitive enough for non-developers, so I created a Sanity schema for images with clear GUI controls. The cherry on top was the ability to quickly write a custom React component at the end of the image editor with a button to copy the JSX image syntax. Sanity provides a top-notch UI component library that makes extending their UI incredibly quick, so simple additions don’t require writing CSS.

Screenshot of Image component creation in Sanity Studio

One critical piece of the publishing workflow—exacerbated when custom React components are in the mix—is previewing before publishing. In the Git/MDX-based workflow, I would send Vercel deploy preview links in Slack after each edit. With Sanity, I was able to integrate Next.js Preview Mode to allow split-screen live-previewing draft blog posts on our production website from inside the CMS.

Screenshot of Sanity Studio split screen editor and preview mode on the live website

As the developer, it’s been super easy to work with Sanity. Jason Lengstorf’s Smashing Magazine article helped enormously with setting up the initial architecture, & using next-mdx-remote made it easy on the frontend. With Next.js dynamic import, pages load the JavaScript for the components they need; the Vimeo player isn’t weighing down the whole blog. I switched on Next.js revalidate for each blog post to refresh each page every few seconds1 while continuing to serve our site statically from the Vercel CDN.

Expanding the site

Screenshot of section of Watershed homepage with customer milestones

This past week, I added a new section to our homepage to show milestones from Watershed customers, each with their logos & hyperlinked to their sites. To prototype the component, I made a quick JSON file in the codebase, but realized connecting these to Sanity would allow other team members to add new milestones as they’re announced. In about half an hour, I spun up a new Sanity schema, including making a descriptive form with per-field instructions, pasted my JSON content in, & wrote a quick query on the frontend to pull them in.

Screenshot of customer milestone configuration in Sanity Studio

As a self-taught developer working on small projects, I never thought I needed a CMS, that they were built for non-technical big companies. Though sometimes a simple JSON or MDX file suffices, when the content keeps growing or you’re collaborating, these modern headless CMS services are anything but outdated. Publishing at Watershed now takes a small fraction of the time it used to, & I’m glad to have added another tool to my toolbox for project-building. I’ll be using Sanity again in the future.


  1. While I love revalidate, I super wish Next.js had a simpler push-based solution, allowing Sanity to ping the site to update per-page. It’d save on useless serverless function executions & allow immediate updates.