Announcing the WPGraphQL REPL

“What is a REPL?”, “Who are you?”, and “What have you done with Jason?” are all good questions you might be asking. I will start with the most important one. Hello, I am Alex aka “moonmeister”! I have been around for a while and you might have seen me in Slack or chatted with me when I worked at Gatsby or WP Engine. I recently started wpdecoupled.dev where I write about all things decoupled/headless WordPress. I mostly work on the JavaScript side of code and leave the PHP to folks like Jason Bahl and David Levine.

I am happy to announce the initial release of the WPGraphQL REPL. In this post, I will be talking about what it is I built and why I built it.

TL;DR

I built a REPL for WPGraphQL. A REPL helps us accelerate learning, bug finding, and more by allowing you to spin up a working WP instance in the time it takes you to load a web page. You can find it at repl.wpgraphql.com. Use it, love it, share it. There are lots of features yet to build and bugs to find. Go wild, and come help build a better future on the repo.

What’s a REPL?

For those who are not familiar, a REPL (Read-Evaluate-Print Loop) is a really valuable tool for any ecosystem. A REPL can be used to reproduce a bug, teach the basics of the ecosystem, experiment, and more quickly collaborate. Still confused? Let’s dig deeper!

Bugs

Okay, you are building with WPGraphQL and something is not working, you have found a bug. You move from your JavaScript client to GraphiQL in the WP Admin. You see the bug happening there too. You now know not to blame JavaScript. What do you do next? My next step is to replicate the bug in a “minimal reproduction”.

Get it? REPL…replicate…it is an acronym and an abbreviation!

The idea of a minimal reproduction is to eliminate variables. No, not the let foo = "bar"; kind. What PHP, WP, and WPGraphQL are you running? What other plugins are installed and what are their versions? These are all questions that help us track down the source of an issue. The reason maintainers often ask folks to create their own minimal reproductions is the act of doing so helps folks break down an issue, eliminate variables, and solve their own problem.

Given the numerous versions of PHP, WP, and WPGraphQL this can be very time-consuming. Usually, PHP and WP versions can be ignored, but not always. Luckily, we have had Local for years which helps accelerate this process, but it is still quite time-consuming. Inevitably, I end up cowboy coding on my production instance cause it is just faster.

This is where a REPL comes in. If I have a place I can quickly get WP running with WPGraphQL and switch between a variety of versions and test code on the fly, I am going to get to a minimal reproduction and an answer faster.

Experimenting

I have often run into situations where I want to try a new plugin, or just delve into an aspect of WPGraphQL I am not super familiar with. GraphiQL in my WP instance can be helpful here. But once again it is slow to spin up WP, even in Local. I just want something fast so that I can grab the one plugin I need and GO! I want to get back to the building by accelerating the learning process.

Learning/Teaching

There are many situations where I need to share the code with others. This could be and embed in a blog post or just a quick reply to someone trying to figure out WPGraphQL in Slack. Either way, I want a fast and controlled way to write code and hand it to them in a way they can see, understand and start their own experimentation.

Are you seeing the theme…speed. This is what REPLs provide us…it gets us away from “it works on my machine”…it gets us away from Docker hell…it gets us to what we really need, a reproducible and flexible environment where I can focus on my specific issue and not worry about my LAMP stack, breaking production, or anything other than the issue at hand.

My Dream Realized

For as long as I can remember, I have wanted a REPL for the WPGraphQL ecosystem. I knew it was theoretically possible but practically, I did not have the skills to implement it. Then I saw an announcement from the WordPress community on https://developer.wordpress.org/playground/.

My dream was possible. I no longer needed to learn how to run PHP and WP in WASM, I could just build the UI and control interface to the specific needs of the WPGraphQL community. After a couple of 4 am coding sessions, I had something I could conceive of sharing. Several weeks of random coding sessions later and an initial release is here.

Getting Started

After navigating to https://repl.wpgraphql.com WP will load within ~20 seconds. First to load will be the familiar GraphiQL instance that comes with WPGraphQL. On the left panel, you are currently able to navigate to a new URL and change the running PHP and WP versions.

Currently, the state (db and filesystem) within WP is ephemeral and will be lost with a page load. Work is being done by the WordPress Playground team to persist the state across refresh. If you want to save your state or load your configured WP into a LAMP stack outside of the browser you can use the download button in the upper right to download a zip of your site. I hope to have an upload button soon to be able to reload that same file back in later.

Please, go check out https://repl.wpgraphql.com, I hope you come to value a good REPL as much as I do. Enjoy! If you run into missing features or bugs please head over to the Github repo and open issues, request features, and contribute code to make this better! We also have a #repl channel on the WPGraphQL Slack!

Customizing GraphQL Cache Keys

WPGraphQL Smart Cache is quickly becoming the standard solution for fast and accurate data when using WPGraphQL for decoupled applications. Since launching in December 2022, it’s grown to 500+ active users!

While it solves a lot of problems for a lot of users, it’s not perfect and might require customization.

In this blog post, we’re going to look at how to customize the keys that are returned with GraphQL responses, which customizes how the GraphQL documents are tagged in the cache, and thus customizes what events will purge the cached document.

How WPGraphQL Smart Cache Invalidation works

Before we dive into customizing WPGraphQL Smart Cache keys, let’s take a look at how caching and cache invalidation with WPGraphQL Smart Cache works.

Understanding how GraphQL Queries are Cached and “Tagged”

When making a query to a WPGraphQL-powered endpoint, WPGraphQL will return headers that caching clients can use to tag the cached document with.

To see this in action, I will use a local WordPress install (powered by Local). In this WordPress install, I have the following plugins installed and activated:

In the WordPress dashboard, I will navigate to the GraphiQL IDE.

I will open Chrome’s dev tools and select the “Network” tab.

Then, in the GraphiQL IDE, I will execute the following query:

query GetPosts {
  posts {
    nodes {
      title
      uri
    }
  }
}

And I see the following response:

Screenshot of the GraphiQL IDE executing a query for posts and showing the JSON response from the query.

With the Network tools open, we can click on the request and inspect the headers:

Here, we see the X-GraphQL-Keys header with a list of keys. If this query were made via a GET request to a site hosted on a host that supports it, the response would be cached and “tagged” with these keys.
For this particular query, we see the following keys:

  • 4382426a7bebd62479da59a06d90ceb12e02967d342afb7518632e26b95acc6f (hash of the query)
  • graphql:Query (operation type)
  • operation:GetPosts (operation name)
  • list:post (identifies the query asked for a list of posts)
  • cG9zdDox (id of the node(s) resolving. In this query response, only 1 node was resolved)

The document will be tagged with these keys, and in turn will be purged (deleted) from the cache if a purge event for one of those tags is triggered.

Understanding Cache Invalidation with WPGraphQL Smart Cache

Above, we looked at how supported hosts will cache GraphQL responses and use the X-GraphQL-Keys to “tag” the cached document.

Now let’s take a look at how WPGraphQL Smart Cache invalidates, or “purges” these tagged documents from the cache.

WPGraphQL Smart Cache listens to events that occur in WordPress, and in response to these events, calls “purge” on specific “tags”, deleting any document from the cache that was tagged with said tag.

The simplified summary of the WPGraphQL Smart Cache invalidation strategy is as follows:

  • Publish events call purge( 'list:$type_name' )
  • Update events call purge( '$nodeId' )
  • Delete events call purge( '$nodeId' )

How Cache Invalidation and Cache Tags work together

Now that we have an idea of how cached documents are tagged, and how events in WordPress call “purge” on different tags, let’s bring it all together.

Above we identified the query keys that the document will be tagged with. And now that we understand the cache invalidation strategy, we know that the GetPosts query will be purged from the cache whenever the following events occur:

  • A new post is published ( purge( 'list:post' ) )
  • The node identified by id cG9zdDox (the “hello world” post) is updated or deleted
  • Clicking “purge cache” in the GraphQL > Settings > Cache page (`purge( ‘graphql:Query’ )` )
  • Any purge event manually added to purge based on operation name or query hash

So far, so good! This all seems reasonable. I think we would probably all want the cache for this query to be invalidated after we updated the “Hello World” post. We would want to see the updated data in the query response.

The Problem

The problem, however, is that the headers that are added to the documents might be more broad than we would like, and we might need to customize this.

For example, let’s take a look at a more complicated query:

query GetPostsWithCategoriesAndTags {
  posts {
    nodes {
      id
      title
      categories {
        nodes {
          id
          name
        }
      }
      tags {
        nodes {
          id
          name
        }
      }
    }
  }
}

In this query, we’ve asked for a list of posts, a list of categories belonging to each post and a list of tags belonging to each post.

In the response, we see that we have Posts, categories and tags (only 1 of each in this case, as we’re on a simple test site with minimal data). And if we inspect the X-GraphQL-Keys header, we will see that it’s tagged with list:post, list:category and list:tag. This is because we asked for each of these types of nodes and when a new post, category or tag is made public, it could be part of the list, so we invalidate this cache. And like magic, we could query this again and get a Cache Miss and see fresh content, like the newly published post.

This all sounds good still, but there is a problem.

The problem is that the list:category and list:tag keys will cause this document to be purged a lot more than it should be.

For example, creating a new tag and assigning it to a post that’s not shown in this query’s results will trigger the purge( 'list:tag' ) and invalidate this query, leading to more cache invalidations than we probably want.

Ideally WPGraphQL and WPGraphQL Smart Cache will be able to better identify when / when not to output the list:$type keys, but for now this is how things work, but there are ways to override it for your specific needs.

Filtering the X-GraphQL-Keys

If we want the GetPostsWithCategoriesAndTags query to NOT output the list:tag and list:category keys, we can filter the keys like so:

// Filter the keys that are returned by the Query Analyzer
add_filter( 'graphql_query_analyzer_graphql_keys', function( $graphql_keys, $return_keys, $skipped_keys, $return_keys_array, $skipped_keys_array ) {
 
        // Convert the keys from a string to an array
	$keys_array = explode( ' ', $return_keys );

        // Only apply the filter to the "GetPostsWithCategoriesAndTags" query
	if ( empty( $keys_array ) || ! in_array( 'operation:GetPostsWithCategoriesAndTags', $keys_array, true )  ) {
		return $graphql_keys;
	}

        // Remove the "list:tag" key from the headers
	if ( ( $key = array_search('list:tag', $keys_array ) ) !== false ) {
		unset( $keys_array[$key] );
	}

        // Remove the "list:category" key from the headers
	if ( ( $key = array_search('list:category', $keys_array ) ) !== false) {
		unset( $keys_array[$key] );
	}

        // Convert the array of keys back to a space separated string
	$graphql_keys['keys'] = implode( ' ', $keys_array );
	
        // Return the "filtered" $graphql_keys
	return $graphql_keys;

}, 10, 5 );

In the snippet above, we target a specific query with the operation name “GetPostsWithCategoriesAndTags” and for that query we remove the list:tag and list:category keys from being returned.

Now, we can execute the same query and inspect the headers, and we will see that list:tag and list:category are both no longer output in the X-GraphQL-Keys header.

This means that publishing new categories and tags, which triggers purge( 'list:category' ) and purge( 'list:tag' ) will not purge this document.

Success!

Now we’re getting the benefits of cached GraphQL documents, and we’re getting the document invalidated when the post is updated or deleted, but we’re letting the cache remain cached when categories or tags are created.

Conclusion

While we ultimately believe WPGraphQL Smart Cache should be, well, smarter, it might take some time to find that perfect solution that works for everyone.

In the mean time, using filters like demonstrated above, can help you tailor your WPGraphQL cache tagging and invalidation strategy to fit your specific project’s needs.

Introducing WPGraphQL Smart Cache

WPGraphQL is fast becoming the standard for building decoupled WordPress websites and applications. Most of the official WordPress boilerplates for JavaScript frameworks such as NextJS and Gatsby use WPGraphQL as the data source..

Developers are choosing WPGraphQL for decoupling their projects because of the flexibility it provides, the control it gives the client developer, the type safety, the tooling and more. The developer experience of using WPGraphQL is pretty nice.

One pain point, however, is the lack of caching.

In this post, we will go over some of the performance problems developers have ran into when using WPGraphQL, and the solutions.

The Problem: WordPress Server Timeouts

The most common way to interact with a GraphQL API is via HTTP POST requests. Most servers do not cache HTTP POST requests, meaning a POST request to WordPress will make its way to the WordPress server to be processed and returned. This requires the WordPress server to process the request and return a response.

This requires WordPress to load, execute the WordPress “bootstrap” process, load plugins, run actions and filters, load options, check for authentication and authorization, then execute the resolvers of the GraphQL request. This can cause scaling issues.

When building decoupled sites with WPGraphQL, using HTTP POST requests as the primary method of fetching data can cause your WordPress server to be overwhelmed. WebDevStudios wrote about this problem and a lot of Gatsby users have experienced this problem as well.

When a WordPress server is overwhelmed with HTTP POST requests, the server can become exhausted and run into performance issues and server timeouts.

Solution: Fetch Less

While this article is announcing WPGraphQL Smart Cache v1.0, I want to point out another solution that can work well, too.

I’ve worked with many WPGraphQL users to improve the performance of their decoupled site build times.

One of the easiest solutions to implement, is to fetch less data from WPGraphQL.

Several NextJS WordPress starter projects have popularized patterns of fetching everything from WPGraphQL in order to build static pages. This is one of the main culprits of performance issues, including the one WebDevStudios wrote about.

I’ve explained at length why this pattern of fetching data should be corrected.

The idea is that instead of fetching EVERY post and page and statically generating each page during every build of your decoupled application, you would instead only fetch what you need to build the most important pages at build time, and let the rest of the pages be built at run time, incrementally regenerated when possible.

So, instead of fetching every post and page and generating static pages for every single url at build time, we would use the WPGraphQL nodeByUri query to fetch the node for the uri at run time and incrementally build static pages at run-time, then re-generate them using ISR.

I also paired with Colby Fayock to refactor his Headless WordPress Starter project to use NextJS Incremental Static Regeneration (pull request here), leading to far fewer requests to the WordPress server.

This leads to faster build times, much fewer requests to the WordPress server, and a better experience for content creators publishing in WordPress as their changes will be live on the decoupled front-end much faster. (see a demo of publishing content on WPGraphQL.com)

By implementing Incremental Static Regeneration with the nodeByUri query, I’ve helped customers on WP Engine Atlas get their NextJS build times from more than 5 minutes, to less than 20 seconds!

Solution: WPGraphQL Smart Cache

WPGraphQL Smart Cache is a free open-source WordPress plugin that brings support for Network Cache, Object Cache, Cache Invalidation and Persisted Queries to WPGraphQL.

Below we’ll look at how WPGraphQL Smart Cache can greatly reduce response times from your WPGraphQL API and reduce build times of your decoupled sites.

You can use WPGraphQL Smart Cache and benefit greatly even if you don’t / can’t implement Incremental Static Regeneration as discussed in the section above.

Below we will look at how WPGraphQL Smart Cache helps reduce build times and alleviates pressure on your WordPress server.

Network Cache

When I refer to “Network Cache” I’m referring to caching when using HTTP GET requests.

WPGraphQL has supported making GraphQL queries via HTTP GET requests for several years. And many WordPress hosts already support HTTP caching of WPGraphQL requests made over GET requests.

The way Network cache works, is that there’s a caching layer between the client application requesting data, and the WordPress server itself.

The client application will send the GraphQL Query over HTTP GET. The first time the query is sent, this will be a “cache miss” and the request will make its way to the WordPress server for execution. WordPress will load up and process the request, executing all of the resolvers for the GraphQL query and a response will be returned.

The response will then be cached in a network cache layer (typically Varnish or similar). Whatever the layer is, if your host supports network caching, the request will be cached there.

Then, the next time the same query (and same variables) is executed, the response will be served from the cache instead of sending the request all the way to WordPress for execution.

Let’s take a look at this in action.

HTTP GET: Cache Miss

Below is a screenshot of an HTTP GET request, with the Chrome Developer tools open, highlighting the request in the “Network” tab. The first time the query is executed, it will be a Cache MISS. We can see this in the the response headers. The request took 304ms to respond.

HTTP GET: Cache Hit

Now, if we refresh the browser, the request will be returned from the network cache, and my WordPress server won’t be hit at all.

Here we can see that after refreshing multiple times, we’re getting the response in 53ms, and the response headers indicate the response is a cache hit.

We can keep refreshing, and keep getting responses from the cache.

