An Open Graph (OG) image is the image social networks (such as Twitter and Facebook) pull from your website to create a preview when someone shares a link to your website.
Regardless of the effectiveness of the tweet or the quality of the actual linked content, an OG image is usually the first thing people notice about a shared post on social media.
In theory, a better OG image should increase how often people click through to your website and re-share the original shared post.
The OG image we will be creating and how it could look once shared on social media.
Use Cloudinary's generous free tier and flexible API to automatically generate a branded OG image for your blog posts.
Cloudinary is primarily an image hosting service and CDN. They also provide a powerful REST image manipulation and generation API. Think of it as a simplified URL-based Figma or Photoshop: you provide arguments as URL paths and it generates, caches, and returns an image file.
For example, the image on the left is generated with the following URL:
URL to generate the image. It may seem a bit complex at first but we will break it down below.
Follow along the step-by-step walkthrough or jump straight to the complete code at the bottom of the post.
join()
before returning.const createOgImage = () => {
return (
[
// prefix: <domain/yourCloudinaryId/file_type/source_type>
`https://res.cloudinary.com/<cloudinaryId>/image/upload`,
// transform composed image: width, height, quality
`w_1600,h_836,q_100`,
// -------------------------
// WE WILL PLACE LAYERS HERE
// -------------------------
// background image: <cloudinary_public_id>
`grain-gradient.png`,
]
// join parameters with a slash to form a valid URL
// [a, b, c] => "a/b/c"
.join("/")
)
}
The path we send to Cloudinary must form a valid URL. Arguments are
separated by commas: w_1600,h_836,q_100
. Transformation layers can be
chained and are separated by slashes: w_1600,h_836,q_100/grain.png
Cloudinary allows you to place text and image layers that can be individually transformed and positioned on top of a base image.
Overlays consist of two components the l_
layer that starts the overlay definition and includes layer transformations and an fl_layer_apply
flag that closes the definition and includes placement qualifiers.
l_<public_id>/<transformations>/fl_layer_apply,<placement qualifiers>
const e = (str: string) => encodeURIComponent(encodeURIComponent(str))
const createOgImage = () => {
return [
`https://res.cloudinary.com/<cloudinaryId>/image/upload`,
`w_1600,h_836,q_100`,
`l_text:Arial_100:${e(
"Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
)}`,
`grain-gradient.png`,
].join("/")
}
l_text
and <font family>_<font size>
By default, if a layer (image or text) has a larger width or height than the base image, the delivered image canvas is resized to display the entire layer.
const createOgImage = () => {
return [
// ...
`l_text:Arial_100:${e("...")},c_fit,w_1400`,
// ...
].join("/")
}
w_
.fit
crop mode.You can make your OG image fit your brand by customizing the font and color. One powerful Cloudinary feature is that you can use any typeface listed on Google fonts. In this case, I am using the same font and color as my website.
const createOgImage = () => {
return [
// ...
`l_text:Karla_72_bold:${e("...")},co_rgb:ffe4e6,c_fit,w_1400`,
// ...
].join("/")
}
l_text
(docs) to supply some font styling options. For some reason, the API docs don't discuss that you can use a Google Fonts id here.co_rgb
(docs) to change the color of the layer.const createOgImage = () => {
return [
// ...
`l_text:Karla_72_bold:${e("...")},co_rgb:ffe4e6,c_fit,w_1400,h_240`,
`fl_layer_apply,g_south_west,x_100,y_180`,
// ...
].join("/")
}
h_
to limit the text layer to a certain height. This will automatically truncate longer titles that would otherwise overflow the height.fl_layer_apply
(docs) transformation to position the l_text
layer.g_
(gravity) option to position the layer. Follows the point of a compass, north for top, south for bottom, etc.x
and y
coordinates to vertically and horizontally offset the layer from the point of gravity.You can use what we've learned to create a new text layer to house post details such as the author, publish date, and category.
const createOgImage = () => {
return [
// ...
`l_text:Karla_72_bold:${e("...")},co_rgb:ffe4e6,c_fit,w_1400,h_240`,
`fl_layer_apply,g_south_west,x_100,y_180`,
`l_text:Karla_48:${e("...")},co_rgb:ffe4e680,c_fit,w_1400`,
`fl_layer_apply,g_south_west,x_100,y_100`,
// ...
].join("/")
}
You can use image layers to embed other images into your composition. These images can be fetched from your Cloudinary media library, an external URL, or even a user's profile image on a social network.
We'll use the latter to add a headshot to our OG image that dynamically syncs with a user's Twitter profile image. Changing the profile image on Twitter will eventually (after caches expire) update the headshots of all generated OG images.
const createOgImage = () => {
return [
// ...
// dynamically fetched from my Twitter profile
`l_twitter_name:delba_oliveira`,
// Transformations
`c_thumb,g_face,r_max,w_380,h_380,q_100`,
// Positioning
`fl_layer_apply,w_140,g_north_west,x_100,y_100`,
// ...
].join("/")
}
l_twitter_name
to fetch a user's 400px by 400px Twitter profile image.c_thumb
, g_face
, w_380
, and h_380
to slightly crop the image using the thumbnail preset and a human face as the ideal focal point.r_max
to create a maximum border radius (essentially cropping the image to a circle).There is a lot more you can do with Cloudinary's URL-based API. Hopefully, this is a good starting point for you to create your own unique OG images that stand out and lead to more people viewing your content.
export const createOgImage = ({
title,
meta,
}: {
title: string
meta: string
}) =>
[
// ACCOUNT PREFIX
// Add your own Cloudinary account ID.
`https://res.cloudinary.com/cloudinaryId/image/upload`,
// Composed Image Transformations
`w_1600,h_836,q_100`,
// TITLE
// Karla google font in light rose
`l_text:Karla_72_bold:${e(title)},co_rgb:ffe4e6,c_fit,w_1400,h_240`,
// Positioning
`fl_layer_apply,g_south_west,x_100,y_180`,
// META
// Karla, but smaller
`l_text:Karla_48:${e(meta)},co_rgb:ffe4e680,c_fit,w_1400`,
// Positioning
`fl_layer_apply,g_south_west,x_100,y_100`,
// PROFILE IMAGE
// dynamically fetched from my twitter profile
`l_twitter_name:delba_oliveira`,
// Transformations
`c_thumb,g_face,r_max,w_380,h_380,q_100`,
// Positioning
`fl_layer_apply,w_140,g_north_west,x_100,y_100`,
// BG
`grain-gradient.png`,
].join("/")
// double escape for commas and slashes
const e = (str: string) => encodeURIComponent(encodeURIComponent(str))
import { createOgImage } from "@/lib/createOrgImage"
import Head from "next/head"
export default function Page({ post }) {
const ogImage = createOgImage({
title: post.title,
meta: ["delba.dev", post.publishedAt, ...post.tags].join(" · "),
})
return (
<>
<Head>
<meta property="og:image" content={ogImage} />
<meta property="og:image:width" content="1600" />
<meta property="og:image:height" content="836" />
<meta property="og:image:alt" content={post.title} />
<meta name="twitter:card" content="summary_large_image" />
</Head>
{/* ... */}
</>
)
}