Automate Open Graph Images
Written by Basile Samel.
Published Oct 9, 2024. Last updated Oct 9, 2024.
Social networks use Open Graph images to display external links, so getting them right is key to increasing conversion rates.
But it can be difficult to manually design each social card. In the case of a blog post for example, even if you use a ready-made Figma template, you still need to customize the image title, add subtle branding to increase brand awareness, maybe add the author’s picture to raise trust, etc. Different social websites also use different image resolutions and formats, and you might want to optimize them to more easily grab attention.
For all these reasons, you should look into automating the creation of your Open Graph images, and Astro.js is amazingly easy to use for that!
1. Adding An OG Card
If each blog post has a specific image to use for their social card―for example, as unique background images―you need to make it available in your content schema:
/src/content/config.ts
import { z, defineCollection } from 'astro:content';
const blogs = defineCollection({
type: 'content',
schema: ({ image }) => z.object({
title: z.string(),
card: image()
}),
});
export const collections = {
blogs
};
In this example, title
is a string representing the title of the blog post, and card
an image used for the Open Graph card.
AstroJS will automatically take care of parsing and making the image available to the rest of your static pages or endpoints. Neat.
We can then just update our Markdown frontmatter to include a path to our base image:
/src/content/blogs/blog-slug/index.md
---
title: ""
card: "./card.webp"
---
2. Create A Static Endpoint
Typically a blog post HTML page will include these tags:
<meta property="og:title" content="Example Blog Post" />
<meta property="og:type" content="article" />
<meta property="og:image" content="https://example.com//blog/slug/card.jpg" />
<meta property="og:url" content="https://example.com//blog/slug" />
<meta property="og:description" content="A blog post with a valid open graph card." />
If we were to do this manually, we would put our social card in the /public
folder.
Instead, we can serve social cards at build time:
/src/pages/blog/[…slug]/card.webp.ts
import { getCollection } from 'astro:content';
import fs from "node:fs/promises"
const dirname = import.meta.dirname
export async function GET({ props }) {
const { entry } = props;
const file = await fs.readFile(entry.data.card.src)
return new Response(file, {
headers: {
"Content-Type": "image/jpg"
}
})
}
export async function getStaticPaths() {
const blogEntries = await getCollection('blogs');
return blogEntries.map(entry => ({
params: { slug: entry.slug }, props: { entry },
}));
}
This piece of code tells AstroJS to generate a GET endpoint for each blog post to get the Open Graph image corresponding to the Markdown markup.
Image Generator Function With Sharp.JS
We can do better, however.
AstroJS already relies on sharp.js
to optimize images. We can use the same library to create new ones.
Why not use a library like satori.js
, you might ask.
satori
generates SVG images from JSX / HTML, so you can just use CSS properties to create nice flex layouts. You can then use the output SVG with sharp
to turn it into a usable jpeg image.
I prefer to skip this step and rely on sharp
directly to create images from scratch programmatically. It’s kind of painful to align elements properly, but it really helps keep my builds shorter when there are hundreds of social cards to generate. It’s also annoying in satori
how you need to convert your images to base64 encoding to use in HTML to avoid additional network calls.
Here is an example of a social card generator:
/src/pages/blog/[…slug]/card.webp.ts
import sharp from 'sharp'
// ...
async function generateFile(post) {
const logo = await sharp(/*LOGO_PATH*/).resize(100).toBuffer()
const title = await sharp({
text: {
text: `<span foreground="black" background="white" size="x-large">${post.data.title}</span>`,
width: 300,
rgba: true,
dpi: 100
}
}).webp().toBuffer()
const cta = await sharp({
text: {
text: '<span foreground="black" background="white" size="small">clowdr.com</span>',
width: 300,
rgba: true,
dpi: 100
}
}).webp().toBuffer()
return sharp({
create: {
width: 600,
height: 400,
channels: 4,
background: { r: 255, g: 255, b: 255, alpha: 1 }
}
})
.composite([
{ input: logo, top: 150, left: 20 },
{ input: title, top: 150, left: 150 },
{ input: cta, top: 375, left: 475 },
])
.jpeg().toBuffer()
}
I use sharp
to combine three images (one base image using my website’s logo + two text images) over a white background.
Note that you can customize each image’s position relative to its parent, so it only takes a few tries to get it right. You can also use alignment and justification properties to easily center your text, and the width
property can be used to limit the width of the text overlay.
I just need to include my generator function in the endpoint:
- const file = await fs.readFile(entry.data.card.src)
+ const file = await generateFile(entry)
And then we can visit our local development web server http://localhost:4321/blog/BLOGPOST-SLUG/card.webp
to see the result in real-time.
And voilà.
This is all it takes to generate social cards.
Of course you can always go further and optimize as you go:
- The logo and CTA could be stored in-memory instead of being re-generated for each blog post.
- You could use dynamic backgrounds to create more attention-grabbing og images.
- Leverage SVG images to easily re-use and add elements without sacrificing performance.
Susbcribe to the newsletter