DEV Community

John Mueller
John Mueller

Posted on • Updated on

Using Craft 3 as a headless CMS with GraphQL and Vue Apollo

Using Craft 3 as a headless CMS with GraphQL and Vue Apollo
As a member of the brilliant team at Telegraph, I recently had the opportunity to use Craft in a new way and thought others might find it helpful to read about our process. Our team was tasked with adding functionality to the Neighborhood Goods website to detect a visitor’s location and offer content and products based on the nearest Neighborhood Goods store.

The Setup

Neighborhood Goods relies on Shopify for online sales and inventory management. While we’ve found that Shopify is great at those tasks, it falls short when trying to manage editorial content. Basic blog entries work fine, but when you start to add more complex layouts, events, and location specific content, Shopify just didn’t seem up to the task.

Enter Craft CMS! We are big fans of Craft and while Twig can be handy for templating within Craft, our template files still needed to live within the realm of Shopify’s templates to take advantage of Shopify’s full feature set. This led us to use Craft 3 as a headless CMS which would provide all of our data via a GraphQL API. This was our first time working with the CraftQL plugin and it made setting up a GraphQL schema and managing granular authentication quite easy.

Once CraftQL was set up we turned to Vue (our front end Javascript framework of choice) and Vue Apollo to consume our API within Shopify templates. This made creating ad hoc queries easy for our front end development. For about 90% of our set up everything was ready to go out of the box. We did encounter a few gotchas, and I thought I’d share those here.

Things to Consider

One issue that we encountered with this setup is the way we handle Matrix Fields. Typically if we have a section of a page that could display different types of content we create a Matrix Field with several optional Block Types. For example: if there is a page that displays basic paragraph text, as well as optional hero images or hero text, we’d build out a Matrix Field that looks like this:

Dynamic Matrix Fields

Then, in Twig we can loop through each Block Type and include the proper template. This gives great flexibility for content where you may have one, many or no Blocks of a certain Type and they can be in any order.

    {% if blocks | length %}
        {% for block in blocks.all() %}
            {% switch block.type %}
                {# Article Body #}
                {% case "articleBody" %}
                    {% include '_components/longform-blocks/article-body' %}

                {# Text Hero #}
                {% case "textHero" %}
                    {% include '_components/longform-blocks/text-hero' %}

                {# Image Hero #}
                {% case "imageHero" %}
                    {% include '_components/longform-blocks/image-hero' %}
            {% endswitch %}
        {% endfor %}
    {% endif %}
Enter fullscreen mode Exit fullscreen mode

While testing this structure in CraftQL’s playground everything looked good:

CraftQL Playground

    query {
        entries(id: 3) {
            title
            id
            ...on MatrixExample {
                dynamicContent {
                    ... on DynamicContentBodyText {
                        copy
                    }
                    ... on DynamicContentTextHero {
                        text
                        backgroundColor {
                            hex
                        }
                    }
                    ... on DynamicContentImageHero {
                        image {
                            url
                        }
                        caption
                    }
                }
            }
        }
    }
Enter fullscreen mode Exit fullscreen mode

However, when trying to pull this query in via Vue Apollo, we encountered the following error:

Heuristic Fragment Warning

WARNING: heuristic fragment matching going on!
Enter fullscreen mode Exit fullscreen mode

It turns out that Vue Apollo can handle Matrix fields if there is only one Block Type (Body Text in the example above). If there are multiple Block Types (Body Text and Text Hero, etc.), Vue Apollo warns that it doesn’t know the full schema and can’t predict what type of content it is receiving. After some digging I found that we weren’t the only ones to encounter this issue.

Our workaround for this issue ended up being a combination of deciding the maximum number of sections and creating Text and Image Asset Fields to accommodate, and creating Matrix Fields with optional Fields that could be dual purpose (ie: a Hero Matrix that can accept either Text or an Image).

Matrix Field Workaround

Another option as described in the Github issue is to run a script on the command line (./craft craftql/tools/fetch-fragment-types) which generates a JSON file to include in your Vue Apollo setup. In hindsight, this seems like an easy enough task as long as everyone on your team remembers to run the command after editing Fields in Craft. I would like to try this approach on future projects. Have you used this approach? Do you have any advice on how to integrate it into your development/deployment workflow?


One other thing that wasn’t as straightforward as I had hoped with CraftQL is querying by a field value. When querying entries you can use the slug like so:

    query {
      entries(
        type: MatrixExample
        slug: "first-example"
      ) {
        id
        title
      }
    }
Enter fullscreen mode Exit fullscreen mode

I had hoped that we could do the same thing when querying by a Category Field that is attached to the Entry, but at this point in time you can only query that relationship using the Id:

    query {
      entries(
        type: MatrixExample
        relatedTo: [{element: 36}] # 36 is the Id of our Category
      ) {
        id
        title
      }
    }
Enter fullscreen mode Exit fullscreen mode

Going Forward

Craft 3.3 or later with a Pro license now includes GraphQL functionality. Based on a brief look it appears that the setup is a bit different than with CraftQL. I’m curious to see if there are any benefits in performance/functionality there. Have you used both? Any thoughts on how they compare?

Latest comments (0)