If you had a client-side component on a page that was executing this query with HTTP GET, and the page received 10,000 visitors at the same time, 9,999 of those visitors would get a response from the network cache, instead of the WordPress server.

By switching your GraphQL client from using POST requests to using GET requests, you can start taking advantage of HTTP caching and your server will immediately have a reduced burden.

Cache Invalidation

Caching is great, but that’s only one part of the picture. The other part is Cache Invalidation, and this is where WPGraphQL Smart Cache really shines.

Stale Data

If all you do is switch from HTTP POST requests to HTTP GET requests, if your host supports network caching for GraphQL queries, you will benefit from faster responses and a reduced load on your WordPress server, but the cached responses will remain cached for a period of time, determined by the max-age headers of the response.

In this screenshot we see the “cache-control” header is set to 600 seconds. That means that for 10 minutes the response will be cached. Any requests for this query in the next 10 minutes will get a cached response, even if these posts were edited or new posts were published in those 10 minutes.

This isn’t ideal.

When building decoupled applications, we want the GraphQL response times to be fast, but we also want the data to be accurate.

Auto purging caches when data changes

WPGraphQL Smart Cache listens for publish, update and delete events in WordPress, and then emits a “graphql_purge” action to purge caches related to the object that was modified.

The eviction strategy is as follows:

  • publish: purge( ‘list:$type_being_published )
  • update: purge( $id_of_node_being_updated )
  • delete: purge( $id_of_node_being_deleted )

This is more nuanced than simply calling purge in response to actions like “save_post”.

We’ll discuss that nuance below.

Publish Events

There’s a lot of nuance around what is considered “Public” vs “Private” and WPGraphQL Smart Cache takes this into consideration.

Creating a new post isn’t automatically considered a “publish” event. New posts are created in many states, such as “draft” or “auto draft”, and draft posts aren’t considered public, and therefore wouldn’t be in responses of public queries that would be cached.

Transitioning a draft post to become a published post, however, would convert a post from a non-public state to a public state, and this would trigger WPGraphQL Smart Cache to call “purge( “list:post” )“.

If the post being published was a page, it would call “purge( “list:page” )” and if it were a custom post type it would call “purge( “list:$graphql_type_name_for_the_cpt” )“.

When it comes to other data, such as users and menus, there’s even more nuance.

Users in WordPress are not considered public entities by default.

If you create a user in WordPress, you must login to WordPress to know that user exists. However, once content is published with that user as the author, that user becomes a public entity, with an archive page, a REST API endpoint, etc.

WPGraphQL Smart Cache treats this scenario as a publish event. So, when a User becomes the author of their first publicly visible post, this would trigger “purge( “list:user” )“, evicting any queries that asked for a list of users.

Menus are similar, in that Menus that are not assigned to a Menu Location are considered non-public entities in WordPress. Once a Menu is assigned to a location, that would trigger “purge( “list:menu” )” and any queries that asked for a list of menus should be evicted.

Update Events

Update events are a bit more straightforward than publish events.

If an object is considered public (published post, user with published content, menu assigned to a location, etc), and that object is updated, it will trigger “purge( $id_of_object )“. This would evict any cached queries that had that “node” in its results.

Delete Events

Delete events are as nuanced as Publish events.

Simply listening to the “delete_post” hook isn’t enough. We need to know when posts transition from a public state to a non-public state to be considered a “delete” event.

If a GraphQL query returned a post or a list of posts, and one of those posts was converted from “publish” back to “draft”, that would be considered a “delete” event and any caches with that post would be evicted. This would trigger “purge( $post_id )“. Of course, if a published post was moved to the trash or force deleted, this would also trigger.

If a “draft” post is moved to the trash, this event is not triggered because it would be deleting something that’s not considered public, and therefore not in cached responses that would need purging.

Supported Hosts & Hosting Guide

In order for Network Cache invalidation to work, this requires some implementation details supported by the WordPress host.

When WPGraphQL returns a response, it contains an “X-GraphQL-Keys”.

This is a list of keys relevant to the query and the response. The list of keys contains information about what types of nodes were querying as a list as well as the individual node IDs.

When the host caches the response, it needs to be able to use these keys to “tag” the cached document.

Then, the host needs to be able to purge cached documents when that tag is purged.

WP Engine is the first host to fully support WPGraphQL Smart Cache. You can give it a try by signing up for a free sandbox account.

If you are a WordPress host that wants to support WPGraphQL Smart Cache, or you want to tell your host to support it, we have a hosting guide.

Object Cache

WPGraphQL provides support for object cache of GraphQL queries. This works similar to network cache, but instead of being cached at a level above WordPress, its cached within WordPress object cache layers.

This does require WordPress to load and retrieve the cache, so there’s more overhead than network cache, but it still reduces the overall processing required to return results for a GraphQL query.

If your WordPress host supports persistent object cache, such as memcached or similar, the cache will be stored there. If not, it will fall back to transients.

WPGraphQL Smart Cache does not enable Object Cache by default. To enable it, you need to visit the settings page under “GraphQL > Settings > Cache” and check the option to “Use Object Cache”.

With this enabled, GraphQL queries made using HTTP POST requests can benefit from caching. This can come in handy if your client application can’t use GET. For example, if you had developed a native iOS application, even if you wanted to switch to GET, the application can’t be changed until the user downloads an update.

From this settings page, you can also purge the GraphQL Object cache.

The invalidation of the object cache works the same as the invalidation for the network cache, but doesn’t require specific implementation details from your host.

When possible, we recommend using GET requests to take advantage of HTTP / network cache as it will lead to faster responses and greater processing reduction on your WordPress server.

Persisted Queries

Persisted Queries is a feature provided by WPGraphQL Smart Cache that is often used together with Network cache.

GraphQL documents can get quite long, so using GET requests for some queries isn’t realistic as there are query string length limitations.

Persisted Queries help avoid this problem.

With persisted queries, you can store GraphQL Query documents on the WordPress server, then make HTTP GET requests referencing the query by ID, instead of passing the full query string in the URL.

The Admin User Interface for Persisted Queries is hidden by default (we will likely be iterating how the UI in the admin looks and behaves, so don’t get married to it’s current state).

To show the UI, you can visit “GraphQL > Settings > Saved Queries” and check the option to “Display saved query documents in admin editor”.

This adds a “GraphQL Documents” menu item in your WordPress Dashboard.

From this User Interface, you can manage GraphQL Documents.

There’s a list view of the documents:

And an individual document editor where you an add the document, provide an optional description, assign aliases (ids), set the allow/deny rule, and customize the max-age header.

Once a document is saved, it can by queried by id.

For example, in the screenshot above, the ID of the document is: “b5f0fe422d78ffdfc7a43206693a8268d2fe368ec0f7d737e778efd5a3a7a08d”. That can be used in a GET request as the value of the “queryId” parameter:

In the screenshot we can see an HTTP GET request to the GraphQL endpoint using the “alias” as the value of the “queryId” parameter, and the results of the referenced Query Document being returned.

Automatic Persisted Queries (APQ)

Automatic Persisted Queries is a technique made popular by Apollo and is supported by WPGraphQL Smart Cache.

The way this works, is that the client application will try and use HTTP GET requests to fetch queries by ID (a hash of the query). The server responds with the results of the query if it can. If the ID is not recognized, the server returns a specific error, and the client follows-up with an HTTP POST request with the full query document and the query ID. The server then stores the GraphQL document and associates it with the ID, and future HTTP GET requests referencing the ID will execute the document.

This technique is implemented and active on WPGraphQL.com.

Allow / Deny Lists

One benefit of having GraphQL Documents saved on the server, is the ability to define allow and deny lists.

By default the WPGraphQL API allows arbitrary execution of queries. Sometimes you might want to disable that and only allow queries you’ve pre-defined.

To restrict the GraphQL endpoint to only execute allowed queries, you can visit “GraphQL > Settings ?> Saved Queries” and select the “Allow only specific queries” value for the “Allow/Deny Mode” setting.

With “allow” mode enabled, arbitrary queries will not be executed. Instead, an error will be returned.

One workflow teams can implement is as follows:

Front-end teams work on the application, writing all the queries they need for the application. Once the front-end team is ready, they generate a list of queries needed for the application and save them in the production environment as GraphQL Documents marked as “allowed”.

Then the production app can execute the queries against the server, while preventing arbitrary queries from being executed.

Conclusion

WPGraphQL Smart Cache is a new and exciting piece of the WPGraphQL ecosystem. We believe that the features provided by WPGraphQL Smart Cache will help teams scale their use of decoupled WordPress and unlock new possibilities for what can be built with this stack.

We’d love to hear what you build with WPGraphQL and WPGraphQL Smart Cache!

You can get the plugin here: https://wordpress.org/plugins/wpgraphql-smart-cache.

If you enjoy the plugin, please leave a review.

New Post Type and Taxonomy Registration options

WPGraphQL makes it easy to expose Custom Post Types and Taxonomies to the GraphQL Schema.

WPGraphQL v1.12.0 introduces new options for how your post types and taxonomies are added to the WPGraphQL Schema.

If you’re already familiar with how registering post types and taxonomies to GraphQL works, go ahead and skip to the new options or read the tutorial. But if you are new to it or want a refresher, keep reading.

Registering Post Types and Taxonomies to show in the WPGraphQL Schema

When registering a custom post type or taxonomy to WPGraphQL, there are 3 required arguments to add:

  • show_in_graphql | boolean | (required) : Whether to expose this post type or taxonomy to the GraphQL API.
  • graphql_single_name | string | (required): The GraphQL Type name that will represent nodes of this post type or taxonomy.
  • graphql_plural_name | string | (required) : The plural name of the GraphQL Type for use in connection field names.

With these 3 properties defined in the registration of a post type or taxonomy, a lot of things are added to the GraphQL Schema.

Let’s dive in!

For this example, we will start with a “testimonial” post type.

We could register the post type like so:

add_action( 'init', function() { 

  register_post_type( 'testimonial', [
    'public' => true,
    'label' => __( 'Testimonials', 'wp-graphql' ),
    'show_in_graphql' => true, # tells the post type to show in the GraphQL Schema
    'graphql_single_name' => 'Testimonial', # determines the GraphQL Type name for nodes of this post type
    'graphql_plural_name' => 'Testimonials', # determines the field name for connections to nodes of this post type
  ] );

} );

By adding these 3 lines of code (show_in_graphql, graphql_single_name, graphql_plural_name) to our post type registration, we are now able to query and mutate posts of the “testimonial” post type using GraphQL.

Let’s take a look at how these 3 lines affected the WPGraphQL API.

Schema

If we were to open the GraphiQL IDE documentation explorer and search our GraphQL Schema for “testimonial” (before setting the post type to show_in_graphql) we would see no results, like so:

Screenshot of GraphiQL Schema Docs with no results for “testimonial” search

If we search for “testimonial” (after setting the post type to “show_in_graphql”) we will see many different Types and Fields in the Schema.

Screenshot of GraphiQL Schema Docs with default results for “testimonial” search

Let’s break down exactly what was added to the GraphQL Schema.

Types & Fields

In the screenshot above, the gold-colored words represent GraphQL Types, and the blue-colored words represent fields.

By registering the post type to show_in_graphql, the schema now has 12 new GraphQL Types and 10 new fields, all for interacting with posts of the “testimonial” post type. (these numbers might differ in your system based on different conditions, but the point is, you should have a lot of new Types and Fields with minimal effort).

The GraphQL Types define the shape of objects in the graph, and the fields are the entry points into the graph for accessing data.

Let’s take a look more specifically at the Queries and Mutations provided by exposing this post type to the GraphQL Schema.

I’m not going to break down every single Type and Field that was added to the schema, but we’ll take a look at some of the main ones.

Queries

If we take a look at the fields that were added to the RootQuery type, we can see a few different ways we can now query for content in our “testimonial” post type.

  • RootQuery.testimonial: This allows for individual Testimonial nodes to be queried by id.
  • RootQuery.testimonialBy (deprecated): This is a deprecated version of the previous field.
  • RootQuery.testimonials: This allows for lists (called connections in GraphQL) of testimonials to be queried.

Mutations

If we look at the fields added to the RootMutation, we will see the following mutations:

  • RootMutation.createTestimonial
  • RootMutation.updateTestimonial
  • RootMutation.deleteTestimonial

These mutations allow for content of the testimonial post type to be created, updated and deleted via GraphQL mutations.

GraphQL Mutations respect the read and write privileges defined for each post type, so only authenticated users with proper capabilities can successfully execute these mutations.

New post type and taxonomy registration options

Now that we understand how registering post types and taxonomies to WPGraphQL works, we’ll take a look at the new options introduced in WPGraphQL v1.12.0 and see how they work.

Below are the new arguments that can be passed to register_post_type and register_taxonomy to provide further customization of how it will be represented in the GraphQL Schema:

  • graphql_kind: Allows the type representing the post type to be added to the graph as an object type, interface type or union type. Possible values are ‘object’, ‘interface’ or ‘union’. Default is ‘object’.
  • graphql_resolve_type: The callback used to resolve the type. Only used if “graphql_kind” is set to “union” or “interface”.
  • graphql_interfaces: List of Interface names the type should implement. These will be applied in addition to default interfaces such as “Node”.
  • graphql_exclude_interfaces: List of Interface names the type should not implement. This is applied after default and custom interfaces are added, so this can remove default interfaces.
    • Note: Interfaces applied by other interfaces will not be excluded unless that interface is also excluded. For example a post type that supports “thumbnail” will have NodeWithFeaturedImage interface applied, which also applies the Node interface. Excluding “Node” interface will not work unless “NodeWithFeaturedImage” was also excluded.
  • graphql_fields: Array of fields to add to the Type. Applied if “graphql_kind” is “interface” or “object”.
  • graphql_exclude_fields: Array of fields names to exclude from the type. Applies if “graphql_kind” is “interface” or “object”.
    • Note: any fields added by an interface will not be excluded with this option.
  • graphql_connections: Array of connection configs to register to the type. Only applies if the “graphql_kind” is “object” or “interface”.
  • graphql_exclude_connections: Array of connection field names to exclude from the type. Only connections defined on the type will be excluded. Connections defined on an Interface that is applied to the Type remain even if “excluded” in this list.
  • graphql_union_types: Array of possible types the union can resolve to. Only used if “graphql_kind” is set to “union”.
  • graphql_register_root_field: Whether to register a field to the RootQuery to query a single node of this type. Default true.
  • graphql_register_root_connection: Whether to register a connection to the RootQuery to query multiple nodes of this type. Default true.
  • graphql_exclude_mutations: Array of mutations to prevent from being registered. Possible values are “create”, “update”, “delete”.

For more information, check out the official documentation for adding Custom Post Types and Custom Taxonomies to the WPGraphQL Schema.

And for a more in-depth look at these options, check out the tutorial where we walk through multiple scenarios for managing “food” in the CMS and querying via WPGraphQL.

Tutorial: Registering a Custom Post Type as a GraphQL Interface

WPGraphQL v1.12.0 introduces new options for customizing how Post Types and Taxonomies are exposed to the WPGraphQL Schema. The goal of this post is to show how to use some of these options in action. You can read this post for an overview of all the options, or check out the release..

To do that, we’re going to explore a few approaches to solving the same problem.

Imagine that we’re working on a project for a restaurant that sells food. More specifically, they only sell Pizza and Cake.

The restaurant needs to be able to enter the different types of pizza and cake they have available.

For simplicity sake, we’ve determined that each all food has some properties, but each type of food also has unique properties.

All cake is food, but not all food is cake. All pizza is food, but not all food is pizza.

  • All food: has a title and a price
  • Pizza: has toppings.
  • Cake: has frosting color.

The goal of this exercise is to configure WordPress to be able to maintain this data in the CMS, and expose it in the WPGraphQL API.

We would like to be able to query for a list of food, asking for the common fields like “title” and “price”, but also specifying the unique fields, such as “frostingColor” for cake and “toppings” for pizza.

We want to be able to execute the following GraphQL Query:

query GetFood {
  allFood {
    nodes {
      __typename
      id
      title
      price
      ...on Cake {
        frostingColor
      }
      ...on Pizza {
        toppings
      }
    }
  }
}

And in response, we’d like to get data like so:

{
  "data": {
    "foods": {
      "nodes": [
        {
          "__typename": "Cake",
          "title": "Cake",
          "price": "$15.99",
          "frostingColor": "white"
        },
        {
          "__typename": "Pizza",
          "title": "Pepperoni Pizza",
          "price": "$10.99",
          "toppings": [
            "pepperoni",
            "sauce",
            "cheese",
            "jalapenos"
          ]
        }
      ]
    }
  }
}

