Automate Open Graph Image Creation in Next.js
Tags: nextjs, webdev, react, seo
Published: 2023-12-17 16:20:16

---
title: Automate Open Graph Image Creation in Next.js
published: true
date: 2023-12-10 00:00:00 UTC
tags: nextjs,webdev,react,seo
canonical_url: https://www.davegray.codes/posts/automate-open-graph-images-nextjs
cover_image: https://raw.githubusercontent.com/gitdagray/my-blogposts/main/images/automate-open-graph-images-nextjs.png
---

In this article, I'll share how you can dynamically create open graph images with Next.js. 

## What are Open Graph Images? 

An open graph image is only one piece of a much larger set of [open graph metadata](https://ogp.me/#metadata) that helps social media platforms build a display card for your content when it is shared. I previously published a full article on [_"How to Create Open Graph Social Media Cards"_](https://www.davegray.codes/posts/nextjs-open-graph-social-media-cards) where you can learn much more about the open graph metadata your site needs. When you provide open graph image data in your metadata, the image becomes part of the generated social media card.

## Why Generate Dynamic Open Graph Images?

By setting up your Next.js website to dynamically generate open graph images, you are automating part of your workflow. You may not want to do this for all pages on your website, but I believe you will find it very useful.

## My Current Workflow 

I'm currently using a mixed approach of manual and dynamic open graph images for my blog.  

Here's what I do for each blog post: 
- Manually select an image on the web
- Resize it to 1200px wide
- Crop it to 1200x630  
- Save it with the same file name as my blog article
- Load it up to the GitHub repository where I store my blog article [MDX](https://mdxjs.com/) files 

Now that I have typed the steps out, I can really see that there are several additional steps I have to take every time I create a blog post. 

In contrast, the open graph images for my [homepage](https://www.davegray.codes/) and for my [tag results](https://www.davegray.codes/tags/nextjs) pages are dynamically generated by Next.js. The images are created automatically with _no extra steps!_ 

## What Do Dynamic Open Graph Images Look Like?

They can be as simple or complex as you want to make them. 

Here's what my open graph image for the tag `nextjs` looks like:

![Open Graph image for nextjs tag results on my blog](https://www.davegray.codes/tags/nextjs/opengraph-image/nextjs?e6574072cc5e5cea)

And here's what the open graph image for my homepage looks like: 
![Open Graph image for my blog homepage](https://www.davegray.codes/opengraph-image?0ebeab659c19d3d8)

The images I'm dynamically creating with Next.js for my tag results pages are very simple. 

The image I'm dynamically creating for my homepage has a more complex layout and fetches some vanity data from both GitHub and YouTube. 

Let's look at how both of these are made with Next.js:

## 1. Specialized Route Handlers

Next.js supports `opengraph-image` and `twitter-image` files that can be placed alongside `page` files in any given route segment. Both of these files are [_specialized route handlers_](https://nextjs.org/docs/app/api-reference/file-conventions/metadata/opengraph-image#route-segment-config) that will dynamically generate open graph images and twitter images respectively. 

Note that twitter images are essentially the same as open graph images but specifically for Twitter (now X). Learn more about the metadata for open graph and twitter in my previous article [_"How to Create Open Graph Social Media Cards"_](https://www.davegray.codes/posts/nextjs-open-graph-social-media-cards). 

The Next.js docs provide some examples of how to [generate images using code](https://nextjs.org/docs/app/api-reference/file-conventions/metadata/opengraph-image#generate-images-using-code-js-ts-tsx). I hope you benefit from seeing the code in my `opengraph-image.tsx` file as well. 

My `opengraph-image.tsx` file has three functions in it: 
- generateStaticParams 
- generateImageMetadata 
- Image 

Here is the full code for my `opengraph-image.tsx`:

```tsx 
// app/tags/[tag]/opengraph-image.tsx 

import { ImageResponse } from "next/og"
import { Inter } from "next/font/google"
import { getPostsMeta } from "@/lib/posts"

const websiteURL = process.env.NODE_ENV === 'production'
    ? 'https://www.davegray.codes/'
    : 'http://localhost:3000/'

const inter = Inter({
    weight: "400",
    subsets: ['latin']
})

export async function generateStaticParams() {
    const posts = await getPostsMeta()

    if (!posts) return []

    const tags = new Set(posts.map(post => post.tags).flat())

    return Array.from(tags).map((tag) => ({ tag }))
}

export async function generateImageMetadata({
    params: { tag }
}: {
    params: { tag: string }
}) {

    return [{
        id: tag,
        size: { width: 1200, height: 630 },
        alt: `Posts about ${tag}`,
        contentType: 'image/png',
    }]
}

export default async function Image({ id }: { id: string }) {

    return new ImageResponse(
        (
            // ImageResponse JSX element
            <div
                className={inter.className}
                style={{
                    fontSize: 184,
                    background: 'black',
                    color: 'white',
                    width: '100%',
                    height: '100%',
                    display: 'flex',
                    justifyContent: 'center',
                    alignItems: 'center',
                    backgroundColor: 'black',
                    backgroundImage: `url(${websiteURL}images/og-card-bg-1.png)`,
                }}>
                {id}
            </div>
        ),
        // ImageResponse options
        {
            width: 1200,
            height: 630,
        }
    )
}
```
The `generateStaticParams` function is identical to the same function in my `page.tsx` file that generates dynamic pages for the tag results. This function returns an array of tag objects that is passed to the `generateImageMetadata` function. 

The [`generateImageMetadata` function](https://nextjs.org/docs/app/api-reference/functions/generate-image-metadata) can be used to return multiple images for one route segment. It does this by sending the objects in its returned array to the companion `Image` function. Note that `generateImageMetadata` returns an array of objects and each object **must** include an **id** value. 

The `Image` function uses the [ImageResponse](https://nextjs.org/docs/app/api-reference/functions/image-response) constructor. This function receives the required **id** parameter from each object provided by the array from `generateImageMetadata` and creates an image for it. In the above example, you can see I assign my **tag** value to **id** in `generateImageMetadata`, and then I use the **id** value in the `Image` function. 

## 2. Static vs Dynamic Output

By default, the dynamically generated images from the specialized route handlers are [statically optimized](https://nextjs.org/docs/app/building-your-application/rendering/server-components#static-rendering-default). For something like tag results pages or even blog article pages, that should be good. 

However, that will not be good if you need to fetch constantly updated data like the vanity metrics in the image for my homepage. 

No worries! 

You can change the static behavior with individual fetch options or route segment options. 

Below is the code in my `opengraph-image.tsx` file for my homepage: 

```tsx 
// app/opengraph-image.tsx 

import { ImageResponse } from 'next/og'
import { Inter } from 'next/font/google'
import { FaYoutube, FaGithub } from 'react-icons/fa6'

const websiteURL = process.env.NODE_ENV === 'production'
    ? 'https://www.davegray.codes/'
    : 'http://localhost:3000/'

export const runtime = 'edge'

const inter = Inter({
    weight: "400",
    subsets: ['latin']
})

// Image metadata
export const alt = 'Preview image for Dave Gray'

export const size = {
    width: 1200,
    height: 630,
}

export const contentType = 'image/png'

export default async function Image() {

    const githubData = fetch(`https://api.github.com/users/gitdagray`, {
        next: {
            revalidate: 0,
        }
    }).then((res) =>
        res.json()
    )

    const youtubeData = fetch(`https://youtube.googleapis.com/youtube/v3/channels?part=statistics&id=UCY38RvRIxYODO4penyxUwTg&key=${process.env.YOUTUBE_API_KEY}`, {
        headers: {
            Accept: 'application/json',
        },
        next: {
            revalidate: 0,
        }
    }).then((res) =>
        res.json()
    )

    const [{
        followers: githubFollowers,
        bio: githubBio,
        name: githubName,
    },
        { items: youtubeItems }
    ] = await Promise.all([githubData, youtubeData])

    const ytSubCount = youtubeItems?.length
        ? youtubeItems[0].statistics.subscriberCount
        : null

    return new ImageResponse(
        (
            // ImageResponse JSX element goes here
        ),
        // ImageResponse options
        {
            // For convenience, you can re-use the exported opengraph-image size config to also set the ImageResponse's width and height.
            ...size,
        }
    )
}
```
You can see the `opengraph-image.tsx` file above is similar but still very different from the previous example I gave. 

This file is used to create one image. I am not using the `generate` functions that I used in the dynamic route segment for tags. Therefore, I define three exports for image metadata: alt, size, and contentType. 

I am also using the **edge runtime** in this file. If you look at the [runtime differences](https://nextjs.org/docs/app/building-your-application/rendering/edge-and-nodejs-runtimes#runtime-differences), you will see that **edge** does not support static rendering. 

When you use the **edge runtime**, _the [revalidate](https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#revalidate) route segment config option is not available_. Because of this, I set revalidate to zero in each of the fetch requests instead. 

The fetch requests use the [Parallel Data Fetching](https://nextjs.org/docs/app/building-your-application/data-fetching/patterns#parallel-data-fetching) pattern with [Promise.all](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all). 

I did not share the HTML utilizing inlined CSS that makes up the JSX Element of the ImageResponse. It is truly an uglier version of the first example. _Why create such ugliness with inline CSS?_ This question leads me to a discussion about the developer experience using the ImageResponse API. I will talk about that [near the end of this article](#5-the-developer-experience).

## 3. Output 

The resulting output of this file is the open graph image I shared above that includes up-to-date vanity metrics. An updated image will be provided when my homepage is shared. 

The only caveat is some social media sites prefer to cache open graph images. Adding any random parameter to the URL like `?dave` or `?reset=1` should get the site to grab the latest image for your post. 

When you create dynamic open graph images, Next.js also adds the related meta tags inside the `<head>` element of your website. Inspect your page with devtools, and you should find the following meta tags, but they may not be next to each other or in this order: 

```html 
<meta property="og:image" content="https://www.davegray.codes/tags/nextjs/opengraph-image/nextjs?e6574072cc5e5cea">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt" content="Posts about nextjs">
<meta property="og:image:type" content="image/png">
```
## 4. Twitter Images (X.com) 

Everything discussed above also applies to the `twitter-image.tsx` files you can create as well. The _only difference_ between my opengraph and twitter files is the background image I applied to each and the chosen color of an icon. Share my homepage on [X.com](https://x.com/) and on [LinkedIn](https://www.linkedin.com/) to see the difference. You can do the same with my tag results pages like the one for [articles tagged with nextjs](https://www.davegray.codes/tags/nextjs).

Here's what my twitter image for the tag `nextjs` looks like:

![Twitter image for the tag nextjs on my blog](https://www.davegray.codes/tags/nextjs/twitter-image/nextjs?6f38845728b6d005)

And here's what the twitter image for my homepage looks like: 

![Twitter image for my blog homepage](https://www.davegray.codes/twitter-image?6269119bae970ac4)

The metadata Next.js creates for your `twitter-image` is different than the metadata for your `opengraph-image`. Here are the meta tags you should find inside your `<head>` element: 

```html 
<meta name="twitter:image" content="https://www.davegray.codes/tags/nextjs/twitter-image/nextjs?6f38845728b6d005">
<meta name="twitter:image:width" content="1200">
<meta name="twitter:image:height" content="630">
<meta name="twitter:image:alt" content="Posts about nextjs">
<meta name="twitter:image:type" content="image/png">
```

## 5. View Your Images 

In dev mode, you can open up devtools, grab the URL for your open graph image from the meta tag, and paste it into another browser tab to display your image. 

After you deploy your site, you can check your live URLs in several ways: 
- [Facebook Sharing Debugger](https://developers.facebook.com/tools/debug/)
- [LinkedIn Post Inspector](https://www.linkedin.com/post-inspector/)
- Create a draft post on any social media and preview the card without actually posting 

The last suggestion is the _only way_ to currently test images on [X.com](https://x.com/). The site previously allowed you to test with a [Twitter Card Validator](https://cards-dev.twitter.com/validator). However, it no longer previews images. 

**Remember:** Add a random parameter to the end of your URL to get the social media sites to refresh their cached image: `?random`, `?abc`, etc.

## 6. The Developer Experience 

The specialized route handlers `opengraph-image` and `twitter-image` were introduced in Next.js version 13.3. If you already know how to create dynamic pages in Next.js, these files follow that pattern. This makes the learning curve intuitive.

One noticeable impact to the DX while working with these files - _especially while tweaking the CSS_ - is that the image does not automatically update after you save your changes. You are probably used to your Next.js app doing that for you, but for these image route handlers, you must make a new request by hitting refresh in your browser in order to see your changes. This is simply how the web works though and should not reflect on Next.js in a negative way. You might want to use a browser extension like [Auto Refresh Page](https://chromewebstore.google.com/detail/auto-refresh-page/aipbahhkojbhioodfbfmnobjnkagpnfg) that refreshes the page for you at a regular interval while you work on these.

The [ImageResponse](https://nextjs.org/docs/app/api-reference/functions/image-response) constructor is limited in what it supports. I mentioned ugly code earlier. The ugliness is really due to [limited support for CSS properties](https://nextjs.org/docs/app/api-reference/functions/image-response#supported-css-properties) and the need to inline all CSS with `style`. I would prefer to use Tailwind as I have on the rest of my site instead of applying inline styles. Not all CSS styles are supported either. I would like to see more support for CSS and possibly Tailwind inside of the ImageResponse constructor. 

## 7. An Additional Option 

You can learn a lot by looking at [leerob's blog repo](https://github.com/leerob/leerob.io/tree/main) which is open to the public. As of this writing (December 2023), Lee's blog has not yet adopted the specialized `opengraph-image` and `twitter-image` files. Instead, he is using a [standard `route` handler file](https://github.com/leerob/leerob.io/blob/main/app/og/route.tsx) to dynamically create his open graph images, and that is always an additional option.

<hr>

## Let's Connect!

Hi, I'm Dave. I work as a full-time developer, instructor and creator. 

If you enjoyed this article, you might enjoy my other content, too.

**My Stuff:** [Courses, Cheat Sheets, Roadmaps](https://courses.davegray.codes/)

**My Blog:** [davegray.codes](https://www.davegray.codes/)

**YouTube:**  [@davegrayteachescode](https://www.youtube.com/davegrayteachescode) 

**X:**  [@yesdavidgray](https://x.com/yesdavidgray)  

**GitHub:** [gitdagray](https://github.com/gitdagray)

**LinkedIn:**  [/in/davidagray](https://www.linkedin.com/in/davidagray/) 

**Buy Me A Coffee:**  [You will have my sincere gratitude](https://www.buymeacoffee.com/davegray)   

Thank you for joining me on this journey. 

Dave