Generate Dynamic Open Graph and Twitter Images in Next.js
Tags: nextjs, webdev, tutorial
Published: 2023-09-05 08:31:33

####**[Live Demo](https://cruip-tutorials-next.vercel.app/social-preview) / [Download](https://github.com/cruip/cruip-tutorials-next/blob/main/app/api/og/route.tsx)**

In this tutorial, we’ll see **how to create dynamic Open Graph and Twitter images with Next.js**. This technique allows us to generate personalized previews for our articles when shared on social media platforms, enhancing engagement with your audience.

The dynamic images will include a background that’s been prepared in advance and some text that corresponds to the title of the page. Additionally, we’ll incorporate information about the author of the page, as demonstrated in the **preview image above**.

What makes this approach intriguing is its ability to **generate images on-the-fly** whenever a page is shared. This means we can offer a tailored preview for each article without needing to create individual images for each one.

This is made possible thanks to the **Dynamic Open Graph Image Generation** feature introduced with Next.js version 13.3, and the new [Metadata API](https://nextjs.org/docs/app/building-your-application/optimizing/metadata). In summary, it involves generating images using code (in our case, TSX, HTML, and CSS) with the help of the libraries [@vercel/og](https://vercel.com/docs/concepts/functions/edge-functions/og-image-generation) (already integrated in the App router) and [Satori](https://github.com/vercel/satori). Satori converts HTML and CSS to SVG, and then resvg-js converts the SVG to a PNG image. All of this in just a few milliseconds!

Let’s get started!

Creating a new page in your Next.js app
---------------------------------------

Since we’re using the App router, you only need to create a new folder within the `app` directory and give it a name that corresponds to your desired page path. For instance, let’s consider `app/social-preview`. Within this folder, create a new file named `page.tsx` and include the following code:

```tsx
  export const metadata = {
    title: 'Social Metadata - Cruip Tutorials',
    description:
      "A guide on how to optimize SEO with static and dynamic metatags using Next.js 13's new Metadata API.",
  }
  
  import Banner from '@/components/banner'
  
  export default function SocialPreviewPage() {
    return (
      <>
        <main className="relative min-h-screen flex flex-col justify-center bg-slate-900 overflow-hidden">
          <div className="w-full max-w-6xl mx-auto px-4 md:px-6 py-24">
            <div className="text-center">
  
              <div className="font-extrabold text-3xl md:text-4xl [text-wrap:balance] bg-clip-text text-transparent bg-gradient-to-r from-slate-200/60 to-50% to-slate-200">Generate Dynamic Open Graph and Twitter Images in Next.js</div>
              <p className="text-lg text-slate-500 mt-4">Share this page on Facebook and Twitter to see the preview image</p>
  
            </div>
          </div>
        </main>
  
      </>
    )
  }
```

For now, we’re focusing on setting the title; page content isn’t vital at this stage. The objective is to understand the concept of generating images dynamically.

Dynamic text generation
-----------------------

Before extending the `metadata` object, we’ll create an API route that will serve as an endpoint for dynamically generating the text. Create a folder called `og` inside the `api` directory. Inside that, create a file named `route.tsx` and add this code:

```tsx
  import { ImageResponse } from 'next/server'
    
  export const runtime = 'edge'
    
  export async function GET(request: Request) {
    const interExtrabold = fetch(
      new URL('../../../public/Inter-ExtraBold.ttf', import.meta.url)
    ).then((res) => res.arrayBuffer())
  
    try {
      const { searchParams } = new URL(request.url)
    
      const hasTitle = searchParams.has('title')
      const title = hasTitle
        ? searchParams.get('title')?.slice(0, 100)
        : 'Default title'
    
      return new ImageResponse(
        (
          <div
            style={{
              backgroundImage: 'url(https://cruip-tutorials-next.vercel.app/social-card-bg.jpg)',
              backgroundSize: '100% 100%',
              height: '100%',
              width: '100%',
              display: 'flex',
              flexDirection: 'column',
              alignItems: 'flex-start',
              justifyContent: 'center',
              fontFamily: 'Inter',
              padding: '40px 80px',
            }}
          >
            <div
              style={{
                fontSize: 60,
                fontWeight: 800,
                letterSpacing: '-0.025em',
                lineHeight: 1,
                color: 'white',
                marginBottom: 24,
                whiteSpace: 'pre-wrap',
              }}
            >
              {title}
            </div>
            <img
              width="203"
              height="44"
              src={`https://cruip-tutorials-next.vercel.app/author.png`}
            />
          </div>
        ),
        {
          width: 1200,
          height: 630,
          fonts: [
            {
              name: 'Inter',
              data: await interExtrabold,
              style: 'normal',
              weight: 800,
            },
          ],        
        },
      )
    } catch (e: any) {
      console.log(`${e.message}`)
      return new Response(`Failed to generate the image`, {
        status: 500,
      })
    }
  }