There are probably many ways we could achieve this.

In this post, I’ll show 2 different ways.

In both scenarios, the query above should work. We should be able to query a list of food, asking for common fields, and also asking for unique fields for Pizza and Cake.

Neither of these options is the “right” one. This is a simple example missing a lot of nuanced details that you would likely have in a real project. The goal of this post is not to prescribe some type of optimal information architecture using WordPress post types, the goal is to show the flexibility and options for exposing WordPress data to the WPGraphQL API.

Let’s dive in.

Scenario 1: One “food” post type

In this scenario, we’ve decided to register a “food” post type where we can enter data for different food items.

Register the Post Type

To get started we will register the post type (without thinking too much about WPGraphQL).

add_action( 'init', function() {

  $args = [
    'public' => true,
    'label' => 'Food',
    'supports' => [ 'title', 'editor', 'custom-fields' ]
  ];

  register_post_type( 'food', $args );

} );

Here we’ve registered a public “food” post type with support for title, editor and custom fields.

At this point, we should have a Post Type in WordPress where we can start entering our Cakes and Pizzas into the CMS.

Add some pizza and cake

Click “Add new” and enter the following:

  • Title: Pepperoni Pizza
  • Description: Yum!
  • Custom Fields:
    • food_type: pizza
    • price: $10.99
    • topping: cheese
    • topping: pepperoni
    • topping: sauce

The pizza should look like so:

Screenshot of the "Edit Post" screen for the "food" post type showing data entered for a "pepperoni pizza"

NOTE: For simplicity sake, I’m demonstrating with WordPress’s built-in custom fields, but on a real project I would likely use Advanced Custom Fields.

Now let’s enter information for a Cake.

Click “Add new” and enter the following information:

  • Title: Chocolate Cake
  • Description: Delicious!
  • Custom Fields:
    • food_type: cake
    • price: $15.99
    • frosting_color: white

The cake should look like so:

Screenshot of the "Edit Post" screen for the "food" post type showing data entered for a "pepperoni pizza"

So now we have our post type setup and some food entered, how can we query this food in GraphQL?

Show the Post Type in GraphQL

If you’ve already familiar with WPGraphQL, you might know that exposing a Post Type to the WPGraphQL Schema requires 3 fields on the post type registration:

  • show_in_graphql: true/false
  • graphql_single_name: Name of the Type in the GraphQL Schema (no spaces or special characters. Value must be unique in the Schema)
  • graphql_plural_name: Plural name of the Type, used for querying lists of items. (no spaces or special characters. Value must be unique in the Schema)

Let’s update our post type registration above to look like the following:

add_action( 'init', function() {

  $args = [
    'public' => true,
    'label' => 'Food',
    'supports' => [ 'title', 'editor', 'custom-fields' ],
    'show_in_graphql' => true,
    'graphql_single_name' => 'Food',
    'graphql_plural_name' => 'Food',
  ];

  register_post_type( 'food', $args );

} );

By adding these 3 lines:

'show_in_graphql' => true,
'graphql_single_name' => 'Food',
'graphql_plural_name' => 'Food',

The data in the “food” post type is now exposed in the WPGraphQL Schema.

We can open up the GraphiQL IDE and search for “food” and see all the ways it shows in our Schema now:

We have a lot of new Types and Fields added to the Schema.

Since our goal was to query a list of “allFood” we can see that a “RootQuery.allFood” field now exists in the Schema.

NOTE: Since the “graphql_single_name” and “graphql_plural_name” were both “food” WPGraphQL adds the “plural” field as “allFood”. If the “graphql_plural_name” was a different value, such as “foodItems” it would add a field with that value instead.

At this point, we can write a query to query a list of food, like so:

query GetFood {
  allFood {
    nodes {
      __typename
      id
      title
    }
  }
}

And we’ll get a response like so:

{
  "data": {
    "allFood": {
      "nodes": [
        {
          "__typename": "Food",
          "id": "cG9zdDoxMjY4",
          "title": "Chocolate Cake"
        },
        {
          "__typename": "Food",
          "id": "cG9zdDoxMjY3",
          "title": "Pepperoni Pizza"
        }
      ]
    }
  }
}

Here’s what it looks like executed in the GraphiQL IDE:

One thing you might have noticed, is that we have only queried for 3 fields on each “node”:

  • __typename
  • id
  • title

Our goal was to be able to also query “price” for all food items, and then specify that we want “toppings” for Pizza and “frostingColor” for Cake.

If we look at our Schema docs in GraphiQL, we won’t find any mention of “price”, “toppings”, “frostingColor”, “pizza” or “cake”.

Gif screen recording showing empty results when searching for “price”, “toppings”, “frosting”, “pizza” and “cake” and getting no results.

Add the “price” field to the Schema

Since we agreed that all “food” should have a price field, we can add this to the Schema using the register_graphql_field function.

Let’s use the following snippet to add this field to the Schema:

add_action( 'graphql_register_types', function() {

  register_graphql_field( 'Food', 'price', [
    'type' => 'String',
    'description' => __( 'The cost of the food item', 'your-textdomain' ),
    'resolve' => function( $food ) {
      $price = get_post_meta( $food->databaseId, 'price', true );
      return ! empty( $price ) ? $price : null;
    }
  ] );

} );

Let’s break down what this code is doing:

First, it hooks into the “graphql_register_types” action. This ensures the code will only execute when the GraphQL Schema is being built. Most requests to WordPress aren’t GraphQL requests, so no need to execute GraphQL related functions unless necessary.

Next, we call the register_graphql_field function. The first argument is the GraphQL Type we want to add a field to. And the 2nd argument is the name of the field to add. We passed “Food” as the first argument and “price” as the 2nd argument because we want “Food” to have a “price” field. The third argument is an array to configure the field.

In this config array we define the “Type” of data the field will return. We’ve opted for “String” as the type of data we will return for the “price” field. This is the contract between the API consumer and the server. We’ve agreed now that whenever the “price” field is asked for, it will either be a “null” value, or a string. This field will not return an array or an object or an integer or any other type of data.

Next, we added a description, which shows in tools like GraphiQL. This can be helpful to describe to consumers of the API what a field is meant to be used for.

And last, we define our “resolve” function.

The resolver will get the resolving object passed to it. Since we’ve registered a field to the “Food” type, this means we will get “Food” objects passed to the field.

In WPGraphQL the objects passed down are typically “GraphQL Model” objects. In this case, the resolver will get a GraphQL\Model\Post object passed to the resolver.

We use the databaseId from the model to get the value of the “price” post_meta. If there is a value, return it. Otherwise, return a “null” value.

Now, with this field registered to the Schema, we can search GraphiQL again for “price” and we should find it in the Schema:

At this point, we can update our query to query for the “price” field and we should see the values we entered as custom fields returned.

Screenshot of a query for allFood including the price field.

Differentiate types of food

One other thing you might be noticing is that the response for the “__typename” field is “Food”, but in our goal we were hoping to have the values be “Cake” and “Pizza”.

Additionally, we wanted to be able to query for “toppings” if the type of food is Pizza, and “frostingColor” if the type of food is Cake.

How do we do this?

We will convert the “Food” type to be a GraphQL Interface, then register new GraphQL Object Types for “Pizza” and “Cake” that implement the “Food” Interface.

Let’s take a look:

Make “Food” an Interface

According to the GraphQL Documentation “Interface is an abstract type that includes a certain set of fields that a type must include to implement the interface”.

In practice, this means that multiple Unique Types can have a common bond.

All Pizza is Food, but not all Food is Pizza. All Cake is Food, but not all Food is Cake.

We want to be able to define unique Types of Food (in our case Pizza and Cake), but let them implement the “Food” interface.

We can update our post type registration to make the “Food” type an “Interface” by adding this line:

$args = [
 ... // existing args
 'graphql_kind' => 'interface',
 'graphql_resolve_type' => function( $food ) {
   $food_type = get_post_meta( $food->databaseId, 'food_type', true );

   // if the "food_type" custom field is set to "pizza" return the "Pizza" type
   if ( 'pizza' === $food_type ) {
     $return_type = 'Pizza';
   } else {
     $return_type = 'Cake';
   }

   return $return_type;
 }
];

register_post_type( 'Food', $args );

By adding these 2 lines to our “register_post_type” $args, we’ve now told WPGraphQL to treat “Food” as an Interface instead of an Object Type, and we’ve told it how to determine what Object type to resolve, based on the “food_type” meta value.

In order for this to work, however, we need a “Pizza” and “Cake” Type added to the Schema.

Register the Pizza GraphQL Object Type

In the “graphql_resolve_type” function above, we determined that “Food” can return a “Pizza” or “Cake” based on the value of the “food_type” post meta value.

In order for this to work, we need a Pizza and Cake type to be added to the Schema.

Within our “graphql_register_types” hook where we registered the “price” field, we can add the following:

register_graphql_object_type( 'Pizza', [
  // Description shows in the Schema for client developers using tools like the GraphiQL IDE
  'description' => __( 'A tasty food best prepared in an air-fryer', 'your-textdomain' ),
  // This tells the Schema that "All Pizza is Food" and will inherit the "Food" fields (such as Price)
  'interfaces' => [ 'Food' ],
  // This helps with caching. If your Object Type is associated with a particular Model, you should define it here. In our case, Pizza is resolved by the Post model.
  'model' => WPGraphQL\Model\Post::class, 
   // This field shows the Type in the Schema even if there are no root fields that reference the Type directly.
  'eagerlyLoadType' => true,
  // Define the unique fields of this type. For Pizza, "toppings" will be a "list of strings"
  'fields' => [
    'toppings' => [
      'type' => [ 'list_of' => 'String' ],
      'resolve' => function( $pizza ) {
        $toppings = get_post_meta( $pizza->databaseId, 'toppings', false );
        return is_array( $toppings ) ? $toppings : null;
      }
    ],
  ],
]);

Here we’ve registered a “Pizza” GraphQL Object Type. On the Type we declared that it implements the “Food” interface, and that it has a “toppings” field which returns a list of String, resolved from the “topping” meta key.

Register the Cake GraphQL Object Type

Now, we can register the “Cake” Object Type, very similar to how we registered the “Pizza” Type.

register_graphql_object_type( 'Cake', [
  // Description shows in the Schema for client developers using tools like the GraphiQL IDE
  'description' => __( 'A tasty dessert, most likely also good if heated in an air-fryer', 'your-textdomain' ),
  // This tells the Schema that "All Cake is Food" and will inherit the "Food" fields (such as Price)
  'interfaces' => [ 'Food' ],
  'eagerlyLoadType' => true,
  'model' => WPGraphQL\Model\Post::class,
  'fields' => [
    'frostingColor' => [
      'type' => 'String',
      'resolve' => function( $cake ) {
        return get_post_meta( $cake->databaseId, 'frosting_color', true );
      }
    ],
  ]
]);

Here we’ve registered a “Cake” object type. On the type we’ve declared that it implements the “Food” interface, and that it has a “frostingColor” field which returns a String, resolved from the “frosting_color” meta key.

At this point, we’ve converted the “Food” type to be an Interface using the “graphql_kind” arg on “register_post_type”. We also declared the “graphql_resolve_type” function, returning either a “Pizza” or “Cake” when “Food” is queried.

Then we defined the “Pizza” and “Cake” Types and their fields.

At this point, we can successfully execute the query we set out to.

query GetFood {
  allFood {
    nodes {
      __typename
      id
      title
      price
      ...on Cake {
        frostingColor
      }
      ...on Pizza {
        toppings
      }
    }
  }
}

Since the “allFood” connection is added to the schema from the “food” post type, the resolver will query posts of the “food” post type. Each post will be converted into a “Post” Model and then our “graphql_resolve_type” function will use the “food_type” meta to determine whether the “food” is “Pizza” or “Cake”, then each field (price, toppings, frostingColor) is resolved.

Success! We end up with the expected results:

{
  "data": {
    "foods": {
      "nodes": [
        {
          "__typename": "Cake",
          "title": "Cake",
          "price": "$15.99",
          "frostingColor": "white"
        },
        {
          "__typename": "Pizza",
          "title": "Pepperoni Pizza",
          "price": "$10.99",
          "toppings": [
            "pepperoni",
            "sauce",
            "cheese",
            "jalapenos"
          ]
        }
      ]
    }
  }
}

Scenario 2: 2 different “Pizza” and “Cake” post types

Now, we already accomplished the goal we set out to, but I wanted to show a different way to get the the same goal.

In this approach, instead of using one “food” post type, let’s use 2 different post types: “Pizza” and “Cake”.

But, the goal is still the same. We want to be able to query for “allFood” and depending on whether the food is “Pizza” or “Cake” we’d like to query for “toppings” or “frostingColor” respectively.

Register the Food Interface

Instead of registering a “Food” post type and setting its “graphql_kind” as “interface”, this time we will manually register an Interface, then register 2 post types and apply the “Food” interface to those 2 post types.

With the following snippet, we can register a “Food” interface:

add_action( 'graphql_register_types', function() {

  register_graphql_interface_type( 'Food', [
    'description' => __( 'An item of food for sale', 'your-textdomain' ),
    // ensure all "food" nodes have a title
    'interfaces' => [ 'Node', 'NodeWithTitle' ],
    'fields' => [
      'price' => [
        'type' => 'String',
	'description' => __( 'The cost of the food item', 'your-textdomain' ),
	'resolve' => function( $food ) {
	  return get_post_meta( $food->databaseId, 'price', true );
	},
      ],
    ],
    'resolveType' => function( $node ) {
      // use the post_type to determine what GraphQL Type should be returned. Default to Cake
      return get_post_type_object( $node->post_type )->graphql_single_name ?: 'Cake';
    }
  ]);

});

Register the Post Types

We just defined our “Food” Interface, but it doesn’t really do anything yet, because no Type in the Graph implements this interface.

Let’s register our “Cake” and “Pizza” post types and implement the “Food” interface on them.

add_action( 'init', function() {

  $pizza_args = [
    'public' => true,
    'label' => 'Pizza',
    'show_in_graphql' => true,
    'supports' => [ 'title', 'editor', 'custom-fields' ],
    'graphql_single_name' => 'Pizza',
    'graphql_plural_name' => 'Pizza',
    'graphql_interfaces' => [ 'Food' ],
    'graphql_fields' => [
      'toppings' => [
        'type' => [ 'list_of' => 'String' ],
        'resolve' => function( $pizza ) {
           $toppings = get_post_meta( $pizza->databaseId, 'topping', false );
          return is_array( $toppings ) ? $toppings : null;
        }
      ],
    ],
  ];

  register_post_type( 'pizza', $pizza_args );

  $cake_args = [
    'public' => true,
    'label' => 'Cake',
    'show_in_graphql' => true,
    'supports' => [ 'title', 'editor', 'custom-fields' ],
    'graphql_single_name' => 'Cake',
    'graphql_plural_name' => 'Cakes',
    'graphql_interfaces' => [ 'Food' ],
    'graphql_fields' => [
      'frostingColor' => [
        'type' => 'String',
        'resolve' => function( $cake ) {
          return get_post_meta( $cake->databaseId, 'frosting_color', true );
        }
      ],
    ]
  ];

  register_post_type( 'cake', $cake_args );

});

In this snippet, we’ve registered both a “cake” and a “pizza” post type. We set both to “show_in_graphql”, defined their “graphql_single_name” and “graphql_plural_name”, then we applied the “Food” interface using the “graphql_interfaces” argument.

Additionally, we used the “graphql_fields” argument to add a “toppings” field to the “Pizza” Type and a “frostingColor” field to the “Cake” Type.

At this point, we can search our GraphQL Schema for “food” and we’ll find the “Food” interface, and see that it is implemented by “Pizza” and “Cake”.


However, we will also see that no field in the Schema returns the “food” type.

There’s no “allFood” field, like we set out to query.

There is a “RootQuery.allPizza” and “RootQuery.cakes” field for querying lists of Pizza and lists of Cakes independently, but no “allFood” field.

Let’s add that!

Register “allFood” connection

Since we decided to manage our Pizza and Cake in different Post types, we need to provide an entry point into the Graph that allows us to query items of both of these post types as a single list.

To do this we will use the register_graphql_connection API.

Within the “graphql_register_types” hook above, we can add the following:

register_graphql_connection([
  'fromType' => 'RootQuery',
  'toType' => 'Food',
  'fromFieldName' => 'allFood',
  'resolve' => function( $root, $args, $context, $info ) {
    $resolver = new \WPGraphQL\Data\Connection\PostObjectConnectionResolver( $root, $args, $context, $info );
    $resolver->set_query_arg( 'post_type', [ 'pizza', 'cake' ] );
    return $resolver->get_connection();
  }
]);

