Generate Dynamic Open Graph and Twitter Images in Next.js

Generate Dynamic Open Graph and Twitter Images in Next.js

·

6 min read

Originally published at cruip.com

--

👉 Live Demo / Download

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. In summary, it involves generating images using code (in our case, TSX, HTML, and CSS) with the help of the libraries @vercel/og (already integrated in the App router) and 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:

  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:

  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:

  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:

  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:

  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

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, 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:

  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,
      })
    }
  }
Â