Bryan Lee

Adding Headless CMS To Eleventy

· Web Dev

Couple of weeks back, as I was working on the previous "GTM Global Click Event Tags" post, I took a wrong turn by accident and slid deep down into the Internet rabbit hole of headless CMSes.

I wanted to add images into the post, and to introduce a featured image into my posts. I also did not want to store these images together in the git repo, but I think the biggest reason was that I was just looking for an excuse to play around with headless CMSes.

Choosing as my CMS of choice

Excuse in hand, I looked into the various headless CMSes out in the market. There are a lot out there like Contentful, Prismic, DatoCMS etc. I eventually settled on, primarily because it seemed to be really developer-friendly and had a good set of features in the free plan.

The sign up process is a good example of their position to be developer-friendly. Instead of filling up a form to sign up as you normally would, you install and run the Sanity CLI npm package, which walks you through initialising a new Sanity project and creating your account.

npm install @sanity/cli -g
sanity init

Sanity CLI installs a local copy of Sanity Studio. This is a single page React app and is your CMS user interface that you can customize. I opted to have my Sanity project files in a sanity folder inside my 11ty blog project. Inside the sanity folder, you will want to pay particular attention to the schemas subfolder. This contains the various configurations for your content types. You can create new content types, and add/modify/remove different field types.

I won't go into detail about configuring Sanity as their documentation is already very comprehensive. When happy with your changes, you can deploy them. Your "production" Sanity Studio can then be accessed by going to

Retrieving data from Sanity in 11ty

I really like the simplicity of 11ty. Date can be supplied to your templates either statically in a JSON file, or dynamically via a JavaScript function.

Data can also be made available to 11ty templates on multiple levels, content, template, directory, or global. I decided to sync my posts from Sanity on the global level since multiple templates would need to access them - homepage, blog index page, and blog post page.

Inside _data/posts.js:

const sanityClient = require('@sanity/client')

const projectId = process.env.SANITY_PROJECT
const apiToken = process.env.SANITY_TOKEN

const client = sanityClient({
dataset: 'your_dataset_name',
token: apiToken,

module.exports = async function () {
const query = `
*[ _type == "post" && !(_id in path("drafts.**")) ]{
slug { current },
} | order(publishedAt desc)

const params = {}

return await client.fetch(query, params)

You'll notice the weird stuff going on in query which is written in GROQ, Sanity's own query language. Essentially in this query, I'm asking for all posts that are published (not draft). I'm also only requesting for a few specific fields.

Why not GraphQL?

Sanity actually has GraphQL support which I started out with. It seems to be a new feature and I ran into some issues later on when I wanted to query for inverse relations which I am not going into here. I was able to solve my issue by using GROQ, so I switched out all my queries to GROQ for consistency.

Overall, I've found GROQ to be a little more powerful than GraphQL. Depending on what you're trying to do in your Sanity integration, GraphQL might be more than enough. If not, give GROQ a go. The documentation for GROQ is pretty clear as well.

Displaying posts in 11ty

Now when 11ty builds, the posts are fetched from Sanity and available for use in templates. I wanted to display a list of my posts in my blog/index.html:

{% for post in posts %}
<h2>{{ post.title }}</h2>
{% endfor %}

The posts variable is immediately available in the template without us having to declare anything as this is already merged into 11ty's data cascade. In the template (using Liquid), I'm simply looping through the posts and rendering out the title.

Rendering individual posts

Now that the blog page is listing out all my blog posts, I would like to show them individually in a page. To do that, I'll create a post template and make use of pagination in 11ty.

Inside a new template file posts/posts.liquid:

layout: blog-post.liquid
data: posts
size: 1
alias: post
permalink: "/{{ post.slug.current }}/"

{{ post.body | sanityToHTML }}

What's going on here?

In the frontmatter, we are telling 11ty to paginate all our posts, rendering one per page. We override the permalink to use our own slug that is defined in Sanity.

In the template body, the post body is ran through a custom filter sanityToHTML. Instead of Markdown or plain HTML, Sanity stores page contents as Portable Text. To parse and render it properly, we can use a package written by the folks at Sanity - block-content-to-html.

In my .eleventy.js config, I defined a custom filter:

const blocksToHtml = require('@sanity/block-content-to-html')

eleventyConfig.addFilter('sanityToHTML', function(value) {
return blocksToHtml({
blocks: value,

These are the basics needed to integrate with Sanity into 11ty.

Developer-friendly but rough around the edges

Overall, I quite enjoy using Sanity to write in my blog. The documentation has been fantastic, and it was quite straightforward to setup the content models. Sanity's GROQ language also has support for a GraphQL playground-like tool called Vision to test queries which made it easy to pick up.

It is still rather rough around the edges and I can see a more complex website may run into limitations. For one, the interface is still quite unpolished. For example, modals don't have a save / confirmation button. You can't use markdown shortcuts in the editor, and while the common Cmd+B, Cmd+I shortcuts work, Cmd+K to create a link does not.

One other thing that I really enjoy about Sanity is also their image pipeline. Most other major headless CMS solutions would also have their own variation. By appending parameters in the URL, you can transform images on the fly. The results are cached behind a CDN so they are always served fast. This makes working with images and photos and serving the most optimized versions extremely easy.

The limits in the free account are generous enough, and even if I start to exceed the bandwidth limits, it is a relatively small amount to pay for additional allowances. It's been a pleasant experience using and integrating Sanity into 11ty, and I look forward to them polishing up their interface in the near future.

Did you enjoy this post? I enjoy writing on a range of tech-related topics. Most recently, I migrated my Jamstack blog from Hugo to 11ty and wrote down my experience. Big fan of Jamstack too? Let's connect.