This code registers a GraphQL Connection from the “RootQuery” to the “Food” type. A Connection is a way in GraphQL to query paginated lists of data. Here, we’ve defined our connection’s “fromType” and “toType”, and set the “fromFieldName”to “allFood’.

This will expose the connection to the Schema. At this point, we would be able to execute our query, but we wouldn’t get any results unless we also had a resolver.

Our resolver function handles querying the “food” from the database and returning data in the proper “connection” shape.

In GraphQL, all resolvers are passed 4 arguments:

  • $source: The object being executed that the resolving field belongs to
  • $args: Any input arguments on the field
  • $context: Context about the request
  • $info: Info about where in the resolve tree execution is

In our resolver above, we take all 4 of these arguments and pass them to a new instance of “\WPGraphQL\Data\Connection\PostObjectConnectionResolver”. This class handles a lot of the complicated work of fetching data from the WordPress Posts table and returning it in a proper shape for connections.

After instantiating the PostObjectConnectionResolver, we use its “set_query_arg” method to ensure the connection resolver will resolve data from the “pizza” and “cake” post types. Then we return the connection.

At this point, we should be able to explore the Schema and see that we now have a “RootQuery.allFood” field, and it returns a “RootQueryToFoodConnection”.

If we tried to query it now, the query would be valid and we shouldn’t get any errors, but we would also get no results.

That’s because we haven’t entered data into our Pizza and Cake post types!

Create a Pizza and a Cake

We can follow the same steps we did in the first scenario to enter data for the “Pizza” and “Cake” post type.

The difference, this time, is that we can enter them in their own Custom Post Type, and we don’t need to specify the “food_type” meta field, as we’re using the post type itself to make the distinction.

Here’s what my Pizza looks like:

Here’s what my Cake looks like:

Query for allFood

Now that we have some data in our “pizza” and “cake” post types, we can successfully execute the query we set out for.

Conclusion

In this post we explored 2 different ways to manage different types of Food and then query that food using WPGraphQL.

The first scenario opted for using a single post type for managing all food, and using a meta field to differentiate the type of food.

In this scenario, since only one post type was registered to the Schema we didn’t need to register a custom connection, it was done automatically by WPGraphQL.

One thing we didn’t cover in detail in this post, was mutations.

In scenario 1, by registering just one post type, WPGraphQL would have added the following mutations: “createFood”, “updateFood”, “deleteFood”. But in scenario 2 each post type would have its own mutations: “createPizza”, “updatePizza”, “deletePizza” and “createCake”, “updateCake”, “deleteCake”.

There are pros and cons to both options, here. I encourage you to play with both options, explore the Schema and understand how each option might best serve your projects.

I hope you learned something in this post and look forward to hearing stories about how these APIs have helped speed up your project development!

Adding End 2 End Tests to WordPress plugins using wp-env and wp-scripts

I recently published a video walking through how End to End tests are set up for WPGraphQL, but I thought it would be good to publish a more direct step-to-step tutorial to help WordPress plugin developers set up End 2 End tests for their own WordPress plugins.

Setting up End to End tests for WordPress plugins can be done in a number of ways (Codeception, Cypress, Ghost Inspector, etc), but lately, the easiest way I’ve found to do this is to use the @wordpress/env and @wordpress/scripts packages, distributed by the team working on the WordPress Block Editor (a.k.a. Gutenberg), along with GitHub Actions.

If you want to skip the article and jump straight to the code, you can find the repo here: https://github.com/wp-graphql/wp-graphql-e2e-tests-example

What are End to End tests?

Before we get too far, let’s cover what end to end tests even are.

When it comes to testing code, there are three common testing approaches.

  • Unit Tests: Testing individual functions
  • Integration Tests: Testing various units when integrated with each other.
  • End to End Tests (often called Acceptance Tests): Testing that tests the application as an end user would interact with it. For WordPress, this typically means the test will open a browser and interact with the web page, click buttons, submit forms, etc.

For WPGraphQL, the majority of the existing tests are Integration Tests, as it allows us to test the execution of GraphQL queries and mutations, which requires many function calls to execute in the WPGraphQL codebase and in WordPress core, but doesn’t require a browser to be loaded in the testing environment.

The End to End tests in WPGraphQL are for the GraphiQL IDE tools that WPGraphQL adds to the WordPress dashboard.

What’s needed for End to End Tests with WordPress?

In order to set up End to End tests for WordPress, we need a way for the test suite to visit pages of a WordPress site and interact with the web page that WordPress is serving. We also need a way to write programs that can interact with the Web Page, and we need to be able to make assertions that specific behaviors are or are not happening when the web page(s) are interacted with. We also need a way for this to run automatically when our code changes.

Let’s break down how we’ll tackle this:

  • @wordpress/env: Sets up a WordPress environment (site) for the test suites to interact with
  • @wordpress/scripts: Runs the tests using Puppeteer and Jest. This lets our tests open the WordPress site in a Chrome browser and interact with the page.
    • Puppeteer: A Node library which provides a high-level API to control Chrome or Chromium over the DevTools Protocol. Puppeteer has APIs that we will use to write tests that interact with the pages.
    • Jest: JavaScript Testing Framework with a focus on simplicity
  • GitHub Actions: We’ll be using GitHub actions for our Continuous Integration. You should be able to apply what is covered in this post to other CI tools, such as CircleCI.

Setting up our dependencies

I’m going to assume that you already have a WordPress plugin that you want to add tests to. But, if this is your first time building a WordPress plugin, you can see this commit to the example plugin to see what’s needed to get a basic WordPress plugin set up, with zero functionality.

If you do not have a package.json already, you’ll need to create a new package.json file, with the following:

{
  "name": "wp-graphql-e2e-tests-example",
  "version": "0.0.1",
  "description": "An example plugin showing how to set up End to End tests using @wordpress/env and @wordpress/scripts",
  "devDependencies": {},
  "scripts": {},
}

npm “devDependencies”

???? If you don’t already have node and npm installed on your machine, you will need to do that now.

We need the following “dev dependencies” for our test suite:

  • @wordpress/e2e-test-utils
  • @wordpress/env
  • @wordpress/jest-console
  • @wordpress/jest-puppeteer-axe
  • @wordpress/scripts
  • expect-puppeteer
  • puppeteer-testing-library

The difference between “dependencies” and “devDependencies” is that if you are bundling a JavaScript application for production, the “dedpendencies” will be included in the bundles for use at runtime, but “devDependencies” are only used during development for things like testing, linting, etc and are not included in the built application for use at runtime. We don’t need Jest or Puppeteer, etc in our runtime application, just while developing.

We can install these via the command line:

npm install @wordpress/e2e-test-utils @wordpress/env @wordpress/jest-console @wordpress/jest-puppeteer-axe @wordpress/scripts expect-puppeteer puppeteer-testing-library --d

Or you can paste the devDependencies in the package.json and run npm install.

Whether you install via the command line or pasting into package.json, the resulting devDependencies block in your package.json should look like the following:

"devDependencies": {
  "@wordpress/e2e-test-utils": "^6.0.0",
  "@wordpress/env": "^4.2.0",
  "@wordpress/jest-console": "^5.0.0",
  "@wordpress/jest-puppeteer-axe": "^4.0.0",
  "@wordpress/scripts": "^20.0.2",
  "expect-puppeteer": "^6.1.0",
  "puppeteer-testing-library": "^0.6.0"
}

Adding a .gitignore

It’s also a good idea to add a .gitignore file to ensure we don’t version the node_modules directory. These dependencies are only needed when developing, so they can be installed on the machine that needs them, when needed. They don’t need to be versioned in the project. I’ve also included an ignore for .idea which are files generated by PHPStorm. If your IDE or operating system includes hidden files that are not needed for the project, you can ignore them here as well.

# Ignore the node_modules, we don't want to version this directory
node_modules

# This ignores files generated by JetBrains IDEs (I'm using PHPStorm)
.idea

At this point, we have our package.json and our .gitignore setup. You can see this update in this commit.

Setting up the WordPress Environment

Now that we’ve got the initial setup out of the way, let’s move on to getting the WordPress environment set up.

The @wordpress/env package is awesome! It’s one, of many, packages that have been produced as part of the efforts to build the WordPress block editor (a.k.a. Gutenberg). It’s a great package, even if you’re not using the block editor for your projects. We’re going to use it here to quickly spin up a WordPress environment with our custom plugin active.

Adding the wp-env script

The command we want to run to start our WordPress environment, is npm run wp-env start, but we don’t have a script defined for this in our `package.json`.

Let’s add the following script:

...
"scripts": {
  "wp-env": "wp-env"
}

You can see the commit making this change here.

Start the WordPress environment

With this in place, we can now run the command: npm run wp-env start

You should see output pretty similar to the following:

> wp-graphql-e2e-tests-example@0.0.1 wp-env
> wp-env "start"

WordPress development site started at http://localhost:8888/
WordPress test site started at http://localhost:8889/
MySQL is listening on port 61812
MySQL for automated testing is listening on port 61840

Two WordPress environments are now running. You can click the links to open them in a browser.

And just like that, you have a WordPress site up and running!

Stopping the WordPress environment

If you want to stop the environment, you can run npm run wp-env stop.

This will generate output like the following:

> wp-graphql-e2e-tests-example@0.0.1 wp-env
> wp-env "stop"

✔ Stopped WordPress. (in 1s 987ms)

And visiting the url in a browser will now 404, as there is no longer a WordPress site running on that port.

Configuring wp-env

At this point, we’re able to start a WordPress environment pretty quickly, but, if we want to be able to test functionality of our plugin, we’ll want the WordPress environment to start with our plugin active, so we can test it.

We can do this by adding a .wp-env.json file to the root of our plugin, and configuring the environment to have our plugin active when the environment starts.

Set our plugin to be active in WordPress

At the root of the plugin, add a file named .wp-env.json with the following contents:

{
  "plugins": [ "." ]
}

We can use this config file to tell WordPress which plugins and themes to have active by default, and we can configure WordPress in other ways as well.

In this case, we’ve told WordPress we want the current directory to be activated as a plugin.

You can see this change in this commit.

Login and verify

Now, if you start the environment again (by running npm run wp-env start), you can login to the WordPress dashboard to see the plugin is active.

You can login at: http://localhost:8888/wp-admin using the credentials:

  • username: admin
  • password: password

Then visit the plugins page at: http://localhost:8888/wp-admin/plugins.php

You should see our plugin active:

Screenshot of the Plugin page in the WordPress dashboard, showing our plugin active.

Running tests

Now that we’re able to get a WordPress site running with our plugin active, we’re ready to start testing!

At this point, there are 2 more things we need to do before we can run some tests.

  • write some tests
  • define scripts to run the tests

Writing our first test

Since our plugin doesn’t have any functionality to test, we can write a simple test that just makes an assertion that we will know is always true, just so we can make sure our test suites are running as expected.

Let’s add a new file under /tests/e2e/example.spec.js.

The naming convention *.spec.js is the default naming convention for wp-scripts to be able to run the tests. We can override this pattern if needed, but we won’t be looking at overriding that in this post.

Within that file, add the following:

describe( 'example test', () => {

    it( 'works', () => {
        expect( true ).toBeTruthy()
    })

})

This code is using two global methods from Jest:

  • describe: Creates a block of related tests
  • it: A function used to run a test (this function is an alias of the “test” function)

Adding scripts to run the tests

In order to run the test we just wrote, we’ll need to add some test scripts to the package.json file.

Right above where we added the wp-env script, paste the following scripts:

"test": "echo \"Error: no test specified\" && exit 1",
"test-e2e": "wp-scripts test-e2e",
"test-e2e:debug": "wp-scripts --inspect-brk test-e2e --puppeteer-devtools",
"test-e2e:watch": "npm run test-e2e -- --watch",

These scripts work as follows:

  • npm run test: This will return an error that a specific test should be specified
  • npm run test-e2e: This will run any tests that live under the tests/e2e directory, within files named *.spec.js
  • npm run test-e2e:debug: This will run the e2e tests, but with Puppeteer devtools, which means a Chrome browser will open and we can watch the tests run. This is super handy, and a lot of fun to watch.
  • npm run test-e2e:watch: This will watch as files change and will re-run the tests automatically when changes are made.

Run the tests

Now that we have a basic test in place, and our scripts configured, let’s run the test command so we can see how it works.

In your command line, run the command npm run test-e2e.

This will run our test suite, and we should see output like the following:

> wp-graphql-e2e-tests-example@0.0.1 test-e2e
> wp-scripts test-e2e

Chromium is already in /Users/jason.bahl/Sites/libs/wp-graphql-e2e-tests-example/node_modules/puppeteer-core/.local-chromium/mac-961656; skipping download.
 PASS  tests/e2e/example.spec.js
  example test
    ✓ works (2 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.434 s, estimated 1 s
Ran all test suites.

Amazing! Our first test that checks if true is indeed truthy, worked! Great!

Just to make sure things are working as expected, we can also add a test that we expect to fail.

Under our first test, we can add:

  it ( 'fails', () => {
    expect( false ).toBeTruthy()
  })

This test should fail.

If we run the script again, we should see the following output:

> wp-graphql-e2e-tests-example@0.0.1 test-e2e
> wp-scripts test-e2e

Chromium is already in /Users/jason.bahl/Sites/libs/wp-graphql-e2e-tests-example/node_modules/puppeteer-core/.local-chromium/mac-961656; skipping download.
 FAIL  tests/e2e/example.spec.js
  example test
    ✓ works (1 ms)
    ✕ fails (73 ms)

  ● example test › fails

    expect(received).toBeTruthy()

    Received: false

       6 |
       7 |     it ( 'fails', () => {
    >  8 |         expect( false ).toBeTruthy()
         |                         ^
       9 |     })
      10 |
      11 | })

      at Object. (tests/e2e/example.spec.js:8:25)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 passed, 2 total
Snapshots:   0 total
Time:        0.519 s, estimated 1 s
Ran all test suites.

We can delete that 2nd test now that we’re sure the tests are running properly.

You can see the state of the plugin at this commit.

Testing that our plugin is active

Right now, testing that true is truthy isn’t a very valuable test. It shows that the tests are running, but it’s not ensuring that our plugin is working properly.

Since our plugin doesn’t have any functionality yet, we don’t have much to test.

One thing we can do to get familiar with some of the test utilities, is testing that the plugin is active in the Admin.

To do this we will need to:

  • Login to WordPress as an admin user
  • Visit the plugins page
  • Check to see if our plugin is active.
    • As a human, we can see a plugin is active because it’s highlighted different than inactive plugins. A machine (our tests) can see if a plugin is active by inspecting the HTML and seeing if the plugin has certain attributes.

Writing the test

In our example.spec.js file, we can add a new test. Go ahead and paste the following below the first test.

it ( 'verifies the plugin is active', async () => {
  // Steps:
  // login as admin
  // visit the plugins page
  // assert that our plugin is active by checking the HTML
});

Right now, these steps are just comments to remind us what this test needs to do. Now, we need to tell the test to do these things.

Login as Admin

One of the dependencies we added in our package.json, was @wordpress/e2e-test-utils. This package has several helpful functions that we can use while writing e2e tests.

One of the helpful functions is a loginUser function, that opens the login page of the WordPress site, enters a username and password, then clicks login.

The loginUser function accepts a username and password as arguments, but if we don’t pass any arguments, the default behavior is to login as the admin user.

In our /tests/e2e/example.spec.js file, let’s import the loginUser function at the top of the file:

import { loginUser } from '@wordpress/e2e-test-utils'

Then, let’s add this function to our test:

it ( 'verifies the plugin is active', async () => {

  // login as admin
  await loginUser();

  // visit the plugins page
  // assert that our plugin is active by checking the HTML

});

Visit the Plugins Page

Next, we want to visit the plugins page. And we can do this with another function from the @wordpress/e2e-test-utils package: visitAdminPage().

Let’s import this function:

import { loginUser, visitAdminPage } from '@wordpress/e2e-test-utils'

And add it to our test:

it ( 'verifies the plugin is active', async () => {

  // login as admin
  await loginUser();

  // visit the plugins page
  await visitAdminPage( 'plugins.php' );

  // assert that our plugin is active by checking the HTML

});

At this point, you should now be able to run the test suite in debug mode and watch the test script login to WordPress and visit the admin page.

Run the command npm run test-e2e:debug.

