Remix > Using DatoCMS Cache Tags

Using DatoCMS Cache Tags

DatoCMS and Remix make the perfect couple to provide a great user experience: using DatoCMS Cache Tags, you can build websites, cache pages on a CDN for maximum performance, and don't worry about cache invalidation!

To take advantage of this technique, it is necessary to pair your Remix application with a CDN capable of managing caching via tags! The CDN should be positioned above your Remix application, directly serving the visitors.

The whole recipe is made of two parts:

  • Obtain cache tags from DatoCMS and use them to instruct the CDN;

  • Invalidate cache entries through the use of a webhook.

Step 1: Retrieve and apply cache tags

By adding a X-Cache-Tags: true header into your usual Content Delivery API GraphQL queries, the response will include a set of related cache tags in the X-Cache-Tags header:

$ curl 'https://graphql.datocms.com/' \
-H 'Authorization: YOUR-API-TOKEN' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'X-Cache-Tags: true' \
--include \
--data-binary '{ "query": "query { allPosts { title } }" }'
HTTP/2 200
...
X-Cache-Tags: BQD?* 2.a*q f7e N*r;L 6-KZ@ t#k[uP t#k[ub t#k[uU
...
{
"data": {
...
}
}
Cache tags are not readable, and that's a good thing!

DatoCMS provides cache tags that are intentionally opaque, to prevent misinterpretation and misuse on your end. Cache invalidation is a complicated process with a high possibility of errors and overlooking specific edge-cases. Our cache tags help us handle these complexities for you.  Their non-transparent nature also allows us the flexibility to improve our tagging strategies in the future, without necessitating changes on your frontend.

The actual code to use to perform your queries should be something like this:

import { rawExecuteQuery } from '@datocms/cda-client';
export async function executeQuery(query, options) {
const [data, response] = await rawExecuteQuery(
query,
{
...options,
returnCacheTags: true,
},
);
const cacheTags = response.headers.get("x-cache-tags");
return { data, cacheTags };
}

We've highlighted two elements in the code above:

  • with returnCacheTags, we set X-Cache-Tags: true instructing DatoCMS to return cache tags;

  • once the API responds, we retrieve cache tags with response.headers.get("x-cache-tags").

Then, we return the data from the GraphQL query and the cache tags string.

Once we have this function to fetch content from DatoCMS, we need to export two functions from the Remix route files where we want to support cache tags, loader() and headers():

import { json } from "@remix-run/node";
import { executeQuery } from "lib/fetch-contents";
export const loader = async () => {
const { data, cacheTags } = await executeQuery(SOME_GRAQHQL_QUERY);
return json(
{ data },
{
headers: cacheTags
? {
"Surrogate-Key": cacheTags,
"Surrogate-Control": "max-age=31536000",
}
: {},
}
);
};
export const headers = ({ loaderHeaders }) => {
const headers = new Headers();
for (const header of ["surrogate-key", "surrogate-control"]) {
const value = loaderHeaders.get(header);
if (value) {
headers.set(header, value);
}
}
return headers;
};

The loader() function instructs Remix on how to fetch the data required to generate a page: we utilize the json() helper to return the result of our query so that it's available in our React component, but most importantly, we pass the headers options to configure how this data will be cached by the CDN (in our case, Fastly):

  1. Surrogate-Control instructs Fastly to cache this data for a year;

  2. Surrogate-Key instructs Fastly to mark this response with the tags coming from DatoCMS.

The headers() function is used to specify the headers that will be associated not with the data, but with the actual page. Instead of repeating a new query to DatoCMS, we take the headers we just returned from the loader, and set them as part of the response.

Headers can change depending on your CDN!

Different CDNs use different names for the same concepts.

What we call cache tags are surrogate keys among some providers (like for Fastly, which we're using in this example) ; instead of invalidate, many use purge. Examples: Netlify, Cloudflare, Fastly.

Similarly, the names of the headers, or the format of the associated value, change from service to service: be sure to check the exact header name in the provider's documentation. Some examples:

  • Fastly uses Surrogate-Key with a space-separated list of tags;

  • CloudFlare uses the Cache-Tag header with comma-separated tags;

  • Netlify has Netlify-Cache-Tag with a comma-separated tag string.

Also, be mindful of potential constraints regarding the length of the header. We strive to minimize tags as much as we can (for instance, we utilize an alphabet of 83 symbols), but the quantity and size of tags are contingent on the query.

Step 2: Implement the "Invalidate cache tag" webhook

After tagging the responses, it's time to see how you can invalidate the cache when editors change content. First, inside your DatoCMS project Settings, create a new webhook and set as trigger the "Invalidate" event of the "Content Delivery API Cache Tags" entity:

When editors change content, DatoCMS will send a webhook containing all the cache tags that must be invalidated. The webhook request looks like this:

POST /your/invalidation/endpoint HTTP/1.1
Content-Type: application/json
{
"entity_type": "cda_cache_tags",
"event_type": "invalidate",
"entity": {
"id": "cda_cache_tags",
"type": "cda_cache_tags",
"attributes": {
"tags": ["N*r;L", "6-KZ@", "t#k[uP"]
}
},
"related_entities": []
}

To process this request, you need to add an API endpoint in Remix that receives it and calls the CDN to request the invalidation of the cache associated with the tags:

import { json } from "@remix-run/node";
async function invalidateFastlySurrogateKeys(serviceId, fastlyKey, keys) {
return fetch(`https://api.fastly.com/service/${serviceId}/purge`, {
method: "POST",
headers: {
"fastly-key": fastlyKey,
"content-type": "application/json",
},
body: JSON.stringify({ surrogate_keys: keys }),
});
}
export const action = async ({ request }) => {
if (request.method !== "POST") {
return json({ success: false }, 404);
}
if (
request.headers.get("authorization") !==
`Bearer ${process.env.CACHE_INVALIDATION_WEBHOOK_TOKEN}`
) {
return json({ success: false }, 401);
}
const body = await request.json();
const { tags } = body.entity.attributes;
const response = await invalidateFastlySurrogateKeys(
process.env.FASTLY_SERVICE_ID,
process.env.FASTLY_KEY,
tags
);
if (!response.ok) {
const responseBody = await response.json();
return json(responseBody, response.status);
}
return json({ success: true }, response.status);
};
The method of invalidating the cache varies depending on your CDN!

The example above is again based on Fastly: depending on the service you're using, you'll have to use a slightly different method for invalidating the cache. Some examples:

Invalidating on deploy

Even though it's not specifically related to the use of our Cache Tags, it's important to remember that when there's a cache layer above your application, you need to worry about invalidating this cache not only when incoming content from DatoCMS changes — as we did during this tutorial — but also when a new version of your application is deployed!

Fortunately, this usually happens much less frequently compared to a content change, and therefore it is often sufficient to handle this situation with a complete invalidation of the CDN cache at each deployment.