How to generate dynamic OG (OpenGraph) images with Satori and React
Tags: webdev, react, tutorial, programming
Published: 2024-06-27 12:16:25

Have you ever seen those great images that appear when you share any link through social media? Twitter, WhatsApp, Discord, Facebook, Slack, and everywhere display those images as a way to aggregate some context value for all the links you share. For example:

![Example of links](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pnc940pc1s8g6w2wyeon.png)

At Woovi, one of our great features is our payment link. A link that you can share with your customers and let them have access to all data related to their charges. Let the customers see the QR code to pay the Pix, the value of the charge, and any other relevant information that could be useful for them.

![Example of a payment link](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yq9yj8o941fj8uxtvts5.png)

## Sharing the Payment Link

But, what happens when we get this payment link, for example, and share it in some place? What we would see was just the QR code related to that Pix, like the example below:

![Old payment link](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2i8zugfw878kn6dzbaw5.png)

But, this isn't a nice thing to see at all, do you agree? Isn't give any useful information, and I, being the customer, doesn't appreciate it. Further, the fact that, in some specific cases, they can't render the QR code because the image is greater than expected (WhatsApp, for example, only accepts 1200x630 OG images).

With that idea in mind, we put a great effort into implementing a new way to generate our OG images in runtime. The idea was: to dynamically generate the image according to some specific data, with that, we can have a better image that lets the customer appreciate what they're seeing.

## Using Satori to Generate the Image

Under the hood, we would use [Satori](https://github.com/vercel/satori) as the solution that will generate the SVG for us. We would need these things:

- An endpoint that will get the request to generate the image
- Satori to create the SVG

It's simple, right? The idea was to get all necessary data through this endpoint and pass it to a React component that will be rendered by Satori and converted into an SVG. Too easy, right?

## Implementing the endpoint

The endpoint is simple and is like any other endpoint that you already implemented. Based on our stack, we will be using [Koa.js](https://koajs.com/) to implement it:

```ts
import { Router } from '@koa/router';
import type { Context } from 'koa';

const router = new Router();

routerOpenPix.get(
  '/api/link/og/:paymentLinkID{.png}?',
  paymentLinkOgImageGet,
);

function paymentLinkOgImageGet(ctx: Context) {
  // let's implement it in another moment, okay?
}
```

The idea of this endpoint is simple: get the param `paymentLinkID` and find all the data through our materialized data into our database and pass it to the Satori.

## Rendenring the SVG

Now that we already implemented the endpoint, we can follow the usage of the Satori to render our image for us:

```tsx
import type { Context } from 'koa';
import satori from 'satori';

async function paymentLinkOgImageGet(ctx: Context) {
  const { paymentLinkID } = ctx.params;

  const paymentLink = await getPaymentLinkByID(paymentLinkID);

  if (!paymentLink) {
    ctx.status = 400;
    ctx.body = 'Error';
    return;
  }

  let svg: string | undefined;

  try {
    const font = await loadFontInArrayBuffer();

    svg = await satori(
      <div style={{ width: 1200, height: 630 }}>
        <h1>{paymentLink.title}</h1>
      </div>,
     {
       fonts: [
        {
          name: 'Font',
          data: font,
          style: 'normal',
          weight: 400,
        },
      ],
    });

    ctx.set('Content-Type', 'image/png');
    ctx.status = 200;
    ctx.body = svg;
  catch (err) {
    ctx.status = 400;
    ctx.body = err;
  }
}
```

I'll explain to you what we're doing here:

1. We're finding and validating if the `paymentLink` really exists.
2. Satori requires at least 1 font loaded as an array buffer, so we're loading one here. In that case, you can follow your preferrable way to do it. Load a font from Google Fonts, get it from local assets, and do what you prefer here.
3. We're calling the `satori` function that will render the SVG for us based on the JSX that we pass (under the hood, they're converting it into an object).

Just a specific detail about CSS on Satori. Satori under the hood uses Yoga to render things, so they can't accept the entire set of CSS rules, just a subset of them. You can see [here](https://github.com/vercel/satori?tab=readme-ov-file#css) all the rules that are being accepted by them.

If everything goes right, you can access this endpoint and validate that the SVG has been right rendered for you.

If necessary, you will need to return it as an image by itself. For that, we can use the [`sharp`](https://www.npmjs.com/package/sharp) to handle it for yourself. See the example below:

```tsx
  const svg = await satori(); // render the svg
  const img = await sharp(Buffer.from(utf8.decode(svg)))
    .png()
    .toBuffer()
```

## Adding the metatag on HTML

Now that you already rendered the new OG, you will need to add this into your HTML to ensure that all crawlers will get it.

For that, you just need to add two new tags to your `<head>` tag. See below:

```html
<!DOCTYPE html>
<head>
  <meta property='og:image' content="https://yoururl.com/api/link/og/<dynamic-payment-link-id>.png" />
  <meta property='twitter:image' content="https://yoururl.com/api/link/og/<dynamic-payment-link-id>.png" />
</head>
<!-- ... -->
```

As already commented in another post, [we render our entire payment link using an SSR strategy](https://dev.to/woovi/server-side-rendering-ssr-from-scratch-with-react-19jm), so we can insert the `<dynamic-payment-link-id>` properly just getting their identifier. You'll need to validate what is the better way to do it by your side.

Both `og:image` and `twitter: image` metatags will ensure that the new OG image will be rendered in all social media and chats.

## Conclusion

With that implementation, we go from that old QR code that was being rendered to our customers, to this new one below:

![New payment link OG](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y3vk2rzjw3m9devr4bjb.png)

With that new OG, we can: get the merchant's name, how much the customer will pay for them, the expiration date, and the QR code in a more readable way.

Further, by the fact that this is dynamically rendered, we can improve our OGs in runtime to render other states like payment link paid, expired, and different scenarios too.

This is how powerful OGs can be in general.

---

Visit us at [Woovi](https://woovi.com)!

Photo by <a href="https://unsplash.com/@hassaanhre?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash">Hassaan Here</a> on <a href="https://unsplash.com/photos/a-black-and-white-photo-of-wavy-lines-8Ed7ldybPa0?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash">Unsplash</a>