You should see the tests run, open Chrome, login as an admin that navigate away from the dashboard to the plugins page, then we should see the tests marked as passing in the terminal.

Screen recording showing the test running in debug mode. The Chrome browser opens and logs into the admin then navigates to another page.

NOTE: If you’re in PHP Storm or another JetBrains IDE, the debugger will kick in for you automatically. If you’re in VSCode, you might need to add a .vscode/launch.json file, like this.

Asserting that the plugin is active

Now that we’ve successfully logged into the admin and navigated to the Plugins page, we can now write our assertion that the plugin is active.

If we wanted to inspect the HTML of the plugins page to see if the plugin is active, we could open up our browser dev tools and inspect the element. We would see that the row for our active plugin looks like so:

<tr class="active" data-slug="wpgraphql-end-2-end-tests-example" data-plugin="wp-graphql-e2e-tests-example/wp-graphql-e2e-tests-example.php">

We want to make an assertion that the plugins page contains a <tr> element, that has a class with the value of active, and a data-slug attribute with the value of wpgraphql-end-2-end-tests-example (or whatever your plugin name is).

We can use XPath expressions for this.

I’m not going to go deep into XPath here, but I will show you how to test this in your browser dev tools.

You can open up the plugins page in your WordPress install (that you started from the npm run wp-env command). Then in the console, paste the following line:

$x('//tr[contains(@class, "active") and contains(@data-slug, "wpgraphql-end-2-end-tests-example")]')

You should see that it found exactly one element, as shown in the screenshot below.

Screenshot of testing XPath in the Chrome browser dev tools.

We can take this code that works in the browser dev tools, and convert it to use the page.$x method from Puppeteer.

NOTE: the page object from Puppeteer is a global object in the test environment, so we don’t need to import it like we imported the other utils functions.

// Select the plugin based on slug and active class
        const activePlugin = await page.$x('//tr[contains(@class, "active") and contains(@data-slug, "wpgraphql-end-2-end-tests-example")]');

Then, we can use the (also global) expect method from jest, to make an assertion that the plugin is active:

// assert that our plugin is active by checking the HTML
expect( activePlugin?.length ).toBe( 1 );

The full test should look like so:

  it ( 'verifies the plugin is active', async () => {

  // login as admin
  await loginUser();

  // visit the plugins page
  await visitAdminPage( 'plugins.php' );

  // Select the plugin based on slug and active class
  const activePlugin = await page.$x('//tr[contains(@class, "active") and contains(@data-slug, "wpgraphql-end-2-end-tests-example")]');

  // assert that our plugin is active by checking the HTML
  expect( activePlugin?.length ).toBe( 1 );

});

Running the test should pass. We can verify that the test is actually working and not providing a false pass, by changing the name of the slug in our expect statement. If we changed the slug to “non-existent-plugin” but still assert that there would be 1 active plugin with that slug, we would have a failing test!

Continuous Integration

Right now, we can run the tests on our own machine. And contributors could run the tests if they cloned the code to their machine.

But, one thing that is nice to set up for tests like this, is to have the tests run when code changes. That will give us the confidence that new features and bugfixes don’t break old features and functionality.

Setting up a GitHub Workflow

We’re going to set up a GitHub Workflow (aka GitHub Action) that will run the tests when a Pull Request is opened against the repository, or when code is pushed directly to the master branch.

To create a GitHub workflow, we can create a file at .github/workflows/e2e-tests.yml.

Then, we can add the following:

name: End-to-End Tests

on:
  pull_request:
  push:
    branches:
      - master

jobs:
  admin:
    name: E2E Tests
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        node: ['14']

    steps:
      - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4

      - name: Setup environment to use the desired version of NodeJS
        uses: actions/setup-node@38d90ce44d5275ad62cc48384b3d8a58c500bb5f # v2.2.2
        with:
          node-version: ${{ matrix.node }}
          cache: npm

      - name: Installing NPM dependencies
        run: |
          npm install

      - name: Starting the WordPress Environment
        run: |
          npm run wp-env start

      - name: Running the tests
        run: |
          npm run test-e2e

If you’ve never setup a GitHub workflow, this might look intimidating, but if you slow down to read it carefully, it’s pretty self-descriptive.

The file gives the Worfklow a name “End-to-End Tests”.

name: End-to-End Tests

Then, it configures what GitHub actions the Workflow should be triggered by. We configure it to run “on” the “pull_request” and the “push” actions, if the push is to the “master” branch.

on:
  pull_request:
  push:
    branches:
      - master

Then, we define what jobs to run and set up the environment to us ubuntu-latest and node 14.

jobs:
  admin:
    name: E2E Tests
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        node: ['14']

Then, we define the steps for the job.

The first step is to “checkout” the codebase.

- uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4

Then, we setup Node JS using the specified version.

      - name: Setup environment to use the desired version of NodeJS
        uses: actions/setup-node@38d90ce44d5275ad62cc48384b3d8a58c500bb5f # v2.2.2
        with:
          node-version: ${{ matrix.node }}
          cache: npm

Then, we install our NPM dependencies.

      - name: Installing NPM dependencies
        run: |
          npm install

Then, we start the WordPress environment.

      - name: Starting the WordPress Environment
        run: |
          npm run wp-env start

And last, we run the tests.

      - name: Running the tests
        run: |
          npm run test-e2e

And now, with this in place, our tests will run (and pass!) in GitHub!

You can see the passing test run here.

Conclusion

I hope this post helps you understand how to use the @wordpress/scripts and @wordpress/env packages, Jest, Puppeteer, and GitHub actions to test your WordPress plugins and themes.

If you’re interested in content like this, please subscribe to the WPGraphQL YouTube Channel and follow WPGraphQL on Twitter!

If you’ve never tried using GraphQL with WordPress, be sure to install and activate WPGraphQL as well!

Query any page by its path using WPGraphQL

One of the most common ways WordPress is used, is by users visiting a URL of a WordPress site and reading the content on the page.

WordPress has internal mechanisms that take the url from the request, determine what type of entity the user is requesting (a page, a blog post, a taxonomy term archive, an author’s page, etc) and then returns a specific template for that type of content.

This is a convention that users experience daily on the web, and something developers use to deliver unique experiences for their website users.

When you go “headless” with WordPress, and use something other than WordPress’s native theme layer to display the content, it can be tricky to determine how to take a url provided by a user and convert that into content to show your users.

In this post, we’ll take a look at a powerful feature of WPGraphQL, the nodeByUri query, which accepts a uri input (the path to the resource) and will return the node (the WordPress entity) in response.

You can use this to re-create the same experience WordPress theme layer provides, by returning unique templates based on the type of content being requested.

WPGraphQL’s “nodeByUri” query

One of the benefits of GraphQL is that it can provide entry points into the “graph” that (using Interfaces or Unions) can return different Types of data from one field.

WPGraphQL provides a field at the root of the graph named nodeByUri. This field accepts one argument as input, a $uri. And it returns a node, of any Type that has a uri. This means any public entity in WordPress, such as published authors, archive pages, posts of a public post type, terms of a public taxonomy, etc.

When a URI is input, this field resolves to the “node” (post, page, etc) that is associated with the URI, much like entering the URI in a web browser would resolve to that piece of content.

If you’ve not already used the “nodeByUri” query, it might be difficult to understand just reading about it, so let’s take a look at this in action.

Here’s a video where I walk through it, and below are some highlights of what I show in the video.

Video showing how to use the nodeByUri query in WPGraphQL

Writing the query

Let’s start by querying the homepage.

First, we’ll write our query:

query GetNodeByUri($uri: String!) {
  nodeByUri(uri: $uri) {
    __typename
  }
}

In this query, we’re doing a few things.

First, we give our query a name “GetNodeByUri”. This name can be anything we want, but it can be helpful with tooling, so it’s best practice to give your queries good names.

Next, we define our variable input to accept: $uri: String!. This tells GraphQL that there will be one input that we don’t know about up front, but we agree that we will submit the input as a string.

Next, we declare what field we want to access in the graph: nodeByUri( uri: $uri ). We’re telling WPGraphQL that we want to give it a URI, and in response, we want a node back.

The nodeByUri field is defined in the Schema to return the GraphQL Type UniformResourceIdentifiable, which is a GraphQL Interface implemented by any Type in the Graph that can be accessed via a public uri.

Screenshot of the nodeByUri field shown in GraphiQL

If we inspect the documentation in GraphiQL for this type, we can see all of the available Types that can be returned.

Screenshot of the UniformResourceIdentifiable GraphQL Interface in GraphiQL.

The Types that can be returned consist of public Post Types, Public Taxonomies, ContentType (archives), MediaItem, and User (published authors are public).

So, we know that any uri (path) that we query, we know what we can ask for and what to expect in response.

Execute the query

Now that we have the query written, we can use GraphiQL to execute the query.

GraphiQL has a “variables” pane that we will use to input our variables. In this case, the “uri” (or path) to the resource is our variable.

First, we will enter “/” as our uri value so we can test querying the home page.

Screenshot of the “uri” variable entered in the GraphiQL Variables pane.

Now, we can execute our query by pressing the “Play” button in GraphiQL.

And in response we should see the following response:

{
  "data": {
    "nodeByUri": {
      "__typename": "ContentType"
    }
  }
}
Screenshot of the nodeByUri query for the “/” uri.

Expanding the query

We can see that when we query for the home page, we’re getting a “ContentType” node in response.

We can expand the query to ask for more fields of the “ContentType”.

If we look at the home page of https://demo.wpgraphql.com, we will see that it serves as the “blogroll” or the blog index. It’s a list of blog posts.

This is why WPGraphQL returns a “ContentType” node from the Graph.

We can write a fragment on this Type to ask for fields we want when the query returns a “ContentType” node.

If we look at the documentation in GraphiQL for the ContentType type, we can see all the fields that we can ask for.

Screenshot of the ContentType documentation in GraphiQL

If our goal is to re-create the homepage we’re seeing in WordPress, then we certainly don’t need all the fields! We can specify exactly what we need.

In this case, we want to ask for the following fields:

  • name: the name of the content type
  • isFrontPage: whether the contentType should be considered the front page
  • contentNodes (and sub-fields): a connection to the contentNodes on the page

This should give us enough information to re-create what we’re seeing on the homepage.

Let’s update our query to the following:

query GetNodeByUri($uri: String!) {
  nodeByUri(uri: $uri) {
    __typename
    ... on ContentType {
      name
      uri
      isFrontPage
      contentNodes {
        nodes {
          __typename
          ... on Post {
            id
            title
          }
        }
      }
    }
  }
}

And then execute the query again.

We now see the following results:

{
  "data": {
    "nodeByUri": {
      "__typename": "ContentType",
      "name": "post",
      "uri": "/",
      "isFrontPage": true,
      "contentNodes": {
        "nodes": [
          {
            "__typename": "Post",
            "id": "cG9zdDoxMDMx",
            "title": "Tiled Gallery"
          },
          {
            "__typename": "Post",
            "id": "cG9zdDoxMDI3",
            "title": "Twitter Embeds"
          },
          {
            "__typename": "Post",
            "id": "cG9zdDoxMDE2",
            "title": "Featured Image (Vertical)…yo"
          },
          {
            "__typename": "Post",
            "id": "cG9zdDoxMDEx",
            "title": "Featured Image (Horizontal)…yo"
          },
          {
            "__typename": "Post",
            "id": "cG9zdDoxMDAw",
            "title": "Nested And Mixed Lists"
          },
          {
            "__typename": "Post",
            "id": "cG9zdDo5OTY=",
            "title": "More Tag"
          },
          {
            "__typename": "Post",
            "id": "cG9zdDo5OTM=",
            "title": "Excerpt"
          },
          {
            "__typename": "Post",
            "id": "cG9zdDo5MTk=",
            "title": "Markup And Formatting"
          },
          {
            "__typename": "Post",
            "id": "cG9zdDo5MDM=",
            "title": "Image Alignment"
          },
          {
            "__typename": "Post",
            "id": "cG9zdDo4OTU=",
            "title": "Text Alignment"
          }
        ]
      }
    }
  }
}

If we compare these results from our GraphQL Query, we can see that we’re starting to get data that matches the homepage that WordPress is rendering.

Screenshot of the homepage

There’s more information on each post, such as:

  • post author
    • name
    • avatar url
  • post date
  • post content
  • uri (to link to the post with)

We can update our query once more with this additional information.

query GetNodeByUri($uri: String!) {
  nodeByUri(uri: $uri) {
    __typename
    ... on ContentType {
      name
      uri
      isFrontPage
      contentNodes {
        nodes {
          __typename
          ... on Post {
            id
            title
            author {
              node {
                name
                avatar {
                  url
                }
              }
            }
            date
            content
            uri
          }
        }
      }
    }
  }
}

Breaking into Fragments

The query is now getting us all the information we need, but it’s starting to get a bit long.

We can use a feature of GraphQL called Fragments to break this into smaller pieces.

I’ve broken the query into several Fragments:

query GetNodeByUri($uri: String!) {
  nodeByUri(uri: $uri) {
    __typename
    ...ContentType
  }
}

fragment ContentType on ContentType {
  name
  uri
  isFrontPage
  contentNodes {
    nodes {
      ...Post
    }
  }
}

fragment Post on Post {
  __typename
  id
  date
  uri
  content
  title
  ...Author
}

fragment Author on NodeWithAuthor {
  author {
    node {
      name
      avatar {
        url
      }
    }
  }
}

Fragments allow us to break the query into smaller pieces, and the fragments can ultimately be coupled with their components that need the data being asked for in the fragment.

Here, I’ve created 3 named fragments:

  • ContentType
  • Post
  • Author

And then we’ve reduced the nodeByUri field to only ask for 2 fields:

  • __typename
  • uri

The primary responsibility of the nodeByUri field is to get the node and return it to us with the __typename of the node.

The ContentType fragment is now responsible for declaring what is important if the node is of the ContentType type.

The responsibility of this Fragment is to get some details about the type, then get the content nodes (posts) associated with it. It’s not concerned with the details of the post, though, so that becomes another fragment.

The Post fragment defines the fields needed to render each post, then uses one last Author fragment to get the details of the post author.

We can execute this query, and get all the data we need to re-create the homepage!! (sidebar widgets not included)

Querying a Page

Now, we can expand our query to account for different types.

If we enter the /about path into our “uri” variable, and execute the same query, we will get this payload:

{
  "data": {
    "nodeByUri": {
      "__typename": "Page"
    }
  }
}
Screenshot of initial query for the “/about” uri

We’re only getting the __typename field in response, because we’ve told GraphQL to only return data ...on ContentType and since the node was not of the ContentType type, we’re not getting any data.

Writing the fragment

So now, we can write a fragment to ask for the specific information we need if the type is a Page.

fragment Page on Page {
  title
  content
  commentCount
  comments {
    nodes {
      id
      content
      date
      author {
        node {
          id
          name
          ... on User {
            avatar {
              url
            }
          }
        }
      }
    }
  }
}

And we can work that into the `nodeByUri` query like so:

query GetNodeByUri($uri: String!) {
  nodeByUri(uri: $uri) {
    __typename
    ...ContentType
    ...Page
  }
}

So our full query document becomes (and we could break the comments of the page into fragments as well, too):

query GetNodeByUri($uri: String!) {
  nodeByUri(uri: $uri) {
    __typename
    ...ContentType
    ...Page
  }
}

fragment Page on Page {
  title
  content
  commentCount
  comments {
    nodes {
      id
      content
      date
      author {
        node {
          id
          name
          ... on User {
            avatar {
              url
            }
          }
        }
      }
    }
  }
}

fragment ContentType on ContentType {
  name
  uri
  isFrontPage
  contentNodes {
    nodes {
      ...Post
    }
  }
}

fragment Post on Post {
  __typename
  id
  date
  uri
  content
  title
  ...Author
}

fragment Author on NodeWithAuthor {
  author {
    node {
      name
      avatar {
        url
      }
    }
  }
}

And when we execute the query for the “/about” page now, we are getting enough information again, to reproduce the page that WordPress renders:

