Twitter Vimeo download Facebook Pinterest flash Brackets { }
Yes this is a rocket

Turn Craft 3 into a headless CMS

What is a headless CMS?

The classic CMS approach is to couple the presentational layer with the CMS directly. With this monolithic approach of building a CMS, your developers need to use a specific template language, and you lose much flexibility. This kind of CMS outputs content for only one channel: web browsers.
You may think that this is enough for now, but we will prove that you can do so much more with your content than show it on one channel. Instead, we want to enable an omnichannel experience.

Why would I want to have a headless CMS?

Performance, Security and Scalability.
In the headless approach there is no presentational layer out-of-the-box, and therefore developers are not bound to any templating or programming language to create one. With a headless CMS instead, you can have multiple heads, and this gives us a lot of freedom, scalability, and flexibility. Think of an app, voice interface, AR or any other new technology you need to deliver content to.

A Static Site Generator like GatsbyJS or Nuxt.js (there are many others) fetches all the content from your CMS via an API and generates old fashioned static sites. Static does not mean the site cannot be interactive. It just means that the site doesn’t need to communicate with the CMS to fetch the content. If you are looking to improve the security of your site, a static site can live on another server without any connection to the CMS. The connection happens during build time which means, our frontend is blazing fast!

Headless Craft

Let’s see how to make use of Craft as a headless CMS. We’ll use CraftQL to access content in Craft, and Gatsby to render it into static HTML. Here we go.


Our static site generator of choice is GatsbyJS. With the plugin CraftQL we can connect our GatsbyJS with the CMS over GraphQL.

Let’s start with installing the plugin:
composer require markhuot/craftql:^1.0.0

Once installed, we need to create an endpoint. Go to your CraftCMS admin and head over to settings —> CraftQL. Enter the URI for your endpoint or stíck with the default ‘api’. Think of a name and generate a new token. Make sure you open the settings of your token you just created. Here you can adjust the power of the token. Should this token be read-only or can you also mutate entries with your token?

Remeber, your api endpoint will be your CraftCMS URL + URI name (default: api):

Do not forget to update the API token settings if you add a new section. Otherwise the API does not know that there is a new section.

Screenshot of CraftQL settings


Let’s jump in the frontend and prepare the project with Gatsby.

Setup the GraphQL plugin

Create a .env file in your project root. The file should be now next to your package.json. We want to hide all credentials with this .env file, so if you are using any version control, make sure to .gitignore (YMMV) the .env file. We do not want to have the credentials visible in a repository.
Edit the .env file and add:


Go to the file Gatsby configuration file (gatsby-config.js) and add the graphql plugin to the list:

  resolve: `gatsby-source-graphql`,
  options: {
    url: `${apiUrl}`,
    typeName: "Craft",
    fieldName: "craft",
    headers: {
      Authorization: `bearer ${graphqlToken}`,
    start_url: '/',
    background_color: '#663399',
    theme_color: '#663399',
    display: 'minimal-ui',

You also need to fetch the credentials we set up in the .env file. To do that, put the following lines at the very top of your gatsby-config.js file.

const graphqlToken = process.env.GRAPHQL_TOKEN;
const apiUrl = process.env.API_URL;

The complete gatsby-config.js should now look like this:


const graphqlToken = process.env.GRAPHQL_TOKEN
const apiUrl = process.env.API_URL

module.exports = {
  siteMetadata: {
    title: `Edenspiekermann CraftQL test project`,
    description: `Let's make CraftCMS headless`,
    author: `@edenspiekermann`,
  plugins: [
      resolve: `gatsby-source-graphql`,
      options: {
        url: `${apiUrl}`,
        typeName: 'Craft',
        fieldName: 'craft',
        headers: {
          Authorization: `bearer ${graphqlToken}`,
        start_url: '/',
        background_color: '#663399',
        theme_color: '#663399',
        display: 'minimal-ui',

Fetch the content

For the last step, we need to fetch the content from our API. Go into your Craft Admin panel and make sure you have a page with the handle homepage.
Rename the file pages/index.js into pages/index.jsx and add the following content:

import React from 'react'
import { StaticQuery, graphql } from 'gatsby';

import Layout from '../components/layout';

const IndexPage = () => (
    query={graphql`{ craft { home: entries(section: [home]) { ... on Craft_Home { id title slug } } } }`}
    render={({ craft }) => {
      const homepage = craft.home[0];
      const {
      } = homepage;
        return (
            <p>Entry Slug: {slug}</p>
            <p>Entry Id: {id}</p>

export default IndexPage;

Now everything should be set. Start the gatsby server with gatsby develop, start — if needed — your MAMP or similar web development solution and go to localhost:8000. You should see the title, slug, and id of your home entry. Gatsby testsite To keep this tutorial simple, we fetched only the CraftCMS default fields. Not it’s up to you to extend your data model. You can query any field you want via the API we showed here. Use the GraphQL interface for a more visual approach to building queries: http://localhost:8000/___graphql. Gatsby has some nice caching going on with which the live-reload is nice and smooth. Make sure if you change anything in the database you need to delete the .cache folder and start the Gatsby task again. Otherwise, you can not see any updates.

Head over to the Official GatsbyJS Quick Start if you need some help here.

Wrap up

CraftCMS is easy enough to use as a headless CMS. Go on and explore, and enjoy the performance, security, and scalability of being headless.

Dimitri Steinel’s avatar

Dimitri Steinel is a frontend developer at Edenspiekermann (Berlin).