Remix Blog
Words are nice... but code speaks louder. Dive into a fully commented project template, showcasing these techniques (and more) in action.
Sorry, no results found for "".
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!
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.
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:
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:
1import { rawExecuteQuery } from '@datocms/cda-client';2
3export async function executeQuery(query, options) {4 const [data, response] = await rawExecuteQuery(5 query,6 {7 ...options,8 returnCacheTags: true,9 },10 );11
12 const cacheTags = response.headers.get("x-cache-tags");13
14 return { data, cacheTags };15}
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()
:
1import { json } from "@remix-run/node";2import { executeQuery } from "lib/fetch-contents";3
4export const loader = async () => {5 const { data, cacheTags } = await executeQuery(SOME_GRAQHQL_QUERY);6
7 return json(8 { data },9 {10 headers: cacheTags11 ? {12 "Surrogate-Key": cacheTags,13 "Surrogate-Control": "max-age=31536000",14 }15 : {},16 }17 );18};19
20export const headers = ({ loaderHeaders }) => {21 const headers = new Headers();22
23 for (const header of ["surrogate-key", "surrogate-control"]) {24 const value = loaderHeaders.get(header);25
26 if (value) {27 headers.set(header, value);28 }29 }30
31 return headers;32};
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):
Surrogate-Control
instructs Fastly to cache this data for a year;
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.
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.
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:
1POST /your/invalidation/endpoint HTTP/1.12Content-Type: application/json3
4{5 "entity_type": "cda_cache_tags",6 "event_type": "invalidate",7 "entity": {8 "id": "cda_cache_tags",9 "type": "cda_cache_tags",10 "attributes": {11 "tags": ["N*r;L", "6-KZ@", "t#k[uP"]12 }13 },14 "related_entities": []15}
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:
1import { json } from "@remix-run/node";2
3async function invalidateFastlySurrogateKeys(serviceId, fastlyKey, keys) {4 return fetch(`https://api.fastly.com/service/${serviceId}/purge`, {5 method: "POST",6 headers: {7 "fastly-key": fastlyKey,8 "content-type": "application/json",9 },10 body: JSON.stringify({ surrogate_keys: keys }),11 });12}13
14export const action = async ({ request }) => {15 if (request.method !== "POST") {16 return json({ success: false }, 404);17 }18
19 if (20 request.headers.get("authorization") !==21 `Bearer ${process.env.CACHE_INVALIDATION_WEBHOOK_TOKEN}`22 ) {23 return json({ success: false }, 401);24 }25
26 const body = await request.json();27
28 const { tags } = body.entity.attributes;29
30 const response = await invalidateFastlySurrogateKeys(31 process.env.FASTLY_SERVICE_ID,32 process.env.FASTLY_KEY,33 tags34 );35
36 if (!response.ok) {37 const responseBody = await response.json();38
39 return json(responseBody, response.status);40 }41
42 return json({ success: true }, response.status);43};
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:
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.