Create an Open Graph image engine in just 4 steps
Tags: nextjs, tailwindcss, javascript, tutorial
Published: 2022-01-30 17:09:09

The [Open Graph protocol](https://ogp.me) is an open standard of website metadata for easy parsing by search engines, messaging apps, and social networks.

The most popular Open Graph metadata attribute is the Open Graph image - a graphic shown alongside links to a webpage as a preview. When shared, pages with Open Graph images are more engaging and appear more professional. Any websites without such image metadata appear with gray boxes or generic icons that don't stand out.

While the idea of Open Graph images was to give a simple preview as a *supplement* to other information about the linked webpage URL, the trend has shifted to make Open Graph images the *primary* attribute being shared. This is clear when we see Open Graph images including large text, logos, colorful backgrounds, reading times, publish dates, and even more! The best Open Graph images today give users a feel for the website before they even visit the associated webpage.

So let's make a dynamic Open Graph image generator that you can use for your websites!

## Frameworks Used
We'll be using [NextJS](https://nextjs.org) to build our site and Open Graph image layout, [Tailwind CSS](https://tailwindcss.com) to style everything, and [Vercel](http://vercel.com) to deploy the site as well as our serverless Open Graph generator function. Of course, we'll also use a few NPM packages along the way, such as `chrome-aws-lambda`.

## Setup
Let's spin up a NextJS app. If you already have a NextJS app, you can skip this step.

```shell
> npx create-next-app open-graph-generator
```
Let's set up Tailwind CSS to make styling easier. Again, you can skip this step if you already have a CSS framework set up.

Install Tailwind CSS and its dependencies:
```shell
> cd open-graph-generator
> npm i -D tailwindcss postcss autoprefixer
> npx tailwindcss init -p
```

Add paths to the JSX files in Tailwind's config
```js
module.exports = {
  content: [
    "./pages/**/*.{js,ts,jsx,tsx}",
    "./components/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}
```

Add Tailwind to your styles file
```css/globals.css
@tailwind base;
@tailwind components;
@tailwind utilities;
```

## Create the layout
We'll build the UI first. If you have an idea for how your Open Graph image should look, great! If not, start with some basic elements and experiment! There's no wrong way to build your image.

Let's create a page for our Open Graph layout. A common size for OG images is 1200px by 630px, so we will use these dimensions for our bounds. Let's add a title, date, author, and a profile image.

```jsx
import { useRouter } from 'next/router'

export default function Opengraph() {
  const { query } = useRouter()
  const { title } = query

  return (
    <div className='w-[1200px] h-[630px]'>
      <div class='flex flex-col h-full items-center justify-between px-12 py-24'>
        <h1 class='text-7xl grow font-semibold'>{title}</h1>
        <img class='w-24 h-24 rounded-full mb-4' src='/profile.jpg' />
        <span class='text-3xl'>by Kartik Chaturvedi</span>
      </div>
    </div>
  )
}
```

You'll notice a few things with this code, so let's break them down.

First, yes you should use large font sizes! Webpage previews using Open Graph images are usually shown at small sizes (think of links in messages or inside a tweet). You can't easily zoom in on those images, so you want to make sure the design is clean and bold and text is legible.

Second, we're using the `useRouter` React hook to extract the `title` from the URL and place it in our React component. This is key to making our Open Graph image dynamic. You will be able to pass in values right within the URL and get an assembled component ready to convert into an image.

To see this all in action, let's start our dev environment.
```shell
> npm run dev
```

Visit `localhost:3000/opengraph?title=Hello%20World` and you will see the query string parsed into text and rendered in the layout.
![layout preview](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6xnfaxrpotovtgkb6k83.png)
 

You can continue styling this layout component as you wish. Some more parameters you can add are date of publication, reading time, word count, or number of likes. There really is no limit!

## Set up a screenshot function
The component we see in the browser above is, obviously, still a React component rendered to HTML, which won't work with Open Graph. The protocol requires an image, so that applications can consistently and instantly display it without interpretation or styling delays.

The best way to convert our React component to an image is to take a screenshot of the browser. And that's exactly what we will do in a serverless function. We will use `chrome-aws-lambda`, a headless version of Chrome designed for serverless applications and often used for automated UI testing. We'll also need `puppeteer`, a tool for controlling headless browsers like Chrome.

```shell
> npm i chrome-aws-lambda puppeteer-core
```

Since NextJS supports serverless functions out-of-the-box, all we need to do is create the function in the `/api` folder.

```jsx
import chromium from 'chrome-aws-lambda'

export default async function opengraph(req, res) {
  // Parse the title
  const { title } = req.query
  const baseURL = req.headers.host

  // Open the browser with the right window size
  const browser = await chromium.puppeteer.launch({
    args: chromium.args,
    defaultViewport: { width: 1200, height: 630 },
    executablePath: await chromium.executablePath, // change for localhost
    headless: chromium.headless,
    ignoreHTTPSErrors: true,
  })

  // Navigate a new browser page to the layout page
  let page = await browser.newPage()
  await page.goto(`${baseURL}/opengraph?title=${title}`, { waitUntil: 'networkidle2' })

  // Take a screenshot
  const screenshotBuffer = await page.screenshot({ type: 'png' })
  await browser.close()

  // Tell the consuming service to cache the image being sent
  res.setHeader('Cache-Control', `public, immutable, no-transform, s-maxage=31536000, max-age=31536000`)
  res.setHeader('Content-Type', 'image/png')
  res.status(200).send(screenshotBuffer)
}
```

To see this in action, you will need to change the `executablePath` to point to your local instance of a Chromium-based browser. Then, if you visit `localhost:3000/api/generate-og?title=Hello%20World`, you will see the exact same layout as before, except it's now a PNG image!

Our serverless function loads the layout with the `title` text passed in, takes a screenshot, and returns the image ready for use.

At this point, it's a good idea to deploy your code and see that everything is working as intended. You'll need a free Vercel account, and then you set things up [in Vercel](https://vercel.com/docs/concepts/deployments/overview) or use the [Vercel CLI](https://vercel.com/docs/concepts/deployments/overview#vercel-cli) to quickly deploy right from your terminal.
```shell
> vercel
🔍  Inspect: https://vercel.com/your-org/opengraph-demo/************ [3s]
✅  Preview: https://opengraph-demo-app.vercel.app [copied to clipboard] [54s]
```

Navigate to your project subdomain's Open Graph API route - for example, `opengraph-demo-app.vercel.app/api/generate-og?title=This%20is%20working!` and you should see your Open Graph image rendered to the screen! You can try changing the `title` query parameter and a new image will be generated each time.

## Link the image to a page
All that is left is to point any service looking for your site's Open Graph image to this serverless API.

Since Open Graph is just a format for metadata, so it's all defined as `<meta>` tags in the webpage HTML header. With an Open Graph image, the image simply needs to be the meta `property` is `og:image` with the URL of the image being the `content`. In the case of our NextJS example, create a new page and React function, and add a header entry using the NextJS `Head` component.
```jsx
import Head from 'next/head'

export default function Demo() {
  return (
    <div>
      <Head>
        <meta property='og:image' content='https://opengraph-demo-app.vercel.app/api/generate-og?title=This%20is%20the%20Demo%20Page' />
      </Head>
      <div className='flex h-full justify-center items-center'>
        <h1 className='text-4xl'>Hello World!</h1>
        <p>This page is a demo for Open Graph images!</p>
      </div>
    </div>
  )
}
```

So the complete flow of what we have done above is:
1. A service loads the site and reads the meta tag
2. Meta tag leads the service to the serverless API
3. Serverless API takes a screenshot of the React component
4. Serverless API returns an image to the service

## Make it dynamic
This step will vary across projects, but the core setup is the same. To make your Open Graph image dynamic, you will need to pass a dynamic `title` value to your API in the `meta` tag. The simplest way to do so is by passing in a prop to your React function. For example, in NextJS, you can create [dynamic routes](https://nextjs.org/docs/basic-features/data-fetching/get-static-props#using-getstaticprops-to-fetch-data-from-a-cms) and fetch blog posts from a database, CMS, or other resource and extract the title to show at the top of the page. Simply pass the same title value to the API.

It's also recommended to add a few other Open Graph properties alongside the image.
```jsx
import Head from 'next/head'

export default function BlogPost({post}) {
  const ogImagePath = `https://opengraph-demo-app.vercel.app/api/generate-og?title=${post.title}`

  return (
    <div>
      <Head>
        <meta property='og:image' content={ogImagePath} />
        <meta property='og:title' content={post.title} />
        <meta property='og:description' content={post.description} />
        <meta property='og:type' content='article' />
      </Head>
      <div className='flex h-full justify-center items-center'>
        <h1 className='text-4xl'>{post.title}</h1>
        <div>
          {post.content}
        </div>
      </div>
    </div>
  )
}

export async function getStaticProps({ params }) {
  const res = await fetch('https://.../posts')
  const posts = await res.json()
  const post = posts.filter((post) => post.title === params.slug).first

  return {
    props: {
      post,
    },
  }
}
```

And that's it! Your Open Graph image will be shown in previews online anyplace where the Open Graph protocol is used to enrich shared links. Simply redeploy the project to Vercel with the `--prod` flag and see it in action when you share a link to your website.

If you have any questions, feedback, or just want to chat, you can reach me on [Twitter](https://twitter.com/itskrtk) or [GitHub](https://github.com/kchaturvedi).