```

This code creates a `GET` function which returns an `ImageResponse` object. The `ImageResponse` constructor generates a dynamic image from JSX and CSS. The image will consist of a background image, a title, and an image containing author information.

The steps are as follows:

*   Incorporating a custom font (Inter Extrabold) from the `public` directory.
*   Defining the `title` constant to hold the page’s title. If a `title` parameter exists in the URL, it is used; otherwise, the default is “Default title.”
*   Designing the structure using CSS in accordance with Satori’s guidelines.
*   Specifying the image size as 1200x630px.
*   Adding Inter Extrabold to the fonts array.

Calling the route handler in `page.tsx` will trigger the dynamic image generation.

Defining OpenGraph and Twitter fields
-------------------------------------

We can now expand the metadata object by adding the `openGraph` and `twitter` fields. These fields hold metadata specifically intended for social media platforms.

First, set up metadata for Open Graph:

```tsx
  openGraph: {
    title: "Generate Dynamic Open Graph and Twitter Images in Next.js",
    description:
      "A guide on how to optimize SEO with static and dynamic metatags using Next.js 13's new Metadata API.",
    type: "article",
    url: "https://cruip-tutorials-next.vercel.app/social-preview",
    images: [
      {
        url: "https://cruip-tutorials-next.vercel.app/api/og?title=Generate Dynamic Open Graph and Twitter Images in Next.js",
      },
    ],
  }
```

Next, we’ll proceed with the metadata for the Twitter Card:

```tsx
  twitter: {
    card: "summary_large_image",
    title: "Generate Dynamic Open Graph and Twitter Images in Next.js",
    description:
      "A guide on how to optimize SEO with static and dynamic metatags using Next.js 13's new Metadata API.",
    images: [
      "https://cruip-tutorials-next.vercel.app/api/og?title=Generate Dynamic Open Graph and Twitter Images in Next.js",
    ],
  }
```

Note how we’re invoking the `/api/og` endpoint to trigger the dynamic image generation. In this case, we’re passing the page’s title as a parameter, but we could pass any parameter we want.

The metadata object will look like this:

```tsx
  export const metadata = {
    title: 'Social Metadata - Cruip Tutorials',
    description:
      "A guide on how to optimize SEO with static and dynamic metatags using Next.js 13's new Metadata API.",
    openGraph: {
      title: "Generate Dynamic Open Graph and Twitter Images in Next.js",
      description:
        "A guide on how to optimize SEO with static and dynamic metatags using Next.js 13's new Metadata API.",
      type: "article",
      url: "https://cruip-tutorials-next.vercel.app/social-preview",
      images: [
        {
          url: "https://cruip-tutorials-next.vercel.app/api/og?title=Generate Dynamic Open Graph and Twitter Images in Next.js",
        },
      ],
    },
    twitter: {
      card: "summary_large_image",
      title: "Generate Dynamic Open Graph and Twitter Images in Next.js",
      description:
        "A guide on how to optimize SEO with static and dynamic metatags using Next.js 13's new Metadata API.",
      images: [
        "https://cruip-tutorials-next.vercel.app/api/og?title=Generate Dynamic Open Graph and Twitter Images in Next.js",
      ],
    },
  }
```

And here’s the end result:

![Twitter card preview](https://cruip.com/wp-content/uploads/2023/08/twitter-card-768x406.png)

Simply share the page’s link on Twitter or Facebook to observe the preview in action!

Styling the image content with Tailwind CSS
-------------------------------------------

Although still an experimental feature of Satori, it’s possible to use Tailwind CSS classes to style the image’s content. We’re big fans of the Css frameworks, as shown by our [Tailwind templates](https://cruip.com), so we had to give it a try!

Satori allows us to define Tailwind CSS classes within dedicated `tw` attributes. Therefore, we can achieve the same result using the following code:

```tsx
  import { ImageResponse } from 'next/server'
    
  export const runtime = 'edge'
    
  export async function GET(request: Request) {
    const interExtrabold = fetch(
      new URL('../../../public/Inter-ExtraBold.ttf', import.meta.url)
    ).then((res) => res.arrayBuffer())
  
    try {
      const { searchParams } = new URL(request.url)
    
      const hasTitle = searchParams.has('title')
      const title = hasTitle
        ? searchParams.get('title')?.slice(0, 100)
        : 'Default title'
    
      return new ImageResponse(
        (
          <div
            tw="h-full w-full flex flex-col align-start justify-center py-10 px-20"
            style={{
              backgroundImage: 'url(https://cruip-tutorials-next.vercel.app/social-card-bg.jpg)',
              backgroundSize: '100% 100%',
              fontFamily: 'Inter',
            }}
          >
            <div
              tw="text-6xl font-extrabold text-white tracking-tight leading-none mb-6 whitespace-pre-wrap"
            >
              {title}
            </div>
            <img
              width="203"
              height="44"
              src={`https://cruip-tutorials-next.vercel.app/author.png`}
            />
          </div>
        ),
        {
          width: 1200,
          height: 630,
          fonts: [
            {
              name: 'Inter',
              data: await interExtrabold,
              style: 'normal',
              weight: 800,
            },
          ],        
        },
      )
    } catch (e: any) {
      console.log(`${e.message}`)
      return new Response(`Failed to generate the image`, {
        status: 500,
      })
    }
  }
```