Migrating from Netlify to Cloudflare for AI bot protection
How I migrated my static site from Netlify to Cloudflare including setting up Functions to handle contact form requests
Automatically generate unique Open Graph images for each page of your website
Did you ever notice those social media share images that automatically pop up for a site and think to yourself, "wow, it would be really cool if I could get that set up for my blog"? In this post, I will show you how I use 11ty shortcodes and Cloudinary to generate unique share images for each page and blog post on my site. Cloudinary has a free tier that's generous enough that most personal sites fit under.
Social share images are a part of the Open Graph protocol, originally created by Facebook. They are defined in a meta tag in the head of your HTML using the og:image
property. You can learn more about the protocol plus learn tips for content in Open Graph Meta Tags: How to Boost Social Media Engagement from the Semrush Blog (they know more about SEO than me).
In this post, I'll cover (jump to a section):
Prefer to watch a video about this? I did a short talk at THE Eleventy Meetup:
Cloudinary is an image and video CDN and API platform. I'm a huge fan as it makes creating and serving responsive, performant images loads easier. See my post Optimize Images in Eleventy Using Cloudinary to learn how I use it for this purpose.
Beyond basic image transformation such as size and format, Cloudinary can also add layers over images. These layers can be additional images or styled text content. This is how we will generate our share images.
In terms of pricing, at the time of writing, the free tier came with 25 monthly credits:
A monthly credit is any combination of transformations, storage, and bandwith. Today is the 24th day of the month, and my Cloudinary account is sitting at 1.94 credits used. This site is the bulk of that usage, but I host images there for a few other sites as well. In short, I'm well covered for a while.
What I haven't mentioned yet is that I actually have 87 monthly credits in my free account! How did I do this?
Not that I need 87 credits when I barely use 2, but it's nice to know I'm insured for the future or if I suddenly go viral. You too can get more credits with a little extra work.
Before we start with the details, let's understand what we're building. Here's an example share image from that other Cloudinary blog post of mine:
The elements of this image include:
Also note that the text uses a custom font.
The URL that delivers this image is:
https://res.cloudinary.com/siacodes/image/upload/w_1280,h_640,q_auto:best,c_fill,f_jpg/w_705,c_fit,co_rgb:221f2c,g_south_west,x_455,y_306,l_text:RecursiveSansExtraBold.woff2_60:Optimize%20Images%20in%20Eleventy%20Using%20Cloudinary/w_705,c_fit,co_rgb:221f2c,g_north_west,x_455,y_356,l_text:RecursiveSansRegular.woff2_36_line_spacing_10:Set%20up%20responsive%20images%20in%20Eleventy%20using%20Cloudinary%20and%20Eleventy%20shortcodes/v1607719366/sia.codes/twitter_tmpl.jpg
That's a mouthful! In the rest of this section, we'll learn how to programmatically build each portion.
Sorry, you're on your own designing this. Start drawing on paper or something. Or copy someone else's general layout and modify the design to make it your own.
The dimensions that worked for me were 1280 x 640.
Here's the code for generating the part of the URL that transforms that base underlying image:
const width = "1280"
const height = "640"
const imageConfig = [
`w_${width}`,
`h_${height}`,
"q_auto:best", // QUALITY: better than the default
"c_fill", // CROP: fill without distortion, may be cropped
"f_jpg", // FORMAT: JPG
].join(",")
It outputs the string:
"w_1280,h_640,q_auto:best,c_fill,f_jpg"
This is just the transformation. I'll show you where the filename goes later.
Before we add our text overlays, we need to understand how positioning layers works in Cloudinary.
You can select your "gravity" basis as a starting point, then offset the position using x and y offsets. For example, using "center" for gravity with an x and y of 0 means that the layer will be positioned in the center of the image as shown in the following example. In the image on the right, I've labeled each area to show you how gravity is applied with cardinal directions with north being up:
In the following image, I've changed the gravity first to south_west
with a 10px offset in both x and y to show you how this starts the overlay in the bottom left corner. Positive x and y move the layer to the right and up, respectively. In the second example, I switched to north_west
. This started the image in the top left, and positive y now moves the image down.
Other than "center", the positive x and y values will move the image away from that edge or corner. See the full docs for Positioning layers with gravity.
So how would we preventing our title and subtitle from overlapping? There are a few options, but I went with Jason's original code:
south_west
with a y-offset from the bottom so the text never flows below that y offset.north_west
with a y-offset from the top so the top of the text is at that y offset, and text flows normally downwards.Now that we know how to position the text, we can proceed with generating it. We will need:
Here's the code for generating the title layer:
const titleConfig = [
`w_${TEXT_AREA_WIDTH}`,
'c_fit', // CROP: Resizes to fit, maintaining aspect ratio
`co_rgb:${TEXT_COLOR}`,
'g_south_west',
`x_${TEXT_LEFT_OFFSET}`,
`y_${TITLE_BOTTOM_OFFSET}`,
`l_text:${TITLE_FONT}_${TITLE_FONT_SIZE}:${cloudinarySafeText(title)}`,
].join(",")
It outputs the string:
"w_705,c_fit,co_rgb:221f2c,g_south_west,x_455,y_306,l_text:RecursiveSansExtraBold.woff2_60:Optimize%20Images%20in%20Eleventy%20Using%20Cloudinary"
The TITLE_FONT
constant is the filename of the WOFF2 file I have uploaded into the root of my Cloudinary media library. If you place yours somewhere else, you will need to prepend the filename with foldername:
, replacing "foldername" with the folder name.
The Cloudinary safe text function is a small helper that modifies the standard encodeURIComponent()
slightly to accommodate special characters in text overlays:
function cloudinarySafeText(text) {
return encodeURIComponent(text)
.replace(/(%2C)/g, '%252C')
.replace(/(%2F)/g, '%252F')
}
The subtitle layer is very similar to the title layer, though I've switched the gravity to northwest and added some line spacing:
const taglineConfig = [
`w_${TEXT_AREA_WIDTH}`,
'c_fit',
`co_rgb:${TEXT_COLOR}`,
'g_north_west',
`x_${TEXT_LEFT_OFFSET}`,
`y_${TAGLINE_TOP_OFFSET}`,
`l_text:${TAGLINE_FONT}_${TAGLINE_FONT_SIZE}
_line_spacing_${TAGLINE_LINE_HEIGHT}:
${cloudinarySafeText(description)}`,
].join(',');
It outputs the string:
"w_705,c_fit,co_rgb:221f2c,g_north_west,x_455,y_356,l_text:RecursiveSansRegular.woff2_36_line_spacing_10:Set%20up%20responsive%20images%20in%20Eleventy%20using%20Cloudinary%20and%20Eleventy%20shortcodes"
Putting it all together:
const BASE_URL = `https://res.cloudinary.com/${CLOUDNAME}/image/upload/`;
const shareImage = `${BASE_URL}${imageConfig}/${titleConfig}/${taglineConfig}/${FOLDER}${SHARE_IMAGE_FILE_NAME}`
The general structure is base URL, transformations for the base image, layers, then finally point to the base image folder and file name.
I wrap all this code in a function socialImageUrl
that I use as an 11ty shortcode in my base layout file:
<!-- Base layout file -->
<meta property="og:description" content="{{ renderDescription }}" />
<meta property="og:url" content="https://sia.codes{{ page.url }}" />
<meta property="og:title" content="{{ renderTitle }}" />
<meta property="og:image" content="{% socialImage renderTitle, renderDescription %}">
<meta property="og:image:type" content="image/png">
<meta property="og:image:width" content="1280">
<meta property="og:image:height" content="640">
See the full implementation in my shortcodes file and my base layout file.
In the future, I want to redesign my share image to include the highlighted/header image for blog post pages. For example, I use an amazing possum on a movie camera photo in my other Cloudinary post. Here's a really terrible example, just overlaying it in the northwest of my current share image:
Here's the underlying URL - I cheated and overlayed it on a screenshot of the existing share image:
https://res.cloudinary.com/siacodes/image/upload/c_scale,g_north_west,l_sia.codes:A_possum_and_a_movie_camera_1943_f4yflt,w_233/v1729800954/sia.codes/twitter_tmpl_adjeyd.jpg
The relevant portion for the possum layer is:
"c_scale,g_north_west,l_sia.codes:A_possum_and_a_movie_camera_1943_f4yflt,w_233".
Social share images, or Open Graph images, are a powerful tool for branding your site and blog posts and driving more engagement from social media and chat apps.
Cloudinary allows us to generate unique, dynamic images for each page of our site. It also caches the transforms in their CDN for fast loading performance.
I'm a freelance performance engineer and web developer, and I'm available for your projects.
Hire meHow I migrated my static site from Netlify to Cloudflare including setting up Functions to handle contact form requests
What's holding you back from starting your blog? Sort through the real issues from the noise and start today.
It's really easy.
If you liked this article and think others should read it, please share it.
@sia @jlengstorf careful there: without using signed URLs anyone can generate social images that look authentically yours source
I don't see it yet ???? source
@sia It seems to get cached, so if you've already shared a post that didn't have it, I think it won't pick it up. source
@anniegreens @sia wonder if this will work... https://sia.codes/posts/social-share-images-using-cloudinary/?bust=cache Dynamic social share images using Cloudinary | sia.codes source
@anniegreens @sia yup! (added `?bust=cache` to the url) source
These are webmentions via the IndieWeb and webmention.io. Mention this post from your site: