Lesson 5

Creating dynamic Open Graph images with Vercel/OG

Say goodbye to manually creating social preview images in Figma or Photoshop. You are about to learn how to automatically generate dynamic Open Graph images that pull your data directly from Sanity, saving you hours of design work and ensuring your social previews are always up to date with your content.

Learning objectives

By the end of this lesson, you'll be able to:

  • Generate dynamic Open Graph images using Next.js Edge Runtime
  • Extract and use dominant colors from featured images
  • Create professional, branded social previews

Understanding Open Graph images

Open Graph images (or social cards) are the preview images that appear when your content is shared on social media platforms. Dependent on which platform you're sharing to, you may want to create a range of different aspect ratios. For this tutorial, it's simply the most common one: 1200 x 630 pixels.

Setting up the edge route

Let's create a new API route using Next.js Edge Runtime. This route will:

  • Accept parameters like page ID or slug
  • Return an image response using Next.js's ImageResponse

Make sure before you proceed any further, you have read the limitations (opens in a new tab). This is a very important step.

app/api/og/route.ts
import { ImageResponse } from "next/og";
 
export const runtime = "edge";
 
// Define your image dimensions
const dimensions = {
  width: 1200,
  height: 630,
};
 
export async function GET(request: Request) {
  // We'll expand this in the next steps
  const { searchParams } = new URL(request.url);
  const title = searchParams.get("title");
 
  return new ImageResponse(
    (
      <div tw="flex w-full h-full bg-blue-500 text-white p-10">
        <h1 tw="text-6xl font-bold">{title}</h1>
      </div>
    ),
    dimensions
  );
}

A couple of things to note:

This creates a basic endpoint that generates an image with a title parameter. We're using Tailwind-style utilities with the tw prop for styling. If you use "className" you will get an error.

Creating the Sanity query

We'll need to fetch specific data for our OG images:

  • Page title
  • Featured image URL
  • Color palette information

Also if you didn't know about the dominant color palette, you can use that to get a nice background color for your image, there's lots of neat metadata you can pull from sanity images (opens in a new tab).

First, let's create our query:

lib/queries/og-image.ts
export const ogImageQuery = `*[_id == $id][0]{
  title,
  publishedAt,
  "image": mainImage.asset->{
    url,
    metadata {
      palette {
        dominant {
          background
        }
      }
    }
  }
}

Now you can use this query in your route to fetch dynamic data into your open graph image.

app/api/og/route.ts
import { getClient } from "~/lib/sanity";
import { ogImageQuery } from "~/lib/queries/og-image";
 
export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const id = searchParams.get("id");
 
  const data = await getClient().fetch(ogImageQuery, { id });
  const dominantColor =
    data?.image?.metadata?.palette?.dominant?.background ?? "#3B82F6";
 
  return new ImageResponse(
    (
      <div
        tw="flex w-full h-full p-10"
        // Because this is dynamic, we need to use the style prop
        style={{ backgroundColor: dominantColor }}
      >
        <h1 tw="text-6xl font-bold text-white">{data.title}</h1>
      </div>
    ),
    dimensions
  );
}

This query fetches the page title, image, and its color palette information. The route now uses this data to create a dynamic background color based on the image.

You can test this route by visiting /api/og?id=your-document-id in your browser, replacing your-document-id with an actual Sanity document ID.

Building the image template

Our template will include:

  • A dynamic background color based on the featured image
  • The page title in a consistent font
  • The featured image itself
  • Optional elements like publication date or category

Let's enhance our design with a more complete template:

one we made earlier
 
// ...
  return new ImageResponse(
    (
      <div
        tw="flex w-full h-full relative"
        style={{ backgroundColor: dominantColor }}
      >
        {/* Add a gradient overlay */}
        <div tw="absolute inset-0 bg-black/25" />
 
        {/* Content container */}
        <div tw="flex flex-row w-full h-full p-10 relative">
          {/* Text content */}
          <div tw="flex-1 flex flex-col justify-between">
            <h1 tw="text-5xl font-bold text-white max-w-[600px] leading-tight">
              {data.title}
            </h1>
          </div>
 
          {/* Image container */}
          {data.image?.url && (
            <div tw="w-[250px] h-[250px] rounded-xl overflow-hidden">
              <img
                src={data.image.url}
                alt=""
                tw="w-full h-full object-cover"
              />
            </div>
          )}
        </div>
      </div>
    ),
    // ...

This template creates a professional layout with:

  • A background color extracted from the image
  • A subtle skim to make the text more readable
  • The page title with proper spacing and sizing
  • The featured image in a rounded container

It's also worth noting, that the route is somewhat temperamental, and during changes to the design, may break, which will require you to re-run npm run build.

One of the best bits about this is that once you have built your design, you can quickly iterate on it, into multiple different dimensions for the optimal image for each platform.

Implementing metadata

Now that you have your OG image generation set up, let's integrate it into your page metadata.

app/[slug]/page.tsx
import { Metadata } from "next";
import { getClient } from "~/lib/sanity";
 
// Function to generate metadata for the page
export async function generateMetadata({ params }): Promise<Metadata> {
  const { slug } = params;
 
  // Get your page data from Sanity
  const page = await getClient().fetch(
    `
    *[_type == "page" && slug.current == $slug][0]
  `,
    { slug }
  );
 
  return {
    // ... additional metadata
    openGraph: {
      // ... additional open graph metadata
      images: [
        {
          url: `/api/og?id=${page._id}`,
          width: 1200,
          height: 630,
        },
      ],
    },
  };
}
 
// Your page component...

This setup generates metadata dynamically for each page, uses the page's Sanity ID to generate the correct OG image, and maintains consistent dimensions across platforms.

Testing your implementation

There are a few ways to test your implementation, the easiest is to deploy to a preview environment. Once deployed you can use the Vercel toolbar to preview your site and see the OG image.

Vercel OG Image Debugger

This is a great way to get instant feedback on your implementation and debug any issues you may have with the OG image.

An alternative is to use the platform-specific debugging tools:

These tools allow you to test your OG image on the platform you are sharing to, and see the preview image. There is an added benefit of these tools, that some of them force the cache to be invalidated, which means you can see the latest version of your OG image across that social platform.

The next lesson will cover remixing content for social platforms using the Sanity AI Assistant.