[Guide] How to use responsive images with Cloudinary and Casper 3


So I’ve been setting up my new Ghost-powered site (not public yet) and something I struggled with recently is how to keep responsive images while using Cloudinary as a storage adapter.

The main issue is that the storage adapter returns only a single URL for the image, which means you can only define a static set of transformations to be applied by Cloudinary (eg. w_200, or a named transform like t_my_transform). This means that you lose the ability to dynamically change the resolution of the image in your template through the Handlebars tags and srcset (eg. {{img_url feature_image size="xl"}} would return the same URL as {{img_url feature_image size="s"}}).

So how to work around this? Well you have two main options.

Option 1: Use the Cloudinary Fetch API

The simplest option is to prepend your {{img_url ...}} tags with the Cloudinary Fetch API and whatever dynamic transforms you require.

https://res.cloudinary.com/demo/image/fetch/w_200/{{img_url feature_image}}

This works great, but it’s messy and you give up some functionality and caching becomes a little abstract.

Option 2a: Create a custom Handlebars helper for Cloudinary images

Ideally, the best option would be to create a custom Handlebars helper for processing the Cloudinary image URL and automatically injecting dynamic transformation options.

{{cloudinary_img_url feature_image width="200"}}

Unfortunately, it’s currently not possible to extend Handlebars within Ghost without modifying the core Ghost codebase in a variety of places.

Option 2b: Modify the {{img_url ...}} helper

While still requiring modification of the core files, this approach only requires changing a single file and doesn’t require much to be added. It also means you get to keep using {{img_url ...}} and it won’t impact any images that aren’t hosted on Cloudinary.

The first step is to modify /core/frontend/helpers/img_url.js and generally add the following:

// Override the external image check in imgUrl() and call a new function
const isInternalImage = detectInternalImage(requestedImageUrl);
if (!isInternalImage) {
  return getExternalImage(requestedImageUrl, options);

Add the new functions to check for Cloudinary and inject dynamic transforms:

function getExternalImage(requestedImageUrl, options) {
    if (detectCloudinaryImage) {
        return injectCloudinaryParameters(requestedImageUrl, options);

    return requestedImageUrl;

function detectCloudinaryImage(requestedImageUrl) {
    return /cloudinary.com\/YOUR_CLOUDINARY_SITE/.test(requestedImageUrl);

function injectCloudinaryParameters(requestedImageUrl, options) {
    const parameters = options && options.hash && options.hash.cloudinary;

    if (!parameters) {
        return requestedImageUrl;

    const newParams = [defaultParam, parameters].join('/');

    return requestedImageUrl.replace(defaultParam, newParams);

Here’s an example Gist of the entire modified img_url.js file for reference.

Make sure to change YOUR_CLOUDINARY_SITE to whatever site/slug you use in your Cloudinary URLs. Also, you’ll need to change YOUR_DEFAULT_CLOUDINARY_TRANSFORM to whatever default transform you set in the storage adapter. For me I set fetch.transformation to a named transform (so paste that named transform, t_something), but by default you might be using fetch.quality set to auto so paste in q_auto.

After this you need to restart Ghost.

The last step is to update the Casper templates to use the new parameters for Cloudinary images:

  srcset="{{img_url feature_image cloudinary="w_300"}} 300w,
          {{img_url feature_image cloudinary="w_600"}} 600w,
          {{img_url feature_image cloudinary="w_1000"}} 1000w,
          {{img_url feature_image cloudinary="w_2000"}} 2000w"
  sizes="(max-width: 800px) 400px,
      (max-width: 1170px) 1170px,
  src="{{img_url feature_image cloudinary="w_2000"}}"

And that’s it! It’s not simple by any means, but it works. :ok_hand:


Since posting this I’ve release my site to the public, so you can see the results this approach in action:

Thanks, but as this need a core file changed, this mean that if we update, this crash, that is right?