Headless Mode + GraphQL
Craft can also act as a “headless” CMS. This style of website or application is typically split into two halves: a back-end, which is responsible for managing content and exposing a developer-friendly API; and a decoupled front-end, that requests data over HTTP from the back-end and displays it to users. Instead of rendering Twig templates on the server, you’ll use the built-in GraphQL API to fetch content and render it with whatever client-side view library you or your team is familiar with.
Craft’s GraphQL API will feel familiar, now that you have experience with loading content in a Twig environment!
Building a front end this way is heavily reliant on experience with JavaScript and other web technologies—there are also a ton of options when it comes to frameworks and approaches! This guide will only explore GraphQL basics that are generally applicable.
# Overview
GraphQL is a popular and expressive query language that works well with Craft’s flexible content modeling tools. In most cases, fetching content with GraphQL is analogous to using element queries in Twig—but instead of returning native element objects, the structure and substance of JSON output from GraphQL is determined by the client rather than the server. Craft would ordinarily return all native and custom field data for an element, but GraphQL requires that you explicitly include each property.
# Configure GraphQL
Let’s open the GraphiQL explorer to browse the auto-generated schema and documentation, by clicking GraphQL, then GraphiQL.
GraphiQL (opens new window) is a GUI tool for using the GraphQL query language (opens new window). The former is often mistaken for a typo and the difference is subtle!
Clear out the comments in the query editor and try running a simple query:
{ ping }
You’ll see pong
in the response pane, signaling that everything’s ready to go:
By default, the Craft CMS GraphiQL interface will use the full “schema” without any restrictions—in other words, it has access to all content through the GraphQL API, including mutations. You can switch schemas with the menu at the top of the screen.
To use GraphQL externally (without a privileged user session), you’ll need to do two things:
- Establish a GraphQL API endpoint for querying externally.
- Either create your own private schema (and an access token), or edit the public schema to enable querying content without an access token. (By default, the public schema leaves all content disabled.) See Define Your Schemas in the GraphQL documentation.
# Optional: Enable Headless Mode
If you want to query Craft for content but handle your own routing, you can enable headlessMode. This optimizes the installation and UI, hiding template settings (like how we connected Blog section entries to templates/blog/_entry.twig
), disabling all routes except for the control panel, and returning JSON for all front-end requests.
To enable headless mode, add this to config/general.php
:
<?php
use craft\config\GeneralConfig;
return GeneralConfig::create()
// ...
->headlessMode(true)
;
# Twig examples as GraphQL queries
Content can be fetched via GraphQL with the same parameters we’ve already used in Twig. Ultimately, Craft is still using element queries to decide what data should be loaded.
Let’s retrace each step of the tutorial where we fetched content, seeing how the Twig example maps to a GraphQL query.
# _layout
The base template includes the default Craft CMS language, site name, and site description:
craft.app.language
siteName
siteInformation.siteDescription
These aren’t available via the GraphQL API, but if you needed them you could store each in a global set, like we did for the description:
# blog/_entry
The blog post detail template displays an entire blog post with its full content.
entry.title
entry.postDate
entry.featureImage
title
url
- dynamic
getUrl()
transform at 900×600px
entry.postContent
block.type
block.text
block.image
, an assets field
entry.postCategories
title
url
siteInfo.description
, processed by Markdown
When Craft is in charge of routing, it matches the requested URL against existing entries, then loads and exposes it as entry
in the template. We don’t have that routing mechanism in the GraphiQL IDE, so we’re going to specify the known slug my-trip-to-bend
for this example.
We also used postDate
in two different formats, so we’re using the GraphQL alias (opens new window) postDateAlt
to return a second format for parity.
{
entry(slug: "my-trip-to-bend") {
title
postDate @formatDateTime(format: "d M Y")
postDateAlt: postDate @formatDateTime(format: "Y-m-d")
url
... on post_Entry {
featureImage {
title
url
sized: url @transform(width: 900, height: 600, quality: 90)
}
postContent {
... on text_Entry {
typeHandle
text
}
... on image_Entry {
typeHandle
image {
title
url
}
}
}
postCategories {
title
url
}
}
}
globalSet(handle: ["siteInfo"]) {
... on siteInfo_GlobalSet {
description @markdown
}
}
}
You’ll notice that anything with its own field layout implements its own element interface, which is why properties consistent for all element types (title
, postDate
, slug
, etc.) are readily available while custom fields must be queried in the context of a relevant element interface—like everything nested within ... on post_Entry {}
.
# blog/index
The blog listing page displays a thumbnail icon and summary for every entry.
This is fairly straightforward with GraphQL. We’ll expose the custom focal point as well in case the front end might make use of it:
{
entries(section: "blog") {
title
postDate @formatDateTime(format: "d M Y")
url
... on post_Entry {
summary
featureImage {
title
url
sized: url @transform(width: 300, height: 300)
focalPoint
}
}
}
}
# blog/_category
The blog category listing is the same as the listing layout above, limited to a specific category defined in the page URL.
We’ll use the relatedTo
query parameter, which requires IDs as its arguments. We can relate to the “Trips” category by its ID, which we can get with another GraphQL query or by visiting Categories and finding the numeric ID in the control panel URL.
If the ID for that category is 11
, the listing query could limit results by that relationship:
{
entries(section: "blog", relatedTo: 11) {
title
# ...
}
}
Limiting on more than one category ID, like entries in category 11
or 22
, would be a matter of passing an array:
{
entries(section: "blog", relatedTo: [30, 40]) {
title
# ...
}
}
For more on querying using relationships, see the Relations page in the Craft CMS documentation.
# _singles/about
The about page is a Single, which is sort of a section and an entry and can be queried either way; using the section
or slug
parameter with a value of about
.
entry.profileImage
entry.bio
{
entry(section: "about") {
... on about_Entry {
profileImage {
title
url
}
bio
}
}
}
# Explore GraphQL further
As with the Twig examples, we’re just scratching the surface of ways you can fetch content with Craft CMS.
See the GraphQL API section of the Craft CMS documentation to learn more about working with GraphQL, or install the Element API (opens new window) plugin to start building custom JSON endpoints!