Custom CDN hostnames for images in Sanity.io with Astro
I recently ported the Jammed marketing pages to Astro, and wanted to use a custom CDN hostname for the images in Sanity.io. Officially, this is only an enterprise feature, but it was fairly easily to implement using Astro and a simple Cloudflare Worker to proxy and rewrite the image URLs.
Astro and Sanity.io
Regular plans for Sanity.io don’t allow you to use a custom CDN hostname for images. This is a pain, as it means you can’t use a custom domain for images in Sanity.io, and you have to use the default cdn.sanity.io
hostname which is a bit ugly and not very professional looking. I didn’t want to pay for an enterprise plan, so I decided to see if I could implement this myself using Astro and a Cloudflare Worker.
Cloudflare worker
Setup a new Cloudflare worker and attach it to a route.
For mine, I’ve attached the custom image CDN worker to a subdomain of jammed.app, and it’s configured to rewrite the image URLs to the default Sanity.io CDN hostname. The code is pretty simple:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// worker.js
export default {
async fetch(request) {
const accountId = "__INSERT_ACCOUNT_ID__";
const { pathname, search } = new URL(request.url);
const queryCacheKey = `1_${request.hostname}${request.pathname}${request.search}`
const matchingPath = pathname.match(`images/${accountId}/production`)
if (matchingPath) {
return fetch(`https://cdn.sanity.io/${pathname}${search}`, {
cf: {
cacheKey: queryCacheKey,
polish: "lossless",
cacheEverything: true,
cacheTtlByStatus: { "200-299": 86400, 404: 1, "500-599": 0 }
},
});
} else {
const init = {
headers: {
"content-type": "text/html;charset=UTF-8"
}
}
return new Response("Invalid path", init)
}
},
};
If the worker is requested with a path that matches the pattern images/${accountId}/production
, it will proxy the request to the default Sanity.io CDN hostname, and cache the response for 24 hours. If the path doesn’t match the pattern, it will return a 404 error. We generate a cache key based on the hostname, path and query string, and cache the response for 24 hours if it’s a valid image URL. We don’t need to store or otherwise change the response, as it’s already a valid image URL.
Due to the fact that the assets are fingerprinted, valid and found assets will be cached for 24 hours. Increasing this value would increase the HIT rate from Cloudflare side, but has trade offs with the Sanity.io CDN - this is more meant to proxy and be a layer on top of the Sanity.io CDN - requesting the image from Sanity.io CDN first, and then caching the response from Sanity.io CDN for 24 hours gives the best of both worlds.
Invalid asset URLs, or ones that return a 500 error will not be cached at all - meaning if there is an API error with Sanity.io, the CDN won’t cache the error and will instead try again after 1 second. Invalid asset URLs should be broken links, so this is fine.
Lastly, to revoke all cached assets, you can increment the 1_
prefix of the cache key, and all cached assets will be invalidated.
Astro
To use the new CDN sudomain within Astro, you’ll need to change the config for how image URLs are generated. This is done by adding a cdn
property to the sanity
object in astro.config.mjs
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { sanityClient } from "sanity:client";
import imageUrlBuilder from "@sanity/image-url";
import type { ImageUrlBuilder } from "sanity";
import type { SanityAsset } from "@sanity/image-url/lib/types/types";
export const imageBuilder = imageUrlBuilder({
...sanityClient.config(),
baseUrl: "https://my-image-cdn.domain.com",
})
export function urlForImage(source: SanityAsset): ImageUrlBuilder|undefined {
if (!source) return
return imageBuilder.image(source);
}
This will change the base URL for all image URLs generated by Sanity.io, and will use the custom CDN hostname instead of the default Sanity.io CDN hostname.
Andy Callaghan makes Jammed - booking software for music rehearsal studios and recording studios