Open Graph images
Tags: node, webdev, tutorial
Published: 2022-12-04 12:07:47

![A Twitter social card example](https://screenshotone.com/blog/open-graph-images/twitter_example_hua88c2d1fef7550a6df53ba0803d408b5_917790_900x0_resize_box_3.png)

Also known as social cards or link previews, these images make a difference between the click and the visit to your site or ignoring it. Boost your brand awareness by generating Open Graph images.

In this article, I share with you a few quick ways how you can easily add support the Open Graph images to your site.

## If you already have an image

If you already have an image for social media, upload it to your server or any other file hosting and add the following tags to your HTML head section:

```html
<!-- Open Graph / Meta (Facebook) -->
<meta property="og:type" content="website">
<meta property="og:url" content="https://example.com/">
<meta property="og:title" content="Your title">
<meta property="og:description" content="Your description">
<meta property="og:image" content="https://example.com/ogimage.png">

<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image">
<meta property="twitter:url" content="https://example.com">
<meta property="twitter:title" content="Your title">
<meta property="twitter:description" content="Your description">
<meta property="twitter:image" content="https://example.com/ogimage.png">
```

There are [a few requirements and recommendations](#open-graph-image-requirements) to avoid common pitfalls:
1. Don’t use a relative URL. Always use an absolute URL of the images in tags. Not every social network or app knows how to resolve it.
2. The recommended image size is 1200x630px for most social networks, and the file size must not exceed a few hundred kilobytes.
  
And there are a few downsides to using static OG images:

1. They are not updated dynamically to reflect the updates on a page.
2. You need manually set up OG images for different pages.
3. You need to spend time and periodically update them.

These downsides can be solved by generating images dynamically.

## Generate open graph images dynamically

There are two ways to generate Open Graph images dynamically: building a custom solution with a rendering engine like Puppeteer or using a well-established API or product that solves that problem.

What to choose depends on your preferences and resource limitations. If you like building and writing code, go with a custom solution. If you are short on time, go with a proven solution.

### Generate Open Graph images with a custom solution

You can draw pictures using low-level libraries or render HTML with browsers and take screenshots. There are so many ways to generate images dynamically.

I will show quickly two options on how to easily render HTML as an image for open graph images:

1. Using [Puppeteer](https://github.com/puppeteer/puppeteer);
2. And using [satori by Vercel](https://github.com/vercel/satori).

Why HTML as images? Because it is a declarative language that is easy to read and edit. In addition to that, you can use modern CSS frameworks to style your OG image quickly. And you can easily scale elements to larger or lesser image sizes if needed.

### Using Puppeteer to generate OG images

> I have written [a complete guide on how to render URLs or HTML as images (screenshots) with Puppeteer](https://screenshotone.com/blog/how-to-take-a-screenshot-with-puppeteer/).

Before rendering an HTML, let's create a simple example of a dynamic HTML template for our OG image:

```html
<!doctype html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-violet-700 h-screen flex">
<div class="m-auto">
    <h1 class="text-white text-7xl font-serif"><span class="underline decoration-sky-500">Open Graph</span> images</h1>
    <h2 class="text-white text-3xl mt-5 font-light">Are easy and powerful way to increase your brand awarness</h2>
</div>
</body>
</html>
```

And now let's render it with `Puppeteer`:

```javascript
'use strict';

// npm i puppeteer
import puppeteer from 'puppeteer';

(async () => {
    const content = `<!doctype html>
    <html>
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <script src="https://cdn.tailwindcss.com"></script>
    </head>
     <body class="bg-violet-700 h-screen flex">
        <div class="m-auto">
         <h1 class="text-white text-7xl font-serif"><span class="underline decoration-sky-500">Open Graph</span> images</h1>
         <h2 class="text-white text-3xl mt-5 font-light">Are easy and powerful way to increase your brand awareness</h2>
       </div>
    </body>
    </html>`;

    const browser = await puppeteer.launch();
    try {
        const page = await browser.newPage();
        await page.setViewport({ width: 1200, height: 630, deviceScaleFactor: 1 })
        await page.setContent(content);
        await page.screenshot({ path: 'screenshot.png' });
    } catch (e) {
        console.log(e)
    } finally {
        await browser.close();
    }
})();
```

And the result is:

![The result of rendering](https://screenshotone.com/blog/open-graph-images/screenshot_huf06fb5d250fa90d085e89ab07bb8ffbb_34672_900x0_resize_box_3.png)

Let's create a simple API around it:

```javascript
'use strict';

// npm i puppeteer
import puppeteer from 'puppeteer';
// npm i express
import express from 'express';

const template = (prefix, title, subtitle) => `<!doctype html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <script src="https://cdn.tailwindcss.com"></script>
</head>
 <body class="bg-violet-700 h-screen flex">
    <div class="m-auto">
     <h1 class="text-white text-7xl font-serif"><span class="underline decoration-sky-500">${prefix}</span>${title}</h1>
     <h2 class="text-white text-3xl mt-5 font-light">${subtitle}</h2>
   </div>
</body>
</html>`;

const app = express();

app.get('/ogi', async function (req, res) {
    const content = template(req.query.prefix, req.query.title, req.query.subtitle);

    const browser = await puppeteer.launch();
    try {
        const page = await browser.newPage();
        await page.setViewport({ width: 1200, height: 630, deviceScaleFactor: 1 })
        await page.setContent(content);
        const image = await page.screenshot({ type: 'png', encoding: 'binary' });

        res.set('Content-Type', 'image/png');
        res.send(image);
    } catch (e) {
        console.error(e);
        res.status(500).send('Failed to render the Open Graph image, see error logs');
    } finally {
        await browser.close();
    }
})

app.listen(3000);
```

>  I start and close a browser on every request because these requests from social networks might be rare if the number of pages you have is minimal. 
> 
> But running a browser in the background might cause many issues—it is a complex case to handle. 

Let's test it:

```
http://localhost:3000/ogi?prefix=Open Graph&title= images&subtitle=are easy and powerful way to increase your awareness
```

And the result is: 

![The result of rendering](https://screenshotone.com/blog/open-graph-images/screenshot_huf06fb5d250fa90d085e89ab07bb8ffbb_34672_900x0_resize_box_3.png)

Once it works, specify URLs in your HTML meta tags:

```html
<!-- Open Graph / Meta (Facebook) -->
<meta property="og:type" content="website">
<meta property="og:url" content="https://example.com/">
<meta property="og:title" content="Your title">
<meta property="og:description" content="Your description">
<meta property="og:image" content="https://example.com/ogi?prefix=Open%20Graph&title=%20images&subtitle=are%20easy%20and%20powerful%20way%20to%20increase%20your%20awareness">

<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image">
<meta property="twitter:url" content="https://example.com">
<meta property="twitter:title" content="Your title">
<meta property="twitter:description" content="Your description">
<meta property="twitter:image" content="https://example.com/ogi?prefix=Open%20Graph&title=%20images&subtitle=are%20easy%20and%20powerful%20way%20to%20increase%20your%20awareness">
```

Use your domain where you deployed the API instead of `example.com`.

And the last is you need to deploy an API or integrate it as part of your app. But be careful because deploying Puppeteer manually to a server has a few pitfalls that you should be aware of:

1. It might have memory leaks.
2. It consumes a lot of CPU and memory.
3. It might periodically crash.

There is a new and lightweight alternative tailored for rendering OG images—satori by Vercel. Or, if you don't want to deal with infrastructure and code, feel free to jump to the section about [how to use API to render HTML](#using-simple-and-tailored-solution-for-open-graph-images).

### Using satori by Vercel to generate OG images

[satori](https://github.com/vercel/satori) is an engine made by Vercel to generate SVG images from simple HTML code that can be used in many use cases. And Vercel provides native support for satori to render Open Graph images.

If you need to generate OG images for your [Next.js](https://nextjs.org/) app, consider hosting your app on Vercel and using their OG image solution.

Install `satori` and friends:

```
npm i satori satori-html @resvg/resvg-js
```

Let's render a simple HTML:

```javascript
'use strict';

import satori from 'satori';
import { Resvg } from '@resvg/resvg-js';
import { html as toReactElement } from 'satori-html';
// npm i express
import express from 'express';
import fsPromises from 'fs/promises';

const template = (prefix, title, subtitle) => `<div style="background-color: rgb(109 40 217); color: white; height: 100vh; width: 100%; display: flex; align-items: center; justify-content: center; flex-direction: column">    
    <h1 style="font-size: 40px">${prefix}${title}</h1>
    <h2 style="font-size: 20px">${subtitle}</h2>
</div>`;

const app = express();

// at least one font file is required
const fontFile = await fsPromises.readFile('inter-latin-ext-700-normal.woff');
const font = fontFile;

app.get('/ogi', async function (req, res) {
    const content = template(req.query.prefix, req.query.title, req.query.subtitle);

    try {
        const width = 1200;

        const svg = await satori(toReactElement(content), {
            width,
            height: 630,
            fonts: [
                {
                    name: 'Inter Latin',
                    data: font,
                    style: 'normal'
                }
            ],
        });

        const resvg = new Resvg(svg, {
            fitTo: {
                mode: 'width',
                value: width
            }
        });

        const image = resvg.render();

        res.set('Content-Type', 'image/png');
        res.send(image.asPng());
    } catch (e) {
        console.error(e);
        res.status(500).send('Failed to render the Open Graph image, see error logs');
    }
})

app.listen(3000);
```

> 👉 To properly render the example, download [the font](https://screenshotone.com/blog/open-graph-images/inter-latin-ext-700-normal.woff) and place it in the same directory as the script. 

And for a request:

```
http://localhost:3000/ogi?prefix=Open%20Graph&title=%20images&subtitle=are%20easy%20and%20powerful%20way%20to%20increase%20your%20awareness
```

The result is:

![The result from satori](https://screenshotone.com/blog/open-graph-images/satori_image_hu9d1b9f61835ecbecf284e4aa91e200ba_16486_900x0_resize_box_3.png)

And updating the HTML meta tags:

```html
<!-- Open Graph / Meta (Facebook) -->
<meta property="og:type" content="website">
<meta property="og:url" content="https://example.com/">
<meta property="og:title" content="Your title">
<meta property="og:description" content="Your description">
<meta property="og:image" content="https://example.com/ogi?prefix=Open%20Graph&title=%20images&subtitle=are%20easy%20and%20powerful%20way%20to%20increase%20your%20awareness">

<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image">
<meta property="twitter:url" content="https://example.com">
<meta property="twitter:title" content="Your title">
<meta property="twitter:description" content="Your description">
<meta property="twitter:image" content="https://example.com/ogi?prefix=Open%20Graph&title=%20images&subtitle=are%20easy%20and%20powerful%20way%20to%20increase%20your%20awareness">
```

Use your domain where you deployed the API instead of `example.com`.

The beauty of using `satori` instead of Puppeteer is that it works faster, consumes less memory, and can be embedded into your main app.

But the downside is that satori is a comparatively new library, so it might have a lot of rendering issues, and it doesn't fully support all the latest HTML and CSS properties. If your rendering capabilities for you are enough to use satori, I would advise going with it. 
But if you want to make sure that everything works like a charm and supports the latest HTML features, I would advice using an established API to render images.

### Using simple and tailored solution for Open Graph images

In case you want to save time and money and don't want to deal with HTML rendering engines and headless browsers. You can use a well-established screenshot API to generate OG images—[ScreenshotOne](https://screenshotone.com/).

Let's examine how easier it is use an API rather Puppeteer or rendering engine like satori. 

First, sign up at [ScreenshotOne](https://screenshotone.com/) to get an API key. Then install [an official SDK for JavaScript](https://screenshotone.com/docs/code-examples/#javascript-and-typescript-nodejs), but it is [available for many languages](https://screenshotone.com/docs/code-examples/):

```
npm i screenshotone-api-sdk
``` 

Let's render an image:

```javascript
import * as screenshotone from 'screenshotone-api-sdk';

// create API client 
const client = new screenshotone.Client('M73PaLOC8lchKQ', 'VjuscNqAfGCC4A');

const template = (prefix, title, subtitle) => `<!doctype html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <script src="https://cdn.tailwindcss.com"></script>
</head>
 <body class="bg-violet-700 h-screen flex">
    <div class="m-auto">
     <h1 class="text-white text-7xl font-serif"><span class="underline decoration-sky-500">${prefix}</span>${title}</h1>
     <h2 class="text-white text-3xl mt-5 font-light">${subtitle}</h2>
   </div>
</body>
</html>`;

const generateOGImageURL = (prefix, title, subtitle) => client.generateTakeURL(screenshotone.TakeOptions.html(template(prefix, title, subtitle)));

const url = generateOGImageURL('Open Graph', ' images', 'Are easy and powerful way to increase your brand awareness');
console.log(url);
// expected output: https://api.screenshotone.com/take?html=%3C%21doctype+html%3E%0...
```

The result is: 

![The result of rendering](https://screenshotone.com/blog/open-graph-images/screenshot_huf06fb5d250fa90d085e89ab07bb8ffbb_34672_900x0_resize_box_3.png)

> 👉 I hide the chat widgets to make a clean screenshot. If needed, you can also [block ads](https://screenshotone.com/docs/options/#block_ads) and [block cookie banners](https://screenshotone.com/docs/options/#block_cookie_banners).

Let's update the meta tags: 

```html
<!-- Open Graph / Meta (Facebook) -->
<!-- ... other Open Graph tags -->
<!-- Use your template engine to render the URL. You don't need API routes, browsers, engines, or additional libraries. -->
<meta property="og:image" content="{{ generateOGImageURL('Open Graph', ' images', 'Are easy and powerful way to increase your brand awareness')  }}">

<!-- Twitter -->
<!-- ... other Twitter Graph tags -->
<!-- Use your template engine to render the URL. You don't need API routes, browsers, engines, or additional libraries. -->
<meta property="twitter:image" content="{{ generateOGImageURL('Open Graph', ' images', 'Are easy and powerful way to increase your brand awareness') }}">
```

You don't need hosting, API routes, handling memory issues, and care about bugs. Most of the problems are already covered. 

You can also dynamically generate an OG image based on the site's screenshot or selector. You can [block cookie banners, ads, and chats]() to make the screenshot look clean. . 

It is a headache-less API. [Sign up](https://screenshotone.com/). It is free to start.

## Test Open Graph images

Once you finish your solution, test it, and it works fine. Test and validate your open graph images with official debuggers:

1. Use a [debugger from Meta (Facebook)](https://developers.facebook.com/tools/debug/).
2. Try a [social card validator from Twitter](https://cards-dev.twitter.com/validator).
3. Test with a [LinkedIn Post Inspector](https://www.linkedin.com/post-inspector/).
4. Use a [Pinterest URL debugger](https://developers.pinterest.com/tools/url-debugger/).
5. For Telegram, WhatsApp or iMessage, send the link to your friend.

## Known issues 

I encountered many issues while rendering and working with OG images. And I want to share them with you.

### Open Graph image requirements

The recommended image size is 1200x630px for most social networks, and the file size must not exceed a few hundred kilobytes.

### Refresh cache for OG image in Telegram

In order to refresh the cache if the image has been changed,  go to [@webpagebot](https://telegram.me/webpagebot) and send the URL (10 is maximum) you want to clear the cache for. 

Telegram servers automatically will scan your site and generate a new Open Graph image preview, site name, and description.

Remember to have the OG prefix in your `HTML` tag as: `<html prefix="og: http://ogp.me/ns#">` or the Telegram bot will not update the graph cache.

### Always use an absolute URL

It might be a simple task for you, but many apps don't crawl an OG image path right if it is specified as relative.

Always specify an absolute URL, including the host and protocol, to the image:

```html
<!-- Not this: -->
<meta property="og:image" content="/image.png">
<meta property="twitter:image" content="/image.png">

<!-- But this: -->
<meta property="og:image" content="https://example.com/image.png">
<meta property="twitter:image" content="https://example.com/image.png">
```

## A few tips on the social card design

I prepared a few tips that you might find helpful to increase the click rate for the link to your site on social media networks. They are not proven to be always working, it is not a science, and you need to test it.

### Place a button on the image

Many reported that placing a button on the OG image increased click through rate. 

![A button example](https://screenshotone.com/blog/open-graph-images/button_example_hu17f90ae2ba11801c0ee0fafc3e193500_668988_900x0_resize_box_3.png)

Nothing is guaranteed. You may want to test it. 

### Increase the font size

Unless it is an intention or design effect to attract attention, don't use small fonts, and make the font size as large as possible. Test if you can read the text on the resulting image. 

### Brand colors and fonts

Obvious, but many forget to reuse their brand colors and fonts. Social cards aim to increase your brand awareness and, if possible, to invite users for a click. 

### Adapt to social networks if possible

Always test how it looks in social networks, white and back styles. 

And you might try to adapt to Twitter because twitter has a special meta tag. You can try to play with Twitter colors and button styles to make it feels native. 

## A closing thought

In case you like building, I advise you to use Puppeteer or satori from Vercel (especially if you use Next.js). But if you want a quick and hassle-free solution, feel free to use [ScreenshotOne API](https://screenshotone.com/), which is ready to render any kind of HTML for your needs.
I hope you enjoyed our journey to link previews and how to generate them.

Have a nice day!