Hey!
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 defaultParam = 'YOUR_DEFAULT_CLOUDINARY_TRANSFORM';
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:
<img
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,
2000px"
src="{{img_url feature_image cloudinary="w_2000"}}"
alt="{{title}}"
/>
And that’s it! It’s not simple by any means, but it works.