@shinyaz

Auto-Generating Dynamic OG Images for a Next.js Blog

3 min read
Table of Contents

The Problem

When I shared blog posts on social media, every article showed the same generic image. It was impossible to tell articles apart at a glance — not great for a blog that publishes regularly in two languages.

I wanted each post to have a unique OG image that includes the title, category, date, and author name, generated automatically at build time with zero runtime cost.

Choosing an Approach

There are several ways to generate dynamic OG images. I went with the Next.js App Router opengraph-image.tsx file convention because it required zero additional dependencies:

ApproachProsCons
External service (Cloudinary, etc.)Easy setupExternal dependency, cost
Canvas APIFlexibleComplex setup in Node.js
opengraph-image.tsxBuilt into Next.js, zero dependenciesSatori layout constraints

The key advantages: next/og (Satori + Resvg) ships with Next.js, placing the file in a route segment auto-generates all the og:image meta tags, and generateStaticParams makes everything static at build time. No changes to generateMetadata required.

How opengraph-image.tsx Works

This is the part I found most elegant. When you place an opengraph-image.tsx file in a route segment, Next.js automatically:

  1. Calls the default export function to produce an ImageResponse
  2. Serves the generated image as a .png at the route
  3. Injects og:image, og:image:width, og:image:height, and twitter:image meta tags into the page's <head>

No manual meta tag management needed. The file-based Metadata API handles everything.

The Satori Gotcha: Inline Styles Only

Here's what the docs don't emphasize enough: Satori only supports inline styles with Flexbox layout. No className, no Tailwind, no CSS-in-JS. Every style must be a style={{ }} object. This is the single biggest constraint when designing OG image layouts.

I defined the layout as a React component in src/lib/og-image.tsx, keeping the blog's monochrome design:

src/lib/og-image.tsx
export function OgImageLayout({
  title,
  date,
  category,
  author,
  siteName,
}: OgImageLayoutProps) {
  return (
    <div
      style={{
        width: "1200px",
        height: "630px",
        display: "flex",
        flexDirection: "column",
        justifyContent: "space-between",
        padding: "60px 80px",
        backgroundColor: "#fafafa",
        fontFamily: "IBM Plex Sans, IBM Plex Sans JP",
      }}
    >
      {/* Top: category badge + date */}
      {/* Center: title (auto-sizes from 52px to 42px for long titles) */}
      {/* Bottom: author + site name */}
    </div>
  );
}

A few design details worth noting:

  • Title auto-sizing: titles over 40 characters automatically shrink from 52px to 42px
  • Colors: Background #fafafa, text #111111, subtext #737373 — matching the blog's monochrome theme
  • Size: 1200x630px, the recommended OG image dimensions

Font Handling

Satori can't use CSS @font-face — font data must be passed as an ArrayBuffer. Since this blog uses IBM Plex Sans / IBM Plex Sans JP, I bundled the Bold TTF files locally:

src/assets/fonts/
  IBMPlexSans-Bold.ttf      # Latin (~200KB)
  IBMPlexSansJP-Bold.ttf    # Japanese (~5.5MB)

Fetching from an external URL is possible, but local bundling avoids network dependencies during builds. The font loading function caches in module-level variables to avoid repeated disk reads when generating images for multiple posts.

The Route Implementation

The opengraph-image.tsx file lives in the same directory as the blog post page. The interesting part is that both page.tsx and opengraph-image.tsx need identical generateStaticParams logic — I extracted this into a shared generateBlogStaticParams() utility to avoid duplication.

The route itself loads fonts, fetches the post data using existing helper functions (getPostBySlug, getCategoryName, formatDate), and returns an ImageResponse with the layout component and font configuration. Named exports for alt, size, and contentType tell Next.js how to generate the meta tags.

Summary

  • File-based Metadata API is powerful — placing opengraph-image.tsx in a route segment handles all meta tag generation automatically. This is one of the underrated features of Next.js App Router.
  • Satori's inline-style constraint is the main challenge — plan your layout around Flexbox and inline styles from the start. Don't try to reuse your Tailwind components.
  • Bundle fonts locally for bilingual sites — CJK fonts are large (~5.5MB for Japanese), but the alternative is build-time network failures. Cache the font data at module level to avoid repeated reads.
  • Share generateStaticParams when multiple files need it — extracting it into a utility prevents drift between page.tsx and opengraph-image.tsx.

Share this post

Shinya Tahara

Shinya Tahara

Solutions Architect @ AWS

I'm a Solutions Architect at AWS, providing technical guidance primarily to financial industry customers. I share learnings about cloud architecture and AI/ML on this blog.

Related Posts