Using Cloudinary as a write-through cache for a Netlify Function that generates images

Chris Biscardi
InstructorChris Biscardi

Share this video with your friends

Send Tweet

Every time we hit our function it runs the headless browser and regenerates the image. We can fix this by using Cloudinary as a write-through cache in front of our Netlify opengraph image generation function. If the image exists, Cloudinary will return it, if not it will ask our function to generate it, then cache it.

Instructor: [00:01] Now that we can generate anything we want with this open-graph image, any title, any tags, any author name, it's time to make sure that it doesn't get called more than once to generate the same image.

[00:13] If you look in the bottom left of the screen, every time I refresh, you can tell it takes a little bit of time far longer than just returning an image would take. To fix this, we can use Cloudinary, but we're not going to use Cloudinary in the way that you might think.

[00:27] We could, of course, generate this image, save it out, and then uploaded the Cloudinary manually. What we are going to do is use Cloudinary as a write-through cache. This means that the URL we are going to use is going to hit Cloudinary first. If it has the image cache, it's going to return the image.

[00:46] If it doesn't have the image cached, Cloudinary is going to hit our function behind the scenes, will generate the image on the fly, and will pass it back to Cloudinary who will then cache it and also return it to whoever's requesting it.

[00:59] This means we don't need to manually manage any of our images, as long as we're not trying to update them.

[01:05] We'll start by uploading an image. You can see in my dashboard, I've uploaded one image into a folder called og-images. I've done that by clicking the right hand upload button over here, where you can drag and drop whatever you want.

[01:17] My image is a one pixel transparent PNG. This is because it's 300 bytes, which is really small, and we're not going to use this image anyway. Since we're doing all of the image generation in our function, I just wanted to make this image, which will get passed to our function for processing, as small as possible.

[01:37] Note the name of the image, image one. It's a PNG and it's in the folder og-images. If we click on Manage where you get the URL in a different way, you can see that there's a version number here.

[01:50] That version number is important because that version number is something that we'll use in our functions to make sure that we can invalidate the cache if we upload a new generation function. Since Cloudinary doesn't really know other than the URL whether our function has changed on Netlify, we need to use this version as a cache invalidation technique.

[02:12] We only have one function inside of functions right now, gen-opengraph-image. We need to make a new directory. We're going to call this function process-url because it'll handle processing the URL that we pass it and prepare it for Cloudinary.

[02:25] We'll initialize the new package JSON and we need to add one package, the Cloudinary package. We'll name our file process-url.js so that we can deploy this on Netlify. Typically, a Cloudinary URL looks something like res.cloudinary.com/the name of the owner/image/upload, in optional version, the folder it's in, and the file name.

[02:50] Note that we have the folder name and the file name, as well as the image in our URL generation here. Let's bring it back from the top. We import Cloudinary and we're using the V2 API, which is pretty important. We're also importing querystring, which will help us take the variables that we have and pass them in.

[03:08] We need querystring because the event that comes in from the Lambda, which is what's underpinning the implementation of Netlify functions, gives us an object that we need to turn back into query strings to stick on the end of our URL to generate the image.

[03:25] Then we configure our Cloudinary account. We've got our Cloud name, the API key, and the API secret. Our handler uses the same function signature as before. It's an async function with an event and a context, and we're exporting the name handler for it. We pull the query string parameters off the event. queryStringParameters here will be an object.

[03:44] Then we wrap everything in a try catch because we really don't care if this fails. If it fails, we don't want to contact Cloudinary at all, we don't want to hit any other service, we're just going to fail. Then we can use cloudinary.url with the version of the image, which is what we talked about earlier, and the location of that image in our account.

[04:03] The next option is an object is required to sign the URL, which is why we're using this function at all. The other option we're going to use is called custom_pre_function. We can also use custom_function. In this case, custom_pre_function allows us to do processing before Cloudinary does anything at all.

