|
| 1 | +--- |
| 2 | +layout: "@layouts/BlogPost.astro" |
| 3 | +title: "How to Create Dynamic Open Graph Images Automatically for Your Site" |
| 4 | +date: "2025-09-22" |
| 5 | +description: "Learn how to automatically generate dynamic Open Graph images for your website using JavaScript." |
| 6 | +tags: ["JavaScript", "Technical Discussion", "Node.js"] |
| 7 | +--- |
| 8 | + |
| 9 | +import Tangent from "@blogComponents/lib/Tangent.astro" |
| 10 | + |
| 11 | +Open graph images are a crucial part of any site (especially a content heavy site like a blog or ecommerce site). They help improve the appearance of your links when shared on social media platforms, but if you are anything like me creating and managing hundreds of images is a massive pain. Fortunately, there is a better way to handle this: dynamic Open Graph images. |
| 12 | + |
| 13 | +In this article I will show you the exact process I used to create dynamic Open Graph images for this blog and the best part is it will work for any site (even if it isn't written in JavaScript). |
| 14 | + |
| 15 | +<Tangent> |
| 16 | + If you want to see a live example of my open graph images you can just add |
| 17 | + `/og.webp` to the end of any blog post URL. For example, [this is the Open |
| 18 | + Graph image for this post](/2025-09/dynamic-og-images/og.webp). |
| 19 | +</Tangent> |
| 20 | + |
| 21 | +## What are Open Graph Images? |
| 22 | + |
| 23 | +Before we can dive into creating Open Graph images we first need to understand what they are and why they are important. Open Graph images are images that show up in a preview when a link to your site is shared on places like social media, Slack, Discord, and even some texting apps. Below is an example what sharing this link on Facebook looks like with an Open Graph image. |
| 24 | + |
| 25 | +<div style="max-width: 500px; margin-inline: auto;"> |
| 26 | +  |
| 28 | +</div> |
| 29 | +If you site does not contain an Open Graph image then the preview will have no image |
| 30 | +at all and will look something like this: |
| 31 | + |
| 32 | +<div style="max-width: 500px; margin-inline: auto;"> |
| 33 | +  |
| 35 | +</div> |
| 36 | + |
| 37 | +As you can see having no image makes your link much less engaging and will result in significantly fewer clicks to your site. |
| 38 | + |
| 39 | +## Creating Dynamic Open Graph Images |
| 40 | + |
| 41 | +If you have read any other articles on how to create dynamic Open Graph images you have probably seen solutions that involve using a headless browser like Puppeteer to render a page and take a screenshot of it. This will work, but I find it is quite slow, doesn't work well with build tools, and is generally more complex than it needs to be. Instead I am going to show you a much simpler approach that you can run in any build process or even just run from the command line to automatically generate every Open Graph image for your site in a matter of milliseconds per image. |
| 42 | + |
| 43 | +<Tangent> |
| 44 | + To put into perspective how quick this process is, I am able to generate all |
| 45 | + 143 Open Graph images for this blog in 40 seconds using the free tier of |
| 46 | + Netlify as part of my build process. Each image takes around 250-300ms to |
| 47 | + generate. On my local machine I can generate all 143 images in under 25 |
| 48 | + seconds. |
| 49 | + <br /> |
| 50 | + If build times were a major concern for you and this still isn't fast enough |
| 51 | + you could set up your build tool to only generate images for new posts or |
| 52 | + posts that have been updated since the last build which would add only a few |
| 53 | + seconds to your build time. |
| 54 | +</Tangent> |
| 55 | + |
| 56 | +The way I am able to create such fast image generation is because we never actually render a full web page. Instead we use a library called [Satori](https://github.com/vercel/satori) which can take JSX and render it to an SVG without needing to spin up a browser. We then follow that up with the a library called [Sharp](https://github.com/lovell/sharp) to convert the SVG to an image format that can be used as an Open Graph image. |
| 57 | + |
| 58 | +Let's take a look at some code for generating a very simple Open Graph image. |
| 59 | + |
| 60 | +```jsx |
| 61 | +const svg = await satori( |
| 62 | + <div |
| 63 | + style={{ |
| 64 | + background: "linear-gradient(orange, blue)", |
| 65 | + width: "100%", |
| 66 | + height: "100%", |
| 67 | + }} |
| 68 | + />, |
| 69 | + { width: 1200, height: 630 }, |
| 70 | +) |
| 71 | +const webp = await sharp(Buffer.from(svg)).webp({ quality: 90 }).toBuffer() |
| 72 | +``` |
| 73 | + |
| 74 | +<div style="max-width: 500px; margin-inline: auto;"> |
| 75 | +  |
| 77 | +</div> |
| 78 | + |
| 79 | +In the above code there are a few sections we need to understand. The first section is the call to `satori`. This function takes two arguments. The first argument is the JSX that we want to render. The second argument is an object that contains options for Satori. You should always set the width and height to `1200` and `630` respectively since that is the recommended size for Open Graph images. We will look at other options later when we deal with fonts. |
| 80 | + |
| 81 | +The second piece of code is the call to `sharp`. This function takes the SVG that was generated by Satori and converts it to a WebP image buffer. You can also convert it to other formats like PNG or JPEG if you prefer, but WebP is a great choice because it has good quality while keeping file sizes small. The `quality` option can be set from `1` to `100` with higher numbers resulting in better quality but larger file sizes. |
| 82 | + |
| 83 | +<Tangent> |
| 84 | + Just because Satori uses JSX does not mean you need to be using React or |
| 85 | + another framework that uses JSX. You could instead write this same code using |
| 86 | + an object that is equivalent to the JSX output. Below is the same code as |
| 87 | + above but written using an object. If you want a more complicated example of |
| 88 | + using objects instead of JSX you can view [the exact code I use for generating |
| 89 | + Open Graph |
| 90 | + images](https://github.com/WebDevSimplified/Web-Dev-Simplified-Official-Blog/blob/master/src/utils/generateOpenGraphImage.js) |
| 91 | + for this blog. |
| 92 | +</Tangent> |
| 93 | + |
| 94 | +```js |
| 95 | +const svg = await satori( |
| 96 | + { |
| 97 | + type: "div", |
| 98 | + props: { |
| 99 | + style: { |
| 100 | + background: "linear-gradient(orange, blue)", |
| 101 | + width: "100%", |
| 102 | + height: "100%", |
| 103 | + }, |
| 104 | + }, |
| 105 | + }, |
| 106 | + { width: 1200, height: 630 }, |
| 107 | +) |
| 108 | + |
| 109 | +const webp = await sharp(Buffer.from(svg)).webp({ quality: 90 }).toBuffer() |
| 110 | +``` |
| 111 | + |
| 112 | +### Adding Text |
| 113 | + |
| 114 | +Most likely you want to add some text to your Open Graph images, which is a bit more complex since we now need to load a font file as well. |
| 115 | + |
| 116 | +```jsx {1,18-25} |
| 117 | +const font = await fs.readFile("path/to/font.ttf") |
| 118 | + |
| 119 | +const svg = await satori( |
| 120 | + <div |
| 121 | + style={{ |
| 122 | + background: "linear-gradient(orange, blue)", |
| 123 | + width: "100%", |
| 124 | + height: "100%", |
| 125 | + fontFamily: "FontName", |
| 126 | + fontSize: "6rem", |
| 127 | + }} |
| 128 | + > |
| 129 | + Hello World |
| 130 | + </div>, |
| 131 | + { |
| 132 | + width: 1200, |
| 133 | + height: 630, |
| 134 | + fonts: [ |
| 135 | + { |
| 136 | + name: "FontName", |
| 137 | + data: font, |
| 138 | + weight: 400, |
| 139 | + style: "normal", |
| 140 | + }, |
| 141 | + ], |
| 142 | + }, |
| 143 | +) |
| 144 | + |
| 145 | +const webp = await sharp(Buffer.from(svg)).webp({ quality: 90 }).toBuffer() |
| 146 | +``` |
| 147 | + |
| 148 | +<div style="max-width: 500px; margin-inline: auto;"> |
| 149 | +  |
| 151 | +</div> |
| 152 | + |
| 153 | +You can pass as many fonts as you want to the `fonts` option in Satori which is great for handling bold and non-bold text. Also, you cannot use variable fonts with Satori so make sure you have a separate font file for each weight you want to use. |
| 154 | + |
| 155 | +### Adding Images |
| 156 | + |
| 157 | +Most likely a plain background gradient isn't enough for your Open Graph images. So we will look at adding images next which need to be loaded and converted to base64 before being passed to Satori. |
| 158 | + |
| 159 | +```jsx {2-5,23,32} |
| 160 | +const font = await fs.readFile("path/to/font.ttf") |
| 161 | +const logo = await fs.readFile("path/to/logo.webp", { encoding: "base64" }) |
| 162 | +const background = await fs.readFile("path/to/background.svg", { |
| 163 | + encoding: "base64", |
| 164 | +}) |
| 165 | + |
| 166 | +const svg = await satori( |
| 167 | + <div |
| 168 | + style={{ |
| 169 | + backgroundImage: "linear-gradient(orange, blue)", |
| 170 | + width: "100%", |
| 171 | + height: "100%", |
| 172 | + fontFamily: "FontName", |
| 173 | + fontSize: "6rem", |
| 174 | + display: "flex", |
| 175 | + flexDirection: "column", |
| 176 | + alignItems: "center", |
| 177 | + position: "relative", |
| 178 | + }} |
| 179 | + > |
| 180 | + <div |
| 181 | + style={{ |
| 182 | + backgroundImage: `url("data:image/svg+xml;base64,${background}")`, |
| 183 | + width: "100%", |
| 184 | + height: "100%", |
| 185 | + position: "absolute", |
| 186 | + inset: 0, |
| 187 | + opacity: 0.4, |
| 188 | + }} |
| 189 | + /> |
| 190 | + <img |
| 191 | + src={`data:image/webp;base64,${image}`} |
| 192 | + style={{ width: "200px", height: "200px" }} |
| 193 | + /> |
| 194 | + <span>Hello World</span> |
| 195 | + </div>, |
| 196 | + { |
| 197 | + width: 1200, |
| 198 | + height: 630, |
| 199 | + fonts: [ |
| 200 | + { |
| 201 | + name: "FontName", |
| 202 | + data: font, |
| 203 | + weight: 400, |
| 204 | + style: "normal", |
| 205 | + }, |
| 206 | + ], |
| 207 | + }, |
| 208 | +) |
| 209 | + |
| 210 | +const webp = await sharp(Buffer.from(svg)).webp({ quality: 90 }).toBuffer() |
| 211 | +``` |
| 212 | + |
| 213 | +<div style="max-width: 500px; margin-inline: auto;"> |
| 214 | +  |
| 216 | +</div> |
| 217 | + |
| 218 | +When you read your images with `fs` you can use the `encoding: "base64"` option to get the image as a base64 string which can then be used in an `img` tag or as a CSS background image. Just make sure you include the correct MIME type in the URL like `data:image/webp;base64,...` or `data:image/svg+xml;base64,...`. |
| 219 | + |
| 220 | +## How To Use Open Graph Images |
| 221 | + |
| 222 | +Now that you can create these images we need to talk about how to render them on your site. If you are using any form of JavaScript based static site generator you can just create a route that generates the image for each page and in that route just return a new response with the correct mime types. For example, if you are using Astro you could use a `og.webp.js` file with `staticPaths` to generate these files dynamically. |
| 223 | + |
| 224 | +```js {4} |
| 225 | +const svg = await satori(/*...*/) |
| 226 | +const webp = await sharp(Buffer.from(svg)).webp({ quality: 90 }).toBuffer() |
| 227 | + |
| 228 | +return new Response(webp, { headers: { "Content-Type": "image/webp" } }) |
| 229 | +``` |
| 230 | + |
| 231 | +### Non-JS Sites |
| 232 | + |
| 233 | +If your site doesn't use JavaScript you can instead just run this code as a JavaScript file from the command line or as part of your build process to generate all of the Open Graph images and save them to disk. You can use `fs.writeFile` to write the image buffer to a file like so: |
| 234 | + |
| 235 | +```js |
| 236 | +import fs from "fs/promises" |
| 237 | + |
| 238 | +const svg = await satori(/*...*/) |
| 239 | +const webp = await sharp(Buffer.from(svg)).webp({ quality: 90 }).toBuffer() |
| 240 | + |
| 241 | +await fs.writeFile("path/to/output.webp", webp) |
| 242 | +``` |
| 243 | + |
| 244 | +## How To Add Open Graph Images to Your Site |
| 245 | + |
| 246 | +The last thing we need to talk about is adding these images to your site. Luckily, this is quite easy. All you need to do is add a few meta tags to the `<head>` of your HTML document. |
| 247 | + |
| 248 | +```html |
| 249 | +<meta |
| 250 | + property="og:title" |
| 251 | + content="How to Create Dynamic Open Graph Images Automatically for Your Site" |
| 252 | +/> |
| 253 | +<meta |
| 254 | + property="og:description" |
| 255 | + content="Learn how to automatically generate dynamic Open Graph images for your website using JavaScript." |
| 256 | +/> |
| 257 | +<meta |
| 258 | + property="og:image" |
| 259 | + content="https://blog.webdevsimplified.com/2025-09/dynamic-og-images/og-image.webp" |
| 260 | +/> |
| 261 | +<meta |
| 262 | + property="og:image:secure_url" |
| 263 | + content="https://blog.webdevsimplified.com/2025-09/dynamic-og-images/og-image.webp" |
| 264 | +/> |
| 265 | +<meta property="og:image:width" content="1200" /> |
| 266 | +<meta property="og:image:height" content="630" /> |
| 267 | + |
| 268 | +<!-- X/Twitter Specific --> |
| 269 | +<meta name="twitter:card" content="summary_large_image" /> |
| 270 | +<meta |
| 271 | + name="twitter:title" |
| 272 | + content="How to Create Dynamic Open Graph Images Automatically for Your Site" |
| 273 | +/> |
| 274 | +<meta |
| 275 | + name="twitter:description" |
| 276 | + content="Learn how to automatically generate dynamic Open Graph images for your website using JavaScript." |
| 277 | +/> |
| 278 | +<meta |
| 279 | + name="twitter:image" |
| 280 | + content="https://blog.webdevsimplified.com/2025-09/dynamic-og-images/og-image.webp" |
| 281 | +/> |
| 282 | +``` |
| 283 | + |
| 284 | +The most important tags are `og:image` and `twitter:image` since those are what actually specify the image to use. The `og:title`, `og:description`, `twitter:title`, and `twitter:description` tags are also important since they specify the title and description to use when your link is shared. The other tags are optional but can help improve how your link looks when shared. |
| 285 | + |
| 286 | +## Conclusion |
| 287 | + |
| 288 | +Creating dynamic Open Graph images is a great way to improve the appearance of your links when shared on social media and other platforms. By using `satori` and `sharp` you can easily generate these images automatically as part of your build process or on demand. This approach is fast, flexible, and works with any site regardless of the technology stack you are using. |
| 289 | + |
| 290 | +If you want to see a full example of how I generate Open Graph images for this blog you can check out [the code on GitHub](https://github.com/WebDevSimplified/Web-Dev-Simplified-Official-Blog/blob/master/src/utils/generateOpenGraphImage.js). |
0 commit comments