{
  "data": {
    "nodeByUri": {
      "__typename": "Page",
      "title": "About",
      "content": "

WP Test is a fantastically exhaustive set of test data to measure the integrity of your plugins and themes.

\n

The foundation of these tests are derived from WordPress’ Theme Unit Test Codex data. It’s paired with lessons learned from over three years of theme and plugin support, and baffling corner cases, to create a potent cocktail of simulated, quirky user content.

\n

The word “comprehensive” was purposely left off this description. It’s not. There will always be something new squarely scenario to test. That’s where you come in. Let us know of a test we’re not covering. We’d love to squash it.

\n

Let’s make WordPress testing easier and resilient together.

\n", "commentCount": 1, "comments": { "nodes": [ { "id": "Y29tbWVudDo1NjUy", "content": "

Test comment

\n", "date": "2021-12-22 12:07:54", "author": { "node": { "id": "dXNlcjoy", "name": "wpgraphqldemo", "avatar": { "url": "https://secure.gravatar.com/avatar/94bf4ea789246f76c48bcf8509bcf01e?s=96&d=mm&r=g" } } } } ] } } } }

Querying a Category Archive

We’ve looked at querying the home page and a regular page, so now let’s look at querying a category archive page.

If we navigate to https://demo.wpgraphql.com/category/alignment/, we’ll see that it’s the archive page for the “Alignment” category. It displays posts of the category.

Screenshot of the Alignment category page rendered by WordPress

If we add “/category/alignment” as our variable input to the query, we’ll now get the following response:

{
  "data": {
    "nodeByUri": {
      "__typename": "Category"
    }
  }
}
Screenshot of querying the “alignment” category in GraphiQL

So now we can write our fragment for what data we want returned when the response type is “Category”:

Looking at the template we want to re-create, we know we need to ask for:

  • Category Name
  • Category Description
  • Posts of that category
    • title
    • content
    • author
      • name
      • avatar url
    • categories
      • name
      • uri

So we can write a fragment like so:

fragment Category on Category {
  name
  description
  posts {
    nodes {
      id
      title
      content
      author {
        node {
          name
          avatar {
            url
          }
        }
      }
      categories {
        nodes {
          name
          uri
        }
      }
    }
  }
}

And now our full query document looks like so:

query GetNodeByUri($uri: String!) {
  nodeByUri(uri: $uri) {
    __typename
    ...ContentType
    ...Page
    ...Category
  }
}

fragment Category on Category {
  name
  description
  posts {
    nodes {
      id
      title
      content
      author {
        node {
          name
          avatar {
            url
          }
        }
      }
      categories {
        nodes {
          name
          uri
        }
      }
    }
  }
}

fragment Page on Page {
  title
  content
  commentCount
  comments {
    nodes {
      id
      content
      date
      author {
        node {
          id
          name
          ... on User {
            avatar {
              url
            }
          }
        }
      }
    }
  }
}

fragment ContentType on ContentType {
  name
  uri
  isFrontPage
  contentNodes {
    nodes {
      ...Post
    }
  }
}

fragment Post on Post {
  __typename
  id
  date
  uri
  content
  title
  ...Author
}

fragment Author on NodeWithAuthor {
  author {
    node {
      name
      avatar {
        url
      }
    }
  }
}

And when I execute the query for the category, I get all the data I need to create the category archive page.

Amazing!

Any Type that can be returned by the nodeByUri field can be turned into a fragment, which can then be coupled with the Component that will render the data.

Building a Bookstore using WordPress, WPGraphQL and Atlas Content Modeler

In this post, we’ll look at how we can create a simple Book Store using WordPress, WPGraphQL and Atlas Content Modeler, a new plugin from WP Engine that allows Custom Post Types, Custom Taxonomies and Custom Fields to be created in the WordPress dashboard and allows the data to be accessed from WPGraphQL.

By the end of this tutorial, you should be able to manage a list of Books, each with a Title, Price and Description field and a connection to an Author.

Then, you should be able to query the data using the GraphiQL IDE in the WordPress dashboard provided by WPGraphQL.

Pre-requisites

In order to follow this tutorial, you will need a WordPress install with WPGraphQL and Atlas Content Modeler installed and activated. This tutorial will not cover setting up the environment, so refer to each project’s installation instructions to get set up.

The WordPress environment I’m using has only 2 plugins installed and activated:

  • WPGraphQL v 1.6.3
  • Atlas Content Modeler v 0.5.0
Screenshot of the WordPress dashboard’s plugin page showing WPGraphQL and Atlas Content Modeler activated

Creating a Book Model with Atlas Content Modeler

Since the goal is to have a Book Store, we’re going to want to get a new Book model (custom post type) set up using Atlas Content Modeler.

If Atlas Content Modeler has not yet been used in the WordPress install, clicking the "Content Modeler" Menu item in the Dashboard menu will open a “Getting Started” page, where we can create a new Content Model.

Screenshot of the Atlas Content Modeler getting started page

After clicking the “Get Started” button, and I’m presented with a form to create a new Content Model.

Screenshot of the “New Content Model” form in Atlas Content Modeler

There are 6 fields to fill in to create a new model, and I used the following values:

  • Singular Name: Book
  • Plural Name: Books
  • Model ID: book
  • API Visibility: Public
  • Model Icon: I searched for a book and selected it
  • Description: A collection of books
Screenshot of the ACM “New Content Model” form filled in

Clicking “Create” will add the “Book” Content Model to WordPress.

We’ll see the “Books” Type show in the Admin Menu:

And we’ll be presented with a new form where we can start adding Fields to the “Book” Content Model.

For books in our bookstore, we’ll want the following fields:

  • Title (text)
  • Price (number)
  • Description (rich text)

We can add these fields by selecting the field type we want to add, then filling in the details:

Add the Title field

To add the Title field, I selected the “Text” field type, and filled in the form:

  • Field Type: Text
  • Name: Title
  • API Identifier: title
  • Make this field required: checked
  • Use this field as Entry Title: checked
  • Input Type: Single Line
Screenshot of adding the “Title” field to the Book Content Model

After clicking create, I’m taken back to the Model screen where I can add more fields:

Add the Price field

Clicking the “plus” icon below the title field allows me to add a new field.

For the Price field I configured as follows:

  • Field Type: Number
  • Name: Price
  • API Identifier: price
  • Required: checked
  • Number Type: decimal

Add the Description field

Next, we’ll add a Description field.

Following the same steps above, we’ll click the Plus icon and add a new field configured like so:

  • Field Type: Rich Text
  • Name: Description
  • API Identifier: description
Screenshot of the “Description” field being added by ACM

Adding Books to our Bookstore

Now that we’ve created a “Books” content model, we can begin adding Books to our bookstore.

We can click “Books > Add New” from the Admin menu in our WordPress dashboard, and we’ll be taken to a screen to add a new book.

The fields we created are ready to be filled in.

You can fill in whatever values you like, but I’ve filled in mine as:

  • Title: Atlas Content Modeler Rocks!
  • Price: 0.00
  • Description: A priceless book about building content models in WordPress.
Screenshot of a Book content being populated

Book Authors

Before we get too far adding more books, we probably want to add support for adding an “Author” to each book.

While we could add a Text field named Author to the Book Model, that could lead to mistakes. Each book would have to type the Author over and over, and if the Author’s name changed, each book would have to be updated, etc.

It would be better to add the Author as it’s own entity, and create connections between the Author and the Book(s) that the Author has written.

Adding the Author Taxonomy

In order to connect Authors to Books, we’re going to use Atlas Content Modeler to create an Author Taxonomy.

In the WordPress Admin menu, we can click “Content Modeler > Taxonomies” and we’ll be greeted by a form to fill out to add a new Taxonomy.

We’ll fill out the following values:

  • Singular Name: Author
  • Plural Name: Authors
  • Taxonomy ID: author
  • Models: Books
  • Hierarchical: unchecked (not-hierarchical as authors should not have parent/child authors)
  • API Visibility: Public

Once created, we’ll see the Author Taxonomy is now associated with the “book” model.

And we can also see this relationship in the Admin Menu:

And in the “Books” list view, we can also see the “Authors” listed for each book.

Adding an Author

Of course, we don’t have any Authors yet.

Let’s add an Author to our new Author Taxonomy.

In the Admin Menu we can click “Books > Authors” and add a new Author.

I’ll give our author the name “Peter Parker” simply because my son is watching Spiderman as I type this ????‍♂️.

And I added this description as Peter’s bio:

Peter Parker is an author of books about Atlas Content Modeler, and also a member of the Avengers.

Assign an Author to our Book

Now that we have Peter Parker added as an Author, we can assign Peter as the author of our book.

If we navigate back to “Books > All Books” and click “Edit” on the book we created, we’ll now see an “Authors” panel where we can make the connection from our Book to Peter Parker, the author.

If we add Peter Parker as the author, then click “Update” on the book, then navigate back to “Books > All Books” we can now see Peter listed as the author of the book.

Adding more Books

Now that we have our Book Model and Author Taxonomy all set up, let’s add a few more Books and Authors. Feel free to input whatever content you like.


I added one more Author: “Tony Stark”:



And 3 more books:

  • Marvel’s Guide to Headless WordPress
  • WordPress, a Super CMS
  • WPGraphQL: The Super Powered API you’ve been waiting for

Querying the Books with WPGraphQL

Now that we’ve created our Book Content Model and Author Taxonomy with Atlas Content Modeler, and populated some data, it’s now time to look at how we can interact with this data using WPGraphQL.

In the WordPress Admin, we’ll navigate to “GraphQL > GraphiQL IDE” and start exploring the GraphQL Schema.

Exploring the GraphQL Schema

In the top right, is a “Docs” button. Clicking this opens the GraphQL Schema documentation.

We can search “book” and see how our Book content model shows in the Schema in various ways.

Additionally, we can click the “Explorer” button to open up a panel on the left side which we can use to compose queries.

Using the “Explorer” we can find the “books” field, and start building a query:

We can also start typing in the Query pane, and get type-ahead hints to help us compose our query:

The final query I ended up with was:

query GetBooks {
  books {
    nodes {
      databaseId
      id
      price
      title
      description
      authors {
        nodes {
          name
        }
      }
    }
  }
}

And the data that was returned was:

{
  "data": {
    "books": {
      "nodes": 
        {
          "databaseId": 9,
          "id": "cG9zdDo5",
          "price": 25.99,
          "title": "WPGraphQL: The Super Powered API you've been waiting for",
          "description": "Learn how to use WordPress data in new ways, using GraphQL!",
          "authors": {
            "nodes":
              {
                "name": "Tony Stark"
              }
            ]
          }
        },
        {
          "databaseId": 8,
          "id": "cG9zdDo4",
          "price": 12.99,
          "title": "WordPress, a Super CMS",
          "description": "Learn all the super powers of the world's most popular CMS.",
          "authors": {
            "nodes":
              {
                "name": "Peter Parker"
              }
            ]
          }
        },
        {
          "databaseId": 7,
          "id": "cG9zdDo3",
          "price": 9.99,
          "title": "Marvel's Guide to Headless WordPress",
          "description": "How to develop headless WordPress sites like a Superhero.",
          "authors": {
            "nodes": 
              {
                "name": "Tony Stark"
              }
            ]
          }
        },
        {
          "databaseId": 5,
          "id": "cG9zdDo1",
          "price": 0,
          "title": "Atlas Content Modeler Rocks!",
          "description": "A priceless book about building content models in WordPress.",
          "authors": {
            "nodes":
              {
                "name": "Peter Parker"
              }
            ]
          }
        }
      ]
    }
  }
}

Conclusion

We’ve just explored how to build a basic Bookstore using WordPress, WPGraphQL and Atlas Content Modeler.

Without writing a line of code, we’ve added a Book model with an Author Taxonomy, and populated our bookstore with Books and Authors and created relationships between them.

Then, we used WPGraphQL to query data in the GraphiQL IDE.

Now that you can access the data via GraphQL, it’s up to you to build something with your favorite front-end technology. Whether you prefer React with Gatsby or NextJS, or Vue, or something else, the data in WordPress is now free for you to use as you please!

Getting started with WPGraphQL and Gridsome

This is a guest post by @nicolaisimonsen

Gridsome is a Vue.js framework for building static generated sites/apps. It’s performant, powerful, yet simple and really faaaaast. Gridsome can pull in data from all sorts of data-sources like CMSs, APIs, Markdown etc. It has a lot of features. Go check ’em out.

Since GraphQL is so efficient and great to work with it makes sense to fetch our WordPress data in that manner. That’s obviously where WPGraphQL comes into the picture and I think it’s a match made in heaven. ????

If you’re up for it, below is a quick-start tutorial that will guide you through building your first WPGraphQL-Gridsome app.

I know I’m stoked about it!

What we will be building

We’ll go ahead and build a small personal site in Gridsome. Basically just a blog. The blog posts will be fetched from WordPress via WPGraphQL.

This project is very minimal, lightweight and this project alone might not blow your socks off, but it’s foundational and a great start to get into headless WordPress with Gridsome.

Setup a WordPress install

First off is to install WordPress.

I highly recommend Local for setting up WordPress locally. It handles everything from server setup and configuration to installing WordPress.

You can also use MAMP/WAMP/LAMP or however else you like to do it. It’s all good.

With WordPress spun up and ready to go, we want to install and activate our one and only plugin. WPGraphQL.

Now go to WPGraphQL > Settings and tick “Enable Public Introspection“.

That’s it. We are now cooking with GraphQL ????????????

Included with WPGraphQL is the IDE tool which is awesome for building/testing out queries directly in WordPress.
It might be a good idea to play around in here for a few minutes before we move along.

Aaaaaaand we’re back. Last thing we need to do is just to add a new post. Add a title, add some content and press publish.

Great. You’re golden. Onwards to some coding!

Gridsome? Let’s go!

I’m including a WPGraphQL-Gridsome starter (well, actually two).

I highly recommend cloning the stripped version – this will only include styles and html, so we can hit the ground running.

However, you can also just start from scratch.

Either way I got you.

If you just want the full code, that’s completely fine too.


Let’s go ahead an open our terminal/console.

The very first thing is to install the Gridsome CLI

npm install --global @gridsome/cli

Navigate to your desired project folder and type in

gridsome create my-personal-site https://github.com/nicolaisimonsen/wpgraphql-gridsome-starter-stripped.git

or if you’re starting from scratch

gridsome create my-personal-site

Now move into the project directory – then start the local develoment

cd my-personal-site
gridsome develop

In our code editor we should have the following:

We’re now exactly where we want to be. From here we need to pull in WPGraphQL to Gridsome as our data-source. For that we’ll be using this gridsome source plugin. Go ahead and install it.

npm install gridsome-source-graphql

The source plugin needs to be configured.
Open up gridsome.config.js and provide the following object for the plugins array.

//gridsome.config.js
module.exports = {
//
  plugins: [
    {
      use: 'gridsome-source-graphql',
      options: {
        url: 'http://{your-site}/graphql',
        typeName: 'WPGraphQL',
        fieldName: 'wpgraphql',
      },
    },
  ],
//
}

Remember the options.url is the site url + graphql endpoint.
(Can be found in WordPress under WPGraphQL > Settings > GraphQL endpoint)

For every change to gridsome.config.js or gridsome.server.js, we need to restart the app. You can type ctrl + c to exit the gridsome develop process and run gridsome develop again to restart.

Now you can test the new GraphQL data-source in Gridsome Playground/IDE – located at http://localhost:8080/___graphql
Write out the following query and hit the execute button (▶︎):

query {
  posts {
    edges {
      node {
        id
        uri
      }
    }
  }
}

There you have it. On the right side you should see your posts data.

That data could prove to be mighty useful, huh?

We’ll start setting up a Gridsome template for our posts.

Within the “src” folder there’s a folder called “templates”.

A template is used to create a single page/route in a given collection (think posts). Go to/create a file within “templates” folder called Post.vue.

/src/templates/Post.vue

In order to query the data from the GraphQL data layer into our templates we can use the following blocks;

<page-query> for pages/templates, requires id.
<static-query> for components.

In the Post.vue template we are fetching a specific post (by id – more on that later), so we’ll write the following <page-query> in between the <template> and <script> blocks:

Also – change console.log(this) to console.log(this.$page).

Important – we’ve only laid the groundwork for our template. It won’t actually fetch the data yet, since the route/page and id (dynamically) haven’t been created. The step needed is the Pages API and that’s where we are heading right now.

Open up gridsome.server.js and provide the following.
(Remember to restart afterwards)

// gridsome.server.js
module.exports = function(api) {
  api.loadSource(({ addCollection }) => {
    // Use the Data Store API here: https://gridsome.org/docs/data-store-api/
  });

  api.createPages(async ({ graphql, createPage }) => {
    const { data } = await graphql(`
      
        query {
          posts {
            edges {
              node {
                id
                uri
              }
            }
          }
        }
      
    `);

    data.posts.edges.forEach(({ node, id }) => {
      createPage({
        path: `${node.uri}`,
        component: "./src/templates/Post.vue",
        context: {
          id: node.id,
        },
      });
    });
  });
};

