# How to generate share images (OG) with code

> Generate the image shown when a link is shared without opening a design editor: next/og, brand fonts and a script that creates one per article and per language.

URL: https://codegea.com/en/generate-share-images-og-with-code
Web · 2026-10-18 · Derian Pinto

When you share a link on WhatsApp, LinkedIn or X, the network shows an image. If you don't define one, it shows something random or nothing. Making them by hand for every article is a chore nobody keeps up. We generate them with code: one per article and per language, with the headline inside and the brand.

That image is the page's `og:image`. It is no longer shown inside the article, where it repeated the title right above it, but it is still what people see when they share the link.

## The tools

- **`next/og`**: the `ImageResponse` class, which turns a tree of React elements into an image. Under the hood it uses Satori.
- **`sharp`**: to convert that image to WebP and compress it.
- **A script** that walks the articles and writes the files.

You could use Next.js's special `opengraph-image` file, which generates the image when it is requested. We chose a script for a practical reason: this way the image is a file, it is uploaded to the CMS media library and can be reused as `meta.image`, with no computation on every request.

## Step 1: the design is a component

The cover is a React component with inline styles. A minimal version:

```ts
function Cover({ title }: { title: string }) {
  return (
    <div
      style={{
        width: '100%',
        height: '100%',
        display: 'flex',
        flexDirection: 'column',
        justifyContent: 'space-between',
        padding: '80px 96px',
        background: '#0B0F1A',
        color: '#EEF1F7',
      }}
    >
      <span style={{ fontFamily: 'Mono', fontSize: 28 }}>codegea</span>
      <div style={{ fontFamily: 'Sans', fontWeight: 500, fontSize: 112 }}>{title}</div>
    </div>
  );
}
```

Satori understands a subset of CSS: layout with **flexbox**, no grid and not many modern properties. If something doesn't show, it is almost always because a `display: 'flex'` is missing.

## Step 2: fonts

Satori doesn't use system fonts: you have to pass it the files. We download the three brand fonts once and store them in a cache folder:

```ts
const fonts = await loadFonts(CACHE);
// [{ name: 'Sans', data, weight: 500 }, { name: 'Serif', … }, { name: 'Mono', … }]
```

Then you hand them to `ImageResponse` in the `fonts` option and use them by name in `fontFamily`.

## Step 3: from component to file

```ts
const res = new ImageResponse(<Cover title={title} />, {
  width: 1600,
  height: 900,
  fonts,
});
const png = Buffer.from(await res.arrayBuffer());
await sharp(png).webp({ quality: 84 }).toFile(`covers/${slug}.${locale}.webp`);
```

One detail: Satori generates a PNG, which is quite heavy. Running it through `sharp` to WebP leaves it at a fraction of the size. We use 1600×900; for social networks a landscape format with a ratio near 1.91:1 is recommended (for example, 1200×630). If you prefer that format, change the dimensions.

## Step 4: the headline with a highlighted word

The site's headlines mark one word with asterisks to show it in serif italic: `How to generate *share images* with code`. A function splits the text into normal and highlighted words, and the component applies another style to the latter:

```ts
{words(title).map((w, i) => (
  <span key={i} style={w.em ? { fontFamily: 'Serif', fontStyle: 'italic', color: '#5C7CF5' } : {}}>
    {w.text}
  </span>
))}
```

## Step 5: one per article and per language

The script reads each article's *frontmatter*, takes `coverTitle` (or `title` if there isn't one), calculates the reading minutes from the body and generates the image for each language. Then it writes the file path into the article itself:

```bash
pnpm covers                # only the ones without a cover
pnpm covers my-article     # regenerate one, in es and en
pnpm covers --force        # all of them
```

Since each image carries the headline inside, there is one per language; the same one doesn't work for the Spanish and English article.

## What we learned

- **Long text gets cut off.** We adjust the font size depending on how many characters the headline has. Without that, a long title spills out of the frame.
- **Test the image on the networks.** Each network caches the previous image; to see the change you have to ask them to re-read the link with their debugging tools.
- **Don't use third-party photos or other people's logos.** A branded image with typography is enough and avoids rights problems.

To see how they connect with the rest of the site, look at [how codegea.com is built](/en/how-codegea-is-built-nextjs-payload-turso-r2), and to review the rest of your pages' SEO, [titles and meta descriptions that raise CTR](/en/titles-and-meta-descriptions-that-raise-your-ctr).