[04:22] Our function generation is always the first thing that gets called. We specify a function_type of remote. The other function type that's available to us is WASM or WebAssembly. WebAssembly doesn't do quite what we need, which is why we needed a list Lambda in the first place.

[04:38] Finally, the source of the remote function is the URL on Netlify with the query strings. The entire source gets signed into the request and we return a redirect to Cloudinary with an empty body. Remember, if you look at this image URL, it's the URL we're generating that goes to Cloudinary that we're passing in as the location here.

[05:00] With all that said, we also need to add the process URL npm install to our make file. This is where our make file starts to get very useful, compared to say, a set of NPM scripts. We can do anything we want inside of install.

[05:14] Remember that the space in the front needs to be a tab and each line is run in its own environment. That's why we're using && to chain calls together.

[05:24] First, we go into the directory and we do an npm install and we do an npm run build. Then we have a different environment where we go into a different directory and do a different install. Before we push, we're going to set our environment variables in Netlify.

[05:41] Back on our deploys page, which we've been watching as we go through this, we can go to the Deploy Settings and click the Environment. These environment variables are the variables that we'll need to set. That is, we're going to need to set the image version, which I've prepended my name to because I intend to have more versions, the CLOUDINARY_KEY, and the CLOUDINARY_SECTRET.

[06:03] Now that we've set our environment variables, which will get baked into the function environment, we can push. While this is building, let's go over one more time what we just did. We now have two functions. One function's job is to generate the image. The other functions job is to sign a URL and redirect us to Cloudinary.

[06:26] Once we're redirected to Cloudinary, Cloudinary either serves us the image that we want or asks our other function to generate it. If it asks our other function to generate it, that function will generate it and return it, where Cloudinary will cache it and also return it to us.

[06:42] Note that we're changing the URL to point to Netlify functions process-url. Note how long this takes. It's also redirected us. If you look at the URL, we're looking at res.cloudinary.com right now.

[06:55] If we do this again, it loads almost instantly. This is how we know it's been cached. Also note that while gen-opengraph-image handles an unURL-encoded at symbol correctly processed URL, having sent it through more different URLs and the signing doesn't.

[07:14] Keep this in mind if you're thinking about using different symbols in the URL query string. You will have to deal with those when they get to your generation function in the end in the same way that we dealt with them earlier per tags. That's it. We can use this URL now to serve up cached images for any website on the Internet.

Joel Hooks
Joel Hooks
~ a year ago

I had some challenges with this episode around the Cloudinary URL signing. It consistently generated URLs with a rogue / whenever I used the ? to add the query params.

const qs = require('querystring')

exports.handler = async function (event, ctx) {
  const {queryStringParameters} = event
  try {
    const imageUrl = `https://res.cloudinary.com/${
      process.env.CLOUD_NAME
    }/image/fetch/${encodeURIComponent(
      `https://competent-goodall-d71d0d.netlify.com/.netlify/functions/gen-opengraph-image?${qs.stringify(
        queryStringParameters,
      )}`,
    )}`
    return {
      statusCode: 302,
      headers: {
        Location: imageUrl,
      },
      body: '',
    }
  } catch (e) {
    console.log(e)
  }
}

I ended up using this for my process-url function. It utilizes Cloudinary's fetch functionality to forward a URL and has the same basic end result as it utilizes the can and caching!

Invalidating the cache isn't something that I explored.

I created a post on the Cloudinary community support forum describing the issue I ran in to and also had a debugging session with Chris on Twitter.

Lauro Silva
Lauro Silva
~ a year ago

I ran into the same issue. I decided to go with Joel's implementation. Note: the fetch method is disabled by default for any new account.

To enable it, you need to navigate to the following page:

https://cloudinary.com/console/c-4aee1e8012de758933b9f51d526756/settings/security

and uncheck the box next to Fetched URL in the "Restricted media types" section.

Joel Hooks
Joel Hooks
~ a year ago

The Cloudinary team also posted a workaround for making the signed URL approach work and mentioned that they would be patching a proper fix in future versions.