Remember the Gridsome Playground query?

Basically the api.createPages hook goes into the data layer fetched from WPGraphQL and queries the posts (the exact query we ran in Playground) and then loops through the collection to create single page/routes. We’ll provide a path/url for the route, a component which is the Post.vue template and lastly and context.id of the post/node id.

Magic happened when running “gridsome develop” and now we have routes (got routes?). These can be found in src/.temp/routes.js.

Try accessing the very first Post route in the browser – localhost:8080/{path} and open up the inspection tool to get the console.

Because of the console.log(this.$page) in the mounted() hook of our Post.vue – the post data from WordPress is now being written out in the console.

With this specific data now being available we just need to bind it to the actual template, so we can finally get the HTML and post displayed. Replace the current <article> block with the following:

Refresh the page.

Well, ain’t that a sight for sore eyes. Our blog posts are finally up.

Even though we’re not quite done yet this is awesome. Good job!

Now. We have posts and that’s really great for a blog, but our visitors might need a way to navigate to these.
Let’s set up a page called “blog” to list all of our blog posts.

There’s a folder with “src” called “pages” and this is a great way to setup single pages/routes non-programmatically.
Basically we just put a file with the .vue extension in there and we now have a singe page for that particular route and only that route. Even if we did set up a Page.vue template within “templates”, the Blog.vue file in the “pages” folder would still supercede. Sweet!

But why would you do that? Well, simple and fast is not always a sin. We also really don’t need to maintain a page in WordPress that only list out blog posts and the content is not really changing. However, just know that we could create a Page.vue template if we choose to, and obviously it would include our blog page.

In our new Blog.vue file in “pages” folder insert this <static-query>
in between the <template> and <script> blocks:

So we want to fetch all the posts to display on our blog page and that’s why we’re writing a static query. There’s no page template/Wordpress data for this page and so even if we wrote out a <page-query> (like in Post.vue) it would return null. nothing. nada. nichego.
Change the console.log(this) to console.log(this.$static) and open up our blog page in the browser. Also open the inspection tool and look at the console.

Awesome. Our static-query ($static) has returned an object with an array of 2 posts. We now have the data, so let’s display it on the page.

Replace the <script> block with the following:

This adds a getDate function that we will be using in our Template.

Now, replace the <template> block with the following:

Voila! Go check out the page in the browser.

We are now displaying our posts or rather an excerpt of these with a button to take us to the actual post. That’s wild! Again, good job.

That pretty much concludes the tutorial. You’ve created a personal site with a blog in Gridsome using WordPress & WPGraphQL.

Build. Deployment. Live.

The last thing to this build is to actually use the command ‘build’.

Go to the terminal/console and execute:

gridsome build

Gridsome is now generating static files and upon completion you’ll find the newly created “dist” folder and all of the files and assets.

That’s the site and all of the data from WordPress in a folder that you can actually just drop onto a FTP server and you have a live site.

However a more dynamic and modern way of doing static deployment is to use a static web host and build from a git repository.

There’s lots of hosts out there. I absolutely love and recommend Netlify, but others include Vercel, Amplify, Surge.sh.

The links above should take you to some guides of how exactly to deploy using their services.

It would also be pretty cool if we could trigger a build whenever a post is created/updated/deleted in WordPress. Otherwise we could have to manually build from time to time retrieve the latest data from WordPress. Luckily plugins like JAMstack Deployments help us in that regard. It takes in a build hook url from a static web host and hits that each time WordPress does its operations. I would suggest you to try it out.

I won’t go into deployment in further details, but just wanted to let you in on some of the options for deploying a static site. I’m quite sure can take it from here.

Where to go from here?

Obviously deployment – taking this site live should be one of the next steps, but we might also want to enhance the project.
I’ve listed some possible improvements, which could also just serve as great practice ↓

Further improvements might be; 

* A word about extensions – WPGraphQL can be extended to integrate with other WordPress plugins.
Advanced Custom Fields is a great plugin used by so many to enrich the content and structure of a WordPress site. There’s an WPGraphQL extension for it (and other great plugins too) and these are maintained by some awesome community contributors. Gridsome also has a badass community and a lot of plugins to get you started.

It’s almost too good to be true ????

Wrap it up already

So that’s basically it. Thanks for reading and coding along.

I definitely encourage you to go further read the documentation on both Gridsome and WPGraphQL. It’s very well written and has examples that will help you no matter what you might build.

Lastly, if you need to get in touch I’ll try to help you out the best I can.
Very lastly, if this was of any use to you, or maybe you just hated it – go ahead and let me know.

@nicolaisimonsen

Gutenberg and Decoupled Applications

In this article I want to dive into the current state of Gutenberg and WPGraphQL.

This is a technical article about using Gutenberg blocks in the context of decoupled / headless / API-driven WordPress, and makes the assumption that you already know what Gutenberg is and have some general understanding of how it works.

TL;DR

Client-server contracts around the shape of data is fundamental to achieving “separation of concerns”, a pillar of modular and decoupled application development.

While much of WordPress was built with decoupling in mind, the WP REST API and Gutenberg were not.

As a result, decoupled application developers interacting with WordPress are limited in what they can achieve.

With the growing demand for headless WordPress, this is a key limitation that will hamper growth.

Fortunately, even with the limitations, there are ways forward. In this article I walk through 3 approaches you can implement to use Gutenberg in decoupled applications today, tradeoffs included, and propose a plan to make the future of Gutenberg for decoupled applications a better one.

Replacing my door lock

I recently replaced the lock on the front door of my house.

I ordered the lock from an online retailer. I was able to select a specific brand of lock in a specific color.

When the lock arrived and I opened the package, it was the same brand and color that I ordered. It wasn’t just any random lock, it was the one that I agreed to pay for, and the online retailer agreed to mail me.

I was able install the lock without any surprises. I didn’t have to drill any new holes in my door. The new lock fit the hole in my door that I removed the old lock from.

The new lock wasn’t made by the same manufacturer that made the door, and yet, the lock installed on my door just fine. In fact, there were at least 30 different locks from a variety of manufacturers that I could have selected that all would have worked in my door without any complications.

Decoupled systems

This wasn’t really a story about doors and locks. It’s a story about decoupled systems, and the contracts, or agreements, that make them work.

And its intent is to help frame what I’m talking about with using WordPress, and specifically Gutenberg, in decoupled contexts.

In order for decoupled systems to work well, whether it’s doors and door locks, or WordPress and a decoupled JavaScript application, there needs to be some sort of agreement between the different parts of the system.

In the case of door and lock manufacturers, it’s an agreement over the size and positioning of the holes in the door.

Diagram showing measurements for a door lock hole

Door manufacturers can build doors at their leisure and lock manufacturers at theirs, and when the time comes to bring them together, they work without issue because both parties are adhering to an agreement.

In the case of e-commerce, there are agreements about what a consumer purchases and what should be delivered. In my case, the online store provided a list of locks that were available to purchase. I selected a specific lock, paid for it, and in response I received the lock we agreed to, in exchange for my payment.

Decoupled tech, decoupled teams

When WPGraphQL first started, I was working at a newspaper that had a CMS team that focused on WordPress, a Native Mobile team that focused on the iOS and Android applications, a Data Warehouse team that collected various data from the organization and a Print team that took the data from WordPress and prepared it for Print.

WordPress was the entry point for content creators to write content, but the web was only one of many channels where content was being used.

Not only was the technology decoupled (PHP for the CMS, React Native for mobile apps, Python for Data warehousing and some legacy system I forget the name of for print), but the teams were also decoupled.

The only team that really needed to understand WordPress was the CMS team. The other teams were able to use WPGraphQL Schema Introspection to build tools for their teams using data from WordPress, without needing to understand anything about PHP or WordPress under the hood.

Much like door and lock manufacturers don’t need to be experts at what the other is building, WPGraphQL’s schema served as the contract, enabling many different teams to use WordPress data when, and how, they needed.

WPGraphQL served as the contract between the CMS team and the other teams as well as WordPress the system and the other team’s decoupled systems.

WordPress contracts

For WordPress, one of the common contracts, or agreements established between multiple systems (such as plugins, themes, and WordPress core) comes in the form of registries.

WordPress has registries for Post Types, Taxonomies, Settings, Meta and more.

The register_post_type function has more than 30 options that can be configured to define the contract between the Post Type existing and how WordPress core and decoupled systems (namely plugins and themes) should interact with it.

The register_taxonomy, register_meta, register_setting, register_sidebar and other register_* functions in WordPress serve a similar purpose. They allow for a contract to be established so that many different systems can work with WordPress in an agreed upon way.

These registries serve as a contract between WordPress core and decoupled systems (themes and plugins) that can work with WordPress. Because these registries establish an agreement with how WordPress core will behave, plugins and themes can latch onto these registries and extend WordPress core in some powerful ways.

The decoupled (pluggable) architecture of WordPress is enabled by these contracts.

Image showing WordPress in the middle with the logos for ElasticPress, WordPress SEO by Yoast, WPGraphQL and Advanced Custom Fields around it.
WordPress registries enable plugins to iterate outside of WordPress core

Registering a new post type to WordPress can get you a UI in the WordPress dashboard, but it can also get your content indexed to Elastic Search via ElasticPress, powerful SEO tools from WordPress SEO, custom admin functionality from Advanced Custom Fields, and API access via WPGraphQL.

If the next release of WordPress started hiding the UI for all post types that were registered with show_ui => true, or stopped allowing plugins from reading the post type registry, there would likely be a bug (or hundreds) reported on Trac, (and Twitter, and Slack, etc), as that would mean WordPress was breaking the established contract.

The client/server contract

Like we discussed earlier, decoupled systems need some sort of shared agreement in order to work well together. It doesn’t have to be a GraphQL API, but it has to be something.

For WordPress, this comes in the form of APIs.

WordPress core has 2 built-in APIs that enable decoupled applications to interact with WordPress data, XML-RPC and the WP REST API.

And, of course, there’s yours truly, WPGraphQL, a free open-source WordPress plugin that provides an extendable GraphQL schema and API for any WordPress site.

Blocks representing REST, GraphQL and RPC API on top of a block representing the Authorization and Business logic layers of WordPress, and at the bottom is a block representing the Persistence Layer (MySQL).
Diagram of the WordPress server + API setup

In order for decoupled applications, such as Gatsby, NextJS, Frontity, native mobile applications or others, to work with WordPress, the APIs must establish a contract that WordPress and the decoupled application can both work against.

The WP REST API provides a Schema

The WordPress REST API provides a Schema that acts as this contract. The Schema is introspect-able, allowing remote systems to see what’s available before asking for the data.

This is a good thing!

But the Schema is not enforced

However, the WP REST API doesn’t enforce the Schema.

WordPress plugins that extend the WP REST API Schema can add fields to the API without defining what data will be returned in the REST API Schema. Or, they can register fields that return “object” as a wildcard catch-all.

This is a bad thing!

Decoupled teams and applications cannot reliably use the WordPress REST API if it doesn’t enforce any type of contract.

Optional Schema and wildcard return types

Plugins such as the Advanced Custom Fields to REST API add a single “acf” field to the REST endpoints and declare in the WP REST API that the field will return “an object”.

We can see this if we introspect the WP REST API of a WordPress install with this plugin active:

Screenshot showing the Introspection of the ACF to REST API Schema definition

This means that decoupled applications, and the teams building them, have no way to predict what data this field will ever return. This also means that even if a decoupled application does manage to get built, it could break at any time, because there’s no contract agreed to between the client and the server. The WordPress server can return anything at anytime.

Unpredictable data is frustrating for API consumers

With the field defined as “object” the data returned can be different from page to page, post to post, user to user, and so on. There’s no predictable way decoupled application developers can prepare for the data the API will return.

This would be like me trying to purchase that door lock, but instead of the website showing me a list of door locks with specific colors to chose from, I was just given one “product” as the option to purchase.

The “product” might be a hat or some new sunglasses, or if I’m really lucky, it might be a door lock. I don’t have any way of knowing what the “product” is, until I receive it.

As an e-commerce consumer, this is not helpful.

And as a decoupled application developer, this type of API is frustrating.

Decoupled systems don’t work well if part of the equation is to “just guess”.

GraphQL enforces Schema and Strong Types

WPGraphQL, on the other hand, enforces a strongly Typed Schema. There is no option to extend the WPGraphQL API without describing the type of data the API will return. Additionally, there is no “wildcard” type.

A plugin cannot register a field to the WPGraphQL Schema that returns a door lock on one request, and sunglasses or a hat on the next request.

To extend WPGraphQL, plugins must register fields that declare a specific Type of data that will be returned. And this contract must be upheld.

This removes the “just guess” part of the equation.

Decoupled application developers always know what to expect.

Much like I, as an e-commerce consumer, was able to browse the list of door locks that were possible to purchase on the online store, decoupled application developers can use a tool such as GraphiQL to browse the GraphQL Schema and see what Types and Fields are available to query from the GraphQL API.

The screenshot below shows GraphiQL being used to explore a GraphQL Schema. The screenshot shows the type named “Post” in the GraphQL Schema with a field named “slug” which declares that it will return a String.

Screenshot of GraphiQL showing the “slug” field on the “Post” type.

Application developers can take the information they get from the Schema and construct queries that are now predictable.

And the GraphQL Schema serves as the contract between the server and the client application, ensuring that the server will return the data in the same shape the client was promised.

Just like I received the specific door lock matching the brand and color that I specified in my order, client applications can specify the Types and Fields they require with a GraphQL Query, and the response will match what was asked for.

In the example below, the GraphQL Query asks for a Post and the “slug” field, which we can see in the Schema that it will return a String. And in response to this query, the GraphQL server will provide just what was asked for.

The “just guess” part of the server/client equation is eliminated.

Example GraphQL Query & Response

query {
  post( id: 1, idType: DATABASE_ID ) {
    slug
  }
}
{
  "data": {
    "post": {
      "slug": "hello-world",
    },
  },
}
Screenshot showing a GraphQL Query and Response in the GraphiQL IDE

The Gutenberg block registry

Now that we’re on the same page about contracts between decoupled systems and how WPGraphQL provides a contract between the WordPress server and client applications, let’s move on to discuss Gutenberg more specifically.

Early integration with WPGraphQL

Gutenberg as a concept was fascinating to me early on. Like many others, I saw the potential for this block-based editor to impact WordPress users and the WordPress ecosystem greatly, WPGraphQL included.

I explored exposing Gutenberg blocks as queryable data in WPGraphQL as far back as June 2017:

Challenges and the current state of Gutenberg

While a basic initial integration was straightforward, I ran into roadblocks quickly.

Gutenberg didn’t have a server-side registry for blocks. At this time, all blocks in Gutenberg were fully registered in JavaScript, which is not executed or understood by the WordPress server.

This means that unlike Post Types, Taxonomies, Meta, Sidebars, Settings, and other constructs that make up WordPress, Gutenberg blocks don’t adhere to any type of contract with the WordPress server.

This means that the WordPress server knows nothing about blocks. There are no agreements between Gutenberg blocks and other systems in WordPress, or systems trying to interact with WordPress via APIs.

Blocks were practically non-existent as far as the application layer of WordPress was concerned.

There were no WP-CLI commands to create, update, delete or list blocks. No WP REST API Schema or endpoints for blocks. No XML-RPC methods for blocks. And no way to expose blocks to WPGraphQL.

Without any kind of agreement between the WordPress server and the Gutenberg JavaScript application, the WordPress server can’t interact with blocks in meaningful ways.

For example, the WordPress server cannot validate user input on Gutenberg blocks. Data that users input into the fields in Gutenberg blocks is trusted without question and saved to the database without the server having final say. This is a dangerous precedent, especially as Gutenberg is moving outside of editing Post content and into other parts of full-site editing. As far as I know, the lack of block input validation by the WordPress server is still a problem today.

Anyway, without the WordPress server having any knowledge of blocks, WPGraphQL also could not provide a meaningful integration with Gutenberg.

I was sad, because I was optimistic that this integration could lead to some really great innovations for decoupled applications.

Shortly after my tweet above and running into roadblocks, I raised these concerns with the Gutenberg team on Twitter and Slack. The Gutenberg team asked me to post my thoughts in a Gutenberg Github issue, which I did at length. While my comments received a lot of positive emoji reactions from the community. Unfortunately the issue has been closed with many of the concerns outstanding.

Months later I also voiced similar concerns on the Make WordPress post about Gutenberg and Mobile, pointing out that without a proper server registry and API, decoupled applications, such as the WordPress native mobile application, won’t be able to support Custom Blocks, or even per-site adjustments to core blocks.

