Jekyll with a Headless CMS: Contentful, Sanity, and Decap Compared
Add a visual content editor to your Jekyll site without giving up static performance. A comparison of Contentful, Sanity, and Decap CMS for Jekyll.
Jekyll stores content as Markdown files — ideal for developers, frustrating for non-technical editors who want a visual dashboard. A headless CMS solves this by providing a content editing interface while keeping Jekyll as the static site builder.
Here is how the major options compare, and how to set each one up.
What is a headless CMS?
A headless CMS separates the content editing interface (“head”) from the front-end presentation. Editors log into a dashboard to write and manage content. The CMS stores content and exposes it via an API or writes it back to files. Jekyll consumes that content at build time.
The result: your editors get a friendly UI; your site is still static HTML.
Option 1: Decap CMS (formerly Netlify CMS)
Decap CMS is the most Jekyll-native option. It is an open-source, Git-based CMS — it reads and writes directly to your repository, so content stays in Markdown files alongside your code. No external API, no additional database.
How it works
Editors log into /admin/ on your site. The dashboard reads your existing Markdown files from Git, lets editors create and edit posts, and commits changes back to the repository. Your CI/CD pipeline (Netlify, Cloudflare Pages) picks up the commit and rebuilds the site.
Setup
Create admin/index.html:
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Content Manager</title>
</head>
<body>
<script src="https://unpkg.com/decap-cms@^3.0.0/dist/decap-cms.js"></script>
</body>
</html>
Create admin/config.yml:
backend:
name: git-gateway
branch: main
media_folder: "assets/images/uploads"
public_folder: "/assets/images/uploads"
collections:
- name: "posts"
label: "Blog Posts"
folder: "_posts"
create: true
slug: "{{year}}-{{month}}-{{day}}-{{slug}}"
fields:
- { label: "Layout", name: "layout", widget: "hidden", default: "post" }
- { label: "Title", name: "title", widget: "string" }
- { label: "Description", name: "description", widget: "text" }
- { label: "Date", name: "date", widget: "datetime" }
- { label: "Author", name: "author", widget: "string" }
- { label: "Body", name: "body", widget: "markdown" }
Enable Identity and Git Gateway in your Netlify dashboard under Site settings → Identity.
Pros: Free, Git-based, no external API, content stays in your repository.
Cons: Requires Netlify for OAuth; limited widgets compared to commercial options.
Option 2: Contentful
Contentful is a cloud-based headless CMS with a polished editor, powerful content modelling, and a generous free tier (25,000 records, 2 users). Content is stored in Contentful’s cloud and delivered via their API.
How it works with Jekyll
Unlike Decap, Contentful does not write Markdown files — it stores content in the cloud. You use a build plugin or a custom Ruby script to fetch content from Contentful’s API at build time and generate Jekyll data files or Markdown posts.
Using a Jekyll Contentful plugin
# Gemfile
gem "jekyll-contentful-data-import"
# _config.yml
contentful:
spaces:
- example:
space: YOUR_SPACE_ID
access_token: YOUR_ACCESS_TOKEN
cda_query:
include: 2
all_entries: true
Run bundle exec jekyll contentful to pull content before building. In CI, add this step to your build command:
bundle exec jekyll contentful && bundle exec jekyll build
Content arrives as YAML data files in _data/contentful/spaces/. You loop through them in Liquid like any other data:
{% for post in site.data.contentful.spaces.example.blogPost %}
<h2>{{ post.title }}</h2>
{% endfor %}
Pros: Polished editor, strong content modelling, free tier is generous, good for large teams.
Cons: Content lives in the cloud (not your repo), API dependency at build time, can be complex to set up.
Option 3: Sanity
Sanity is a flexible, API-first headless CMS with a real-time collaborative editor (Sanity Studio) you can customise with JavaScript. It is particularly popular for structured content and complex content models.
How it works with Jekyll
Sanity stores content in its cloud. You fetch it at build time using a Ruby script or the Sanity JavaScript client.
Install the Sanity CLI and create a project:
npm create sanity@latest
Fetch content at build time with a Node.js script:
// tools/fetch-sanity.js
const { createClient } = require("@sanity/client");
const fs = require("fs");
const path = require("path");
const client = createClient({
projectId: "YOUR_PROJECT_ID",
dataset: "production",
useCdn: true,
apiVersion: "2024-01-01",
});
async function fetchPosts() {
const posts = await client.fetch(`*[_type == "post"]{title, slug, body, publishedAt}`);
posts.forEach(post => {
const content = `---
layout: post
title: "${post.title}"
date: ${post.publishedAt}
---
${post.body}`;
const filename = `${post.publishedAt.split("T")[0]}-${post.slug.current}.md`;
fs.writeFileSync(path.join("_posts", filename), content);
});
console.log(`Fetched ${posts.length} posts`);
}
fetchPosts();
Add to your build command:
node tools/fetch-sanity.js && bundle exec jekyll build
Pros: Extremely flexible content model, real-time collaboration, excellent for complex structured content.
Cons: More setup than Decap, content not in your repo, costs money beyond the free tier.
Comparison table
| Decap CMS | Contentful | Sanity | |
|---|---|---|---|
| Content storage | Git (your repo) | Cloud | Cloud |
| Free tier | Free (open source) | 25k records, 2 users | Up to 3 users |
| Setup complexity | Low | Medium | Medium-High |
| Editor experience | Good | Excellent | Excellent |
| Content modelling | YAML config | Drag-and-drop | JavaScript |
| Real-time collab | No | Yes (paid) | Yes |
| Best for | Small teams, developers | Mid-size teams | Complex content |
Which should you choose?
Choose Decap CMS if: Your team is small, you want content in Git, you are already on Netlify, and you want zero additional infrastructure cost.
Choose Contentful if: You have non-technical editors who need a polished experience, your content model is straightforward, and you want a managed solution with a free tier.
Choose Sanity if: You need a highly customised editing experience, complex structured content with references and blocks, or real-time collaboration out of the box.
For most Jekyll sites with one or two editors, Decap CMS is the right choice — it is free, keeps content in Git, and requires no external API at build time.
Choosing between Git-based and API-based headless CMSs
The core architectural decision when adding a CMS to Jekyll is whether to use a Git-based CMS (content stored in your repository as Markdown files) or an API-based CMS (content stored in the CMS’s own database, fetched at build time via API).
Git-based CMSs — Decap CMS and Forestry/TinaCMS being the primary options — keep your content in your repository. Editors make changes through a UI, but every save is a Git commit. Your content is always under version control, always portable, and never dependent on a third-party service’s continued existence. The trade-off is that the editing experience is constrained by what Markdown and front matter can express, and real-time preview requires additional configuration.
API-based CMSs — Contentful, Sanity, Prismic — store content in their own infrastructure. Jekyll fetches this content at build time using a data file or a custom plugin. The editing experience is typically richer (block editors, real-time collaboration, media libraries), and the content model can be more complex (typed fields, references between content types, versioning). The trade-off is operational dependency on a third party, ongoing API costs at scale, and additional complexity in the build pipeline.
For most Jekyll blogs and portfolio sites, the Git-based approach is strongly preferable. The editorial experience in Decap CMS is sufficient for prose content, the operational simplicity is significant, and the zero-cost pricing removes one more subscription from your monthly expenses.
Structuring your editorial workflow
Regardless of which headless CMS you choose, the editorial workflow matters as much as the technical setup. Define clearly who can publish directly to main versus who must go through a review step. Configure branch protections in GitHub to enforce this — requiring a PR review before merge, for instance, prevents accidental direct publishes to production.
For sites where content quality is critical, Decap CMS’s editorial workflow (which creates GitHub Pull Requests for each draft) gives editors, reviewers, and editors-in-chief distinct roles with appropriate access levels. A post by a junior writer goes to draft → in review → ready to publish, with the editor approving at each stage. This workflow is invisible to readers but creates genuine editorial rigour that improves content quality over time.
Set up a staging environment where the develop branch is deployed automatically by Netlify or Cloudflare Pages. Editors can preview a draft at the staging URL before it goes to production. This is not difficult to configure — both Netlify and Cloudflare Pages support branch deployments with distinct URLs — and it eliminates the class of problems where content looks correct in the CMS preview but renders incorrectly in the actual Jekyll build.
Media management in a headless CMS context
Images are the most challenging aspect of headless CMS setup for Jekyll. Markdown files reference images by path; headless CMSs provide image upload UIs that need to store those images somewhere accessible to both the editor interface and the built Jekyll site.
With Decap CMS, the standard approach is to store uploaded images in your repository under assets/images/uploads/ or similar. Every image upload is a Git commit, which keeps images in version control but increases repository size over time. For sites with moderate image volumes this is acceptable; for photography portfolios or media-heavy publications, a Cloudinary integration (supported natively by Decap CMS’s media library settings) offloads image storage and transformation to a dedicated image CDN.
With API-based CMSs, images are typically hosted by the CMS provider’s CDN and referenced by URL in your content. This removes the repository size concern but creates a dependency on the provider’s image infrastructure. Contentful, Sanity, and Prismic all offer image transformation APIs (resizing, format conversion) via query parameters, which can replace your local Sharp or ImageMagick pipeline.
Whichever approach you choose, configure your Jekyll templates to use responsive image markup — srcset with multiple widths and WebP format where available. A headless CMS makes image management easier for editors; responsive markup ensures those images load efficiently for every visitor regardless of screen size or network speed.
Comparing Git-based vs API-based headless CMS for Jekyll
The fundamental choice in headless CMS selection for Jekyll is between Git-based systems (Decap CMS, CloudCannon, Forestry) and API-based systems (Contentful, Sanity, Prismic, Storyblok). The two approaches differ in where content lives, how builds are triggered, and what the editorial and developer experiences feel like.
In a Git-based system, content lives in your repository as Markdown files with YAML front matter — exactly where it already is for a Jekyll site. Editors use a visual interface, but under the hood every save is a Git commit. The CMS is a thin layer on top of your existing content workflow. Jekyll builds are triggered by repository pushes, which is the same mechanism as any other deployment. Migrating away from the CMS means turning off the web interface — your content is already in Markdown files.
In an API-based system, content lives in the CMS provider’s database and is fetched at build time via an API call. Jekyll uses a plugin or a custom script to retrieve content, transform it into page objects, and build the site from the fetched data. The editorial experience is typically more polished — rich text editors, structured content models, real-time collaboration, content translation workflows. The trade-off is that your content is no longer in files you control, and migrating away requires exporting and converting the data.
For most personal blogs and small team sites, the Git-based approach is the clear choice. Your content stays in your repository, the workflow is familiar to anyone who uses Git, and the cost is zero. For product documentation sites, marketing sites with multiple content types, or organisations with non-technical editorial teams that need real-time collaboration, an API-based CMS justifies its additional complexity and cost.
Editorial workflow: drafts, review, and publishing
The editorial workflow most relevant to teams is the review-before-publish pattern: a content editor drafts a post, a reviewer approves it, and only then does it go live. Git-based CMSs implement this using pull requests — a draft creates a new branch, a reviewer merges the pull request to trigger a production build. API-based CMSs implement it with a publishing state machine — content has Draft, Review, and Published states, with role-based access controls preventing editors from publishing without reviewer approval.
Decap CMS supports an editorial workflow via its optional publish_mode: editorial_workflow configuration. When enabled, saves create pull requests rather than direct commits to the main branch. Reviewers see the draft in the CMS interface and can approve it with one click, which merges the pull request and triggers a production build. This pull-request-based workflow is robust, auditable, and fully integrated with your repository’s existing PR review processes.
The editorial workflow requires careful configuration of branch naming and build preview deployments. Netlify and Cloudflare Pages both support automatic preview deployments for pull requests — a unique URL where the draft site is accessible before merging. Sharing this preview URL with stakeholders for sign-off before publication is a professional, low-friction review process that works well for content-focused teams.
For solo creators and small sites without review requirements, editorial workflow adds friction without benefit. Configure publish_mode: simple (the default) so saves go directly to the main branch and trigger immediate builds. Switching between modes is a one-line change in config.yml if your workflow requirements change as your team grows.