Dynamic social share images using Cloudinary

Automatically generate unique Open Graph images for each page of your website

Post on Discord by me showing some post text and a link, then unfurled underneath that is a social share snippet with the title and article description from the link plus a preview image with that same text content
Social share images unfurl beneath link shares on many popular social platforms, such as Discord, Slack, LinkedIn, and more.

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:

What is Cloudinary and how does their free tier work? #

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:

Free tier: $0, Free forever, No credit card required, 25 monthly credits
Cloudinary's free tier includes all the features we need plus 25 monthly credits. See full details on their pricing page.

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?

  • I wrote an article about responsive images using Cloudinary.
  • I put my personal Cloudinary sign-up link in the article.
  • I eventually got the up to 60 additional credits (and have now removed my sign up link).

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.

Building a multi-layered image in Cloudinary #

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:

Optimize images in Cloudinary using Eleventy, plus a tagline. The sia.codes logo is to the left. The top and bottom borders are pinkish and the background has a splattering of confetti.
My og:image from another blog post

The elements of this image include:

  • Underlying graphic - the base decorative image with branded elements that do not change (like my sia.codes logo and confetti background)
  • Title - styled text overlay positioned uppish
  • Subtitle - styled text overlay positioned downish

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.

Generating and transforming the underlying graphic #

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.

How to position layers with gravity #

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:

Interactive positioning demo with gravity set to center. The cardinal directions for gravity start at the top of the image which is north.

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.

Interactive positioning demo with gravity set to southwest and northwest with small x and y offsets.

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:

  • Title: Use south_west with a y-offset from the bottom so the text never flows below that y offset.
  • Subtitle: Use 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.

Generating and positioning the text overlays #

Now that we know how to position the text, we can proceed with generating it. We will need:

  • The actual text content
  • Which font to use
  • What font size
  • How wide the text box should be
  • How to position the text

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.

Dynamically invoking the share image with 11ty shortcodes #

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.

Future development: adding header images from blog posts #

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:

In the future, I'd like to add blog header images, but I need to come up with a better template design first.

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".

Conclusion #

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.

Hi, I'm Sia.

I'm a freelance performance engineer and web developer, and I'm available for your projects.

Hire me

You might also like

Build that blog already

What's holding you back from starting your blog? Sort through the real issues from the noise and start today.

Webmentions

If you liked this article and think others should read it, please share it.

Likes 16 Reposts 3 Comments 10
Emelia ???????? Emelia ????????

@sia @jlengstorf careful there: without using signed URLs anyone can generate social images that look authentically yours source

Sia Karamalegos early voted ????️ Sia Karamalegos early voted ????️

I don't see it yet ???? source

Bobbing for Apples Annie ???? Bobbing for Apples Annie ????

@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

Eric Portis Eric Portis

@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

Eric Portis Eric Portis

@anniegreens @sia yup! (added `?bust=cache` to the url) source

Mauricio Schneider Mauricio Schneider

Are replies to this post expected to show up automatically in your comments? source

Sia Karamalegos Sia Karamalegos

Yes! But it takes some time because bridgy doesn't poll real-time, and my site is static so I only trigger rebuilds once per day. I'll probably do a manual rebuild to check it in a bit. source

Mauricio Schneider Mauricio Schneider

Pretty cool! source

Mauricio Schneider Mauricio Schneider

Did you manually add the link to this thread using the “add webmention” form at the bottom of the comment section, so it knows what to scrape? source

Sia Karamalegos Sia Karamalegos

No, I set up webmentions a long time ago - here's a post I wrote about it. You can ignore the 11ty bits to see how it's all set up. Essentially, I use Bridgy to grab webmentions from Mastodon and Bluesky (and Twitter originally) sia.codes/posts/webmen... source

These are webmentions via the IndieWeb and webmention.io. Mention this post from your site:

← Home