As of today, my understanding is that the WordPress native mobile applications still do not support custom blocks or adjustments to core blocks, making the App nearly useless for sites that have adopted Gutenberg.

Even with the limitations of Gutenberg, the headless WordPress community has been determined to use Gutenberg with decoupled applications.

Three approaches to using Gutenberg in decoupled applications, today

Below are some of the different approaches, including tradeoffs, that you can implement today to start using Gutenberg in decoupled applications.

Gutenberg blocks as HTML

I believe the fastest way to get started using Gutenberg in decoupled applications today, is to query the “content” field from WPGraphQL (or the WP REST API, if it’s still your flavor).

This is the approach that Frontity is using.

This is also the approach I’m using for WPGraphQL.com, which is in use on this very blog post you’re reading right now.

This post is written in Gutenberg, queried by Gatsby using WPGraphQL, and rendered using React components!

Here’s how it works (and please don’t judge my hacky JavaScript skills ????):

  • The GraphQL Query in Gatsby gets the content (see the code)
  • The content is passed through a parser (see the code)
  • The parser converts standard HTML elements into the Chakra UI equivalent to play nice with theming (see the code)
  • The parser also converts things like HTML for Twitter embeds, and `<code>` blocks into React components (see the code)
    • This is how we get neat things like the Syntax highlighting and “copy” button on the code snippets

Tradeoff: Lack of Introspection, unpredictable data

While this is working for me and WPGraphQL.com, I can’t recommend it for everyone.

Using HTML as the API defeats much of the purpose of decoupled systems. In order to use the markup as an API, the developers of the decoupled application need to be able to predict all the markup that might come out of the editor.

Querying HTML and trying to predict all the markup to parse is like me ordering “product” at the store. At any time I (or other users of WordPress) could add blocks with markup that my parser doesn’t recognize and the consuming application might not work as intended.

Tradeoff: Missing data

Content creators can modify attributes of blocks, and Gutenberg saves these attributes as HTML comments in the post_content. But when the content is prepared for public use in WordPress themes, the WP REST API or WPGraphQL, the raw block attributes are not available, so a parser like the one I described will not have all the block data to work with.

Tradeoff: Undefined Types

To overcome the “missing data” issue, it’s possible to pass attributes from Gutenberg blocks as HTML data-attributes in the render_callback for blocks, as a way to get Gutenberg attributes passed from the editor to the rendered HTML and available for a parser to use, but even doing this leads to client applications not knowing what to expect, and leads to undefined Types as all data-attributes are strings, so mapping data-attributes to something like a React or Vue component is difficult and fragile with this method.

When to use

This approach works for me, because I personally control both sides of wpgraphql.com, what blocks are available in the WordPress install, what content is published, and the Gatsby consumer application that queries the content and renders the site. In the e-commerce analogy, I’m both the person ordering the “product” and the person fulfilling the order, so there are no surprises. I’m not working with different teams, or even different team members, and I’m the primary content creator.

For projects that have multiple team members, multiple authors, multiple teams and/or multi-channel distribution of content, I would not recommend this approach. And multi-team, I would argue, includes the team that builds the project, and the team that maintains it after it’s live, which in many agencies are different teams.

Gutenberg Object Plugin

In late 2018, Roy Sivan, a Senior JavaScript Engineer and recurring Happy Birthday wisher to Ben Meredith, released a plugin that exposed Gutenberg blocks to the WP REST API:

This plugin exposes Gutenberg block data to the WP REST API so that data saved to pages can be consumed as JSON objects.

Exposing Gutenberg data as JSON is what a lot of developers building decoupled applications want. They want to take the data in a structured form, and pass the data to React / Vue / Native components. This plugin gets things headed in the right direction!

Tradeoff: Lack of Introspection, unpredictable data

But, because of the lack of a server-side registry for Gutenberg blocks, and the non-enforced Schema of the WP REST API, this plugin also suffers from the “just guess” pattern for decoupled applications.

This plugin is unable to register blocks or fields to the WP REST API, so inspecting the Schema leaves decoupled application developers guessing.

If we Introspect the REST API Schema from this plugin and we can see that the Schema doesn’t provide any information to the decoupled application developer about what to expect.

Screenshot of the introspection of the Gutenberg Object Plugin REST endpoint

It’s like ordering a “product” from an e-commerce store. The endpoint can return anything at any time, and can change from page to page, request to request.

There’s no contract between the REST endpoints and the consumer application. There’s no scalable way for decoupled application developers to know what type of data the endpoints will return, now or in the future.

Tradeoff: Only available in REST

If you’re building headless applications using WPGraphQL, taking advantages of features that differentiate WPGraphQL from REST, you would not be able to use the GraphQL Objects plugin in your decoupled application without enduring additional pain points, in addition to the lack of introspection.

Caching clients such as Apollo would have to be customized to work with data from these endpoints, and still may not work well with the rest of the application that might be using GraphQL. Additionally, when using REST endpoints with related resources, it becomes the clients responsibility to determine how to map the various block endpoint data to the components that need the data. There’s no concept of coupling GraphQL Query Fragments with Components, like you can do with GraphQL.

When to use:

Again, if you are the developer controlling both sides, the WordPress server and the client application, this approach could work, at least while you’re building the application and the capabilities are fresh in your mind. But in general, this approach can cause some pain points that that might be difficult to identify and fix when things go wrong. For example, 6 months down the road, even the person that built the application will likely forget all the details, and when there’s a bug, and no contract between the applications to refer to, it can be hard to diagnose and fix.

Even when things break with GraphQL applications (and they do), the explicit nature of GraphQL Queries serve as a “documentation of intent” from the original application developer and can make it much easier for teams to diagnose, down to specific leaf fields, what is broken.

WPGraphQL for Gutenberg

In early 2019 Peter Pristas introduced the WPGraphQL for Gutenberg plugin.

The intent of this plugin is to expose Gutenberg blocks to the WPGraphQL Schema, so that decoupled application developers could use tools such as GraphiQL to inspect what blocks were possible to be queried from the API, and compose GraphQL Queries asking for specific fields of specific blocks that they wanted to support.

Now, content creators can publish content with Gutenberg, and decoupled application developers can introspect the Schema and construct queries asking for the specific blocks and fields their application supports.

Decoupled application developers can move at their own pace, independent from the developers and content creators working on the CMS. The decoupled application can specify which blocks are supported, and ask for the exact fields they need. Much like an e-commerce consumer can specify the specific color door lock they want to order from the store! The Schema serves as the contract between the server and the client. Clients can predictably ask for what they want, and get just that in response.

Creating a page

Content creators can use Gutenberg to create pages. In the example blow, we see a page in Gutenberg with a Paragraph block and an Image block.

Screenshot showing the Gutenberg editor with a paragraph and image block.
Screenshot showing the Gutenberg editor with a paragraph and image block.

Exploring the Schema

With the plugin installed and activated (for demo sake I have WPGraphQL v1.2.5 and WPGraphQL for Gutenberg v0.3.8 active), decoupled application developers can use GraphiQL to browse the Schema to see what Gutenberg Blocks are available to query and interact with.

Screenshot of GraphiQL showing Gutenberg Blocks
Screenshot of GraphiQL showing the CoreParagraphBlock and its fields

Querying the blocks

And using the Schema, developers can construct a query to ask for the blocks and fields that their application supports.

Here’s an example query:

{
  post(id: 6, idType: DATABASE_ID) {
    id
    databaseId
    title
    blocks {
      __typename
      name
      ... on CoreImageBlock {
        attributes {
          ... on CoreImageBlockAttributes {
            url
            alt
            caption
          }
        }
      }
      ... on CoreParagraphBlock {
        attributes {
          ... on CoreParagraphBlockAttributes {
            content
          }
        }
      }
    }
  }
}

And the response:

You can see that the response includes the exact fields that were asked for. No surprises.

{
  "data": {
    "post": {
      "id": "cG9zdDo2",
      "databaseId": 6,
      "title": "Test Gutenberg Post",
      "blocks": [
        {
          "__typename": "CoreParagraphBlock",
          "name": "core/paragraph",
          "attributes": {
            "content": "This is a paragraph"
          }
        },
        {
          "__typename": "CoreImageBlock",
          "name": "core/image",
          "attributes": {
            "url": "http://wpgraphql.local/wp-content/uploads/2021/03/Screen-Shot-2021-03-04-at-12.11.53-PM-1024x490.png",
            "alt": "Jason Bahl, dressed in character as JamStackMullet with a Mullet wig and sunglasses, watches the WP Engine Decode conference",
            "caption": "Screenshot of the JamStackMullet watching WP Engine Decode conference"
          }
        }
   }
Screenshot of a query for a post and some blocks using WPGraphQL for Gutenberg.

GraphQL Schema as the contract

Having the GraphQL Schema serve as the contract between the client and server allows each part of the application to move forward at its own pace. There’s now an agreement for how things will behave. If the contract is broken, for example, if the server changed the shape of one of the Types in the GraphQL Schema, it’s easily identifiable and can be fixed quickly, because the client specified exactly what was needed from the server by way of a GraphQL Query.

This removes the “just guess” pattern from decoupled application development with Gutenberg.

Teams that know nothing about WordPress can even make use of the data. For example, a data warehouse team, a native mobile team, a print team, etc. The GraphQL Schema and tooling such as GraphiQL frees up different teams to use the data in their applications how they want.

Client in control

With clients querying Gutenberg blocks as data, this gives clients full control over the presentation of the blocks. Whether the blocks are used in a React or Vue website, or used for a Native iOS app that doesn’t render HTML, or used to prepare a newspaper for print, the client gets to ask for the fields that it needs, and gets to decide what happens with the data. No unexpected changes from the server, the client is in control.

Tradeoffs: Scaling issues

While WPGraphQL for Gutenberg gets us much closer to being able to query Gutenberg blocks as data, it unfortunately has a dependency that makes it very difficult to scale, and it comes back, again, to the lack of a proper server side registry for blocks.

Since Gutenberg Blocks aren’t registered on the server, WPGraphQL for Gutenberg has a settings page where users must click a button to “Update the Block Registry”.

Screenshot of the WPGraphQL for Gutenberg settings page

Clicking this button opens up Gutenberg in a hidden iFrame, executes the JavaScript to instantiate Gutenberg, gets the Block Registry from Gutenberg initialized in JavaScript, sends the list of registered blocks to the server and stores the registry in the options table of the WordPress database. The registered blocks that are stored in the database are then used to map to the GraphQL Schema.

Peter Pristas deservers an award, because this approach is a very creative solution to the frustrating problem of Gutenberg not respecting the WordPress server.

Unfortunately this solution doesn’t scale well.

Since Gutenberg blocks are registered in JavaScript, this means that the JavaScript to register any given block might be enqueued from WordPress on only specific pages, specific post types, or other unique individualized criteria.

That means the JavaScript Block Registry for Page A and Page B might be different from each other, and maybe also different from the registry for Post Type C or Post Type D. So loading one page in an iframe to get the block registry might not get the full picture of what blocks are possible to interact with in a decoupled application.

In order for the block registry that is generated from the iframe to be accurate, every page of every post type that Gutenberg is enabled on in the site would need to be loaded by iframe to account for cases where blocks were registered to specific contexts. Yikes!

Tradoffs: Schema design issues

In addition to the scaling issues, there are some concerns with some of the Schema design choices, and I’ll even take the blame for some of this, as I had many conversations with Peter as he worked on the plugin, and he followed my lead with some of my also poor Schema design choices.

One issue is infinite nesting. Gutenberg blocks, as previously discussed, can sometimes have nested inner blocks. In WPGraphQL for Gutenberg, querying inner blocks requires explicit queries and without knowing what level of depth inner blocks might reach, it’s difficult to compose queries that properly return all inner blocks.

WPGraphQL used to expose hierarchical data in a similar way, but has since changed to expose hierarchical data, such as Nav Menu Items, in flat lists. This allows for nested data in any depth to be queried, and re-structured in a hierarchy on the client.

The unlimited depth issue is commonly reported for projects such as Gatsby Source WordPress.

When to use

If Gutenberg is a requirement for your headless project, this might be a good option, as it allows you to query Gutenberg blocks as structured data. You gain a lot of the predictability that you miss with the other options, and can benefit greatly from features of GraphQL such as Batch Queries, coupling Fragments with components, and more.

So while WPGraphQL for Gutenberg is probably the closest option available for being able to predictably query Gutenberg blocks as data in decoupled applications, there are some serious questions in regards to production readiness, especially on larger projects, and you should consider these issues before choosing it for your next project.

Tradeoffs in mind, agencies such as WebDevStudios are using this approach in production, even for large sites.

Progress for the server side block registry

In 2020, some progress was made in regards to a server side registry for Gutenberg blocks.

While the official Gutenberg documentation still shows developers how to create new blocks fully JavaScript with no server awareness, the core Gutenberg blocks have started transitioning to have some data registered on the server.

You can see here that (as of Gutenberg 5.6.2, released in February 2021) core Gutenberg blocks are now registered with JSON files that can be used by the PHP server as well as the JavaScript client.

These JSON files are now used to expose blocks to the WP REST API.

This is progress!

Inner blocks, inner peace?

Unfortunately it’s not all the progress needed to have meaningful impact for decoupled applications to use Gutenberg. There’s a lot of information that a decoupled application would need about blocks that is not described in the server registry. One example (of many) being inner blocks.

Gutenberg has a concept called “Inner Blocks”, which is blocks that can have other blocks nested within. For example, a “Column” block can have other blocks nested within each column, while other blocks such as an Image block cannot have nested inner blocks.

The bit of server side registry that is now available for core Gutenberg blocks doesn’t declare this information. If we take a look at the Column block’s block.json file, we can see there’s no mention of inner blocks being supported. Additionally, if. we look at the Image block’s block.json file, we don’t see any mention of inner blocks not being supported.

In order for a decoupled application, such as the official WordPress iOS app, to know what blocks can or cannot have inner blocks, this information needs to be exposed to an API that the decoupled application can read. Without the server knowing about this information, decoupled applications cannot know this information either.

So, while there’s been a bit of a migration for the core WordPress blocks to have some server (and REST API) awareness, there’s still a lot of missing information. Also the community of 3rd party block developers are still being directed to build blocks entirely in JavaScript, which means that all new blocks will have no server awareness until the server registry becomes more of a 1st-class citizen for Gutenberg.

What’s next?

The beginnings of a move toward a server-side registry gives hope, and gives a bit of a path toward blocks being properly introspect-able and useful by decoupled teams and applications.

Specification for Server Side Registering Blocks

I believe that the step forward for Gutenberg + decoupled applications, is to come up with a specification for how Gutenberg blocks can be registered on the server to work properly with server APIs.

Once a specification is discussed, vetted, tested and published, the WP REST API, WP CLI and WPGraphQL, and therefore decoupled applications such as the WordPress native mobile app, would all make use of the spec to be able to interact with Gutenberg blocks.

I don’t fully know what this spec needs to look like, but I believe it needs to exist in some form.

Projects such as Gutenberg Fields Middleware from rtCamp, ACF Blocks, and Genesis Custom Blocks all take a server-first approach to creating new Gutenberg blocks, and I think there’s a lot to learn from these projects.

The blocks from these tools are created in a way that the WordPress server knows what blocks exist, what attributes and fields the blocks have, and the server can then pass the description of the blocks to the Gutenberg JavaScript application, which then renders the blocks for users to interact with.

Since the server provides the Gutenberg JavaScript application with the information needed to render the blocks to a content producer, this means the server can also provide the information to other clients, such as the native mobile WordPress app, or teams building decoupled front-ends with Gatsby, Gridsome or NextJS.

The future of decoupled Gutenberg

I believe that with a proper specification for registering blocks on the server, Gutenberg can enable some incredibly powerful integrations across the web.

My thoughts are that we can arrive at a specification for registering blocks that can enable block developers to provide pleasant editing experiences, while providing decoupled application developers with the ability to Introspect the GraphQL API, predictably write GraphQL Queries (and Mutations) to interact with blocks, and get predictable, strongly typed results that can be used in decoupled applications.

In an effort to start discussing what the future of a Gutenberg Block Server Registry Specification like this might look like, I’ve opened the following Github issue: https://github.com/wp-graphql/wp-graphql/issues/1764

If this topic interests you, and you’d like to be involved in discussing what such a specification might look like, please contribute ideas to that issue. Additionally, you can Join the WPGraphQL community on Slack community, and visit the #gutenberg channel and discuss in there.