How to generate dynamic open graph images
Published: 2021-10-18 15:34:41

---
title: How to generate dynamic open graph images
published: true
date: 2021-10-17 11:17:29 UTC
tags: 
canonical_url: https://www.janasundar.dev/posts/how-to-generate-dynamic-open-graph-images
---

> This post was originally published at this [blog](https://www.janasundar.dev/posts/how-to-generate-dynamic-open-graph-images)

## Introduction

In this article, we will see how to generate dynamic open graph images. You might be wondering what an open graph image is? Whenever you share a link in Twitter, Discord, or other applications. A fancy card image / link preview is displayed and that image is called an open graph image / OG image.

### Requirements

To generate an OG image, we'll be using these npm packages.

- [puppeteer-core](https://www.npmjs.com/package/puppeteer-core)
- [chrome-aws-lambda](https://www.npmjs.com/package/chrome-aws-lambda)

## Installing Requirements

Do you have [yarn](https://yarnpkg.com/getting-started/install)? If not, then install it and run the below command or else use npm

`yarn add puppeteer-core chrome-aws-lambda`

## Creating your OG Image

Before creating this function, If you're planning to deploy this project in vercel. I'll suggest everyone create a separate project. Otherwise, we may face this issue :point\_down:

> If we integrate this function with our existing project and deploy it in vercel. There is a chance that we might get an error message that the serverless function dependency package compressed size exceeds 50Mb, and then it stops the deployment process.

![Screenshot 2021-10-16 at 3.14.28 PM.png](https://media.graphcms.com/ZUmY9fUTRcKPOWOAKTsH)

To get an idea, we'll see how our final output looks like

[https://og-image.janasundar.dev/api/ogimage?title=How i built my blog with Next Js&tags=javascript,jsx,nextjs](https://og-image.janasundar.dev/api/ogimage?title=How%20i%20built%20my%20blog%20with%20Next%20Js&tags=javascript,jsx,nextjs)

![Screenshot 2021-10-17 at 4.41.13 PM.png](https://media.graphcms.com/QkWoqJXlQ66SIh3hsjWE)

From the above Url and image, we understand that the dynamic data are passed as the query parameters to generate an image.

So now we need a function that takes a screenshot of our content and passes it as a response. This can be achieved as follows

```js
import chalk from 'chalk';
import { getContent, getCss } from '../../utils/getContent';
import { getPage } from '../../utils/getPage';
import puppeteer from 'puppeteer-core';
import chrome from 'chrome-aws-lambda';

let page;

const isDev = process.env.NODE_ENV === 'development';

const exePath =
  process.platform === 'win32'
    ? 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe'
    : process.platform === 'linux'
    ? '/usr/bin/google-chrome'
    : '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';

export const getPage = async () => {
  if (page) {
    return page;
  }

  const getOptions = async () => {
    let options;
    if (isDev) {
      options = {
        args: [],
        executablePath: exePath,
        headless: true,
      };
    } else {
      options = {
        args: chrome.args,
        executablePath: await chrome.executablePath,
        headless: chrome.headless,
      };
    }

    return options;
  };

  const options = await getOptions();
  const browser = await puppeteer.launch({
    ...options,
  });
  page = await browser.newPage();

  return page;
};


export default async function handler(req, res) {
  console.info(chalk.cyan('info'), ` - Generating Opengraph images`);

  const { title, tags, handle, logo, debug, fontFamily, background, fontFamilyUrl } = req.query;

  const css = getCss(fontFamily, fontFamilyUrl, background);
  const html = getContent(tags, title, handle, logo, css);

  if (debug === 'true') {
    res.setHeader('Content-Type', 'text/html');
    res.end(html);
    return;
  }

  try {
    const page = await getPage();
    await page.setViewport({ width: 1200, height: 630 });
    await page.setContent(html, { waitUntil: 'networkidle2', timeout: 15000 });

    await page.evaluateHandle('document.fonts.ready');

    const buffer = await page.screenshot({ type: 'png', clip: { x: 0, y: 0, width: 1200, height: 630 } });

    res.setHeader('Cache-Control', `public, immutable, no-transform, s-maxage=31536000, max-age=31536000`);
    res.setHeader('Content-Type', 'image/png');
    res.end(buffer);
  } catch (error) {
    console.error(error);

    res.statusCode = 500;
    res.setHeader('Content-Type', 'text/html');
    res.end('<h1>Internal Error</h1><p>Sorry, there was a problem</p>');
  }
}

```

Let's take a closer look at our code.

```js
// getPage

import puppeteer from 'puppeteer-core';
import chrome from 'chrome-aws-lambda';

let page;

const isDev = process.env.NODE_ENV === 'development';

const exePath =
  process.platform === 'win32'
    ? 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe'
    : process.platform === 'linux'
    ? '/usr/bin/google-chrome'
    : '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';

export const getPage = async () => {
  if (page) {
    return page;
  }

  const getOptions = async () => {
    let options;
    if (isDev) {
      options = {
        args: [],
        executablePath: exePath,
        headless: true,
      };
    } else {
      options = {
        args: chrome.args,
        executablePath: await chrome.executablePath,
        headless: chrome.headless,
      };
    }

    return options;
  };

  const options = await getOptions();
  const browser = await puppeteer.launch({
    ...options,
  });
  page = await browser.newPage();

  return page;
};


```

Our getPage function launches the browser and gives us a reference to the browser page. To take a screenshot, we're using the puppeteer-core and chrome-aws-lambda package. If you don't know what puppeteer and chrome-aws-lambda are. Refer to the official docs link in the reference section.

### Reference

- [https://github.com/puppeteer/puppeteer#puppeteer-core](https://github.com/puppeteer/puppeteer#puppeteer-core)
- [https://github.com/alixaxel/chrome-aws-lambda](https://github.com/alixaxel/chrome-aws-lambda)
- [https://github.com/puppeteer/puppeteer/blob/main/docs/api.md#puppeteer-vs-puppeteer-core](https://github.com/puppeteer/puppeteer/blob/main/docs/api.md#puppeteer-vs-puppeteer-core)

```js
export const getAbsoluteURL = (path) => {
  const baseURL = process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : 'http://localhost:3000';
  return baseURL + path;
};

// getCss

export const getCss = (fontFamily, fontFamilyUrl, background) => {
  return `
    ${fontFamilyUrl ?? "@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@400;600&display=fallback');"}

    * {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
    font-family: ${
      fontFamily ?? 'Nunito'
    }, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans','Helvetica Neue', sans-serif;
    color: white;
    }

    .container {
    width: 1200px;
    height: 630px;
    background: ${background ? background :  `url(${getAbsoluteURL('/ogbackground.svg')})`};
    padding:3rem;
    margin:0 auto;
    display: flex;
    flex-direction:column;
    }

    .content {
    padding: 3rem 5rem;
    display: flex;
    flex: 1;
    flex-direction: column;
    justify-content: space-between;
    }

    .title {
    display: flex;
    align-items: center;
    justify-content: center;
    max-width: 840px;
    flex: 1;
    margin:0 auto;
    text-align: center;
    }

    .title > h1 {
    font-size: 64px;
    line-height: 74px;
    font-weight: 600;
    font-style: normal;
    
    }

    .logo {
    justify-content: space-between;
    display: flex;
    align-items: center;
    padding: 1rem 3rem;
    }

    .tags {
    font-size: 1rem;
    display: flex;
    gap: 10px;
    justify-content: center;
    padding: 2rem 0;
    }

    .pill{
      background: #caa8ff33;
      color: white;
      padding: 0.25rem 1rem;
      border-radius: 50rem;
      text-transform: capitalize;
      box-shadow: 0 0 1rem rgba(0,0,0,0.1);
      font-weight: bold;
    }

    .handle{
      font-size: 24px;
      font-weight: 600;
    }

    `;
};

```

Our getCss function gets all the styles of our og card. It'll take three optional parameters to generate CSS.

```js 
// getContent
export const getContent = (tags, title, handle, logo, css) => {
  return `
    <html>
    <meta charset="utf-8">
    <title>Generated Image</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <style>
       ${css}
    </style>
    <body>
        <div class='container'>

        <div class="content">
            <div class="title"><h1>${title ?? 'Welcome to this site'}</h1></div>
            ${
              tags
                ? `<div class="tags">
                  ${tags
                    .split(',')
                    .map((tag) => {
                      return `<span key=${tag} class="pill">${tag}</span>`;
                    })
                    .join('')}
                </div>`
                : ''
            }
            
        </div>
        <div class="logo">
            <img src="${logo ?? getAbsoluteURL(`/logo.svg`)}" alt="logo" width="100px" height="100px" >
            <div class="handle">${handle ?? '@Jana__Sundar'}</div>
        </div>
        </div>
    </body>
    </html>`;
};

```

Our getContent function generate Html based on the CSS, title, twitter handle, logo, and tags.

Generally, the most recommended dimensions to generate an OG image is 1200 x 630. These are not the perfect values. Check this link to find the different recommendations.

### Reference

- [https://iamturns.com/open-graph-image-size/](https://iamturns.com/open-graph-image-size/)

```js
  try {
    const page = await getPage();
    await page.setViewport({ width: 1200, height: 630 });
    await page.setContent(html, { waitUntil: 'networkidle2', timeout: 15000 });

    await page.evaluateHandle('document.fonts.ready');

    const buffer = await page.screenshot({ type: 'png', clip: { x: 0, y: 0, width: 1200, height: 630 } });

    res.setHeader('Cache-Control', `public, immutable, no-transform, s-maxage=31536000, max-age=31536000`);
    res.setHeader('Content-Type', 'image/png');
    res.end(buffer);
  } catch (error) {
    console.error(error);

    res.statusCode = 500;
    res.setHeader('Content-Type', 'text/html');
    res.end('<h1>Internal Error</h1><p>Sorry, there was a problem</p>');
  }

```

Now, we need to pass the generated Html to the set content function then take a screenshot by calling the screenshot function from puppeteer with filetype. Return the buffer value from the screenshot and send it as a response.

### Reference

- [https://phiilu.com/generate-open-graph-images-for-your-static-next-js-site](https://phiilu.com/generate-open-graph-images-for-your-static-next-js-site)
- [https://github.com/vercel/og-image](https://github.com/vercel/og-image)

This project is [open-source](https://github.com/JanaSundar/og-image) on GitHub. Have a closer look if you want.

Hopefully, you have learned how to generate a dynamic OG image. If you have any doubts, you can reach me at mailtojanasundar@gmail.com. Thanks for reading ✌️ and if you enjoyed this article, share it with your friends and colleagues.

* * *