Building WPDecoupled: Caching and Content FTW

Alex Moon on

It has been a fun summer and busy fall, but I have managed to get some things done around the site. Other than several new articles I also built out the Guides page for evergreen content that is more likely to be updated or not go out of date.

Recent Content

With the addition of new content and various types, I wanted to be able to surface any new content easily on the home page. It was time to implement a “recent posts” section with inspiration from the venerable CSS Tricks, I built this:

A row of cards slightly overlapping showing titles of recent articles and guides.

It’s got some fun little animations and shows the 10 most recent posts. In GraphQL this is accomplished by querying `contentNodes` instead of specific content types and merging in JS.

query RecentPostsQuery {
	contentNodes(first: 10, where: { contentTypes: [POST, GUIDE] }) {
		nodes {
			...PostMiniCard
		}
	}
}

Caching

For a while, I’ve been relying on WPGraphQL Smart Cache to help speed up the site but have been relying on the Object Cache. While helpful, the full CDN edge caching provided by using Automated Persistent Queries(APQ) over GET requests is much better. So it was time to implement that!

Background

Some quick background for folks not as familiar with GraphQL. GraphQL as a spec is not tied to its transport layer. This means it can be done over HTTP, WebSockets, or any other method you choose. But, on the web, HTTP is common. GraphQL over HTTP is generally done via a `POST` request to provide the query and variables to the server.

curl -g \
-X POST \
-H "Content-Type: application/json" \
-d '{"query":"query{posts {nodes { title }}}","variables":{}}' \
https://cms.wpdecoupled.dev/graphql

Why not GET? URL query parameters can be used for passing a GraphQL query and variables:

https://cms.wpdecoupled.dev/graphql?query={posts{nodes{title}}}&variables={}

This works to a point before it mysteriously starts breaking. The issue? URLs have a max length and that length is not consistent across browsers. So, one day you will add a field to your query and now your whole app comes to a halt. With this in mind let’s dive into Persisted Queries.

Persisted Queries

Persisted Queries work by assigning an ID to the various queries your app makes. When making a request to the server you do so by providing that queryId instead of the query itself. The server looks up the query by ID, returning the response. We didn’t have to send it the whole query, or did we? At some point, the client and the server must get on the same page about which query they are calling what.

Many GraphQL clients enable an export step that gives you a JSON document of all of your queries that you can provide to your server. But this adds steps. Alternatively, you could build all of your queries on your server and tell your front-end client ONLY the ID. This kinda breaks a lot of the benefits of GraphQL and requires the client and server to be closely tied. So what do we do? This is where the Automated part of APQ comes in.

Automated Persisted Queries

I’ve written my query in my front end. I’ve deployed my site. Upon the page load, my query is hashed by the browser, that hash is then used as the ID and passed to the GraphQL server. But the server doesn’t know this hash and GraphQL returns a GraphQL error telling my client the `ID` is unknown.

My client handles this error by making a standard POST request using the full query along with some code in the “extensions” section to let the server know that this is a persisted query. The server will save the query, along with its ID and then return a valid response for the query. Now, every other time that exact query is used, the initial request will return data.

// Body for registering a persisted query in a POST request
{
  operationName: 'MyQuery',
  variables: null,
  query: `query MyQuery { id }`, // Omit for initial ID only request
  extensions: {
    persistedQuery: {
      version: 1,
      sha256Hash: hashOfQuery
    }
  }
}

All this can be done over POST request. APQs via POST have their benefits, namely basic caching and security (not discussed here). But where Automated Persisted Queries shine is when used with GET requests.

APQs are not currently an official GraphQL spec, hence the use of the “extentions” field. They were invented by the team over at Apollo and you can check out their docs on the matter for more info.

APQ via HTTP GET

Okay, so we discussed earlier how GET requests have a length limit, and because of that our variable query lengths can break them when including a query directly. Let’s update our GET query to use APQs.

https://cms.wpdecoupled.dev/graphql?queryId=hashOfQuery&variables={}

If you are not aware, hashes of a given type (SHA256 in this case), are always the same length. Now that we have stable content in our query parameters, we do not risk running over any given browser’s arbitrary URL max length.

Now that we are using GET requests that are unique based on the queryId and variables means our CDN can cache these based on whatever Cache-Control headers your server returns. WPGraphQL Smart Cache also enables you to integrate with your CDN to purge specific queries when content changes that contained in those queries. Lucky for me, WPEngine supports this out of the box and I didn’t have to do any work to integrate it with a CDN.

Conclusion

Smart Cache has dropped page loads from the testing I’ve done and I’m excited to see that reflected in analytics. If you want more details on how WPGraphQL Smart Cache Jason did a great write-up when he released the plugin.

Alex Moon

About Alex Moon

Avid Open Source maintainer, a longtime developer, and vagabond.