OG Image Text Overlay: Add Titles and Descriptions to Your Social Preview Images

How to add dynamic text overlays to OG images — using Satori, @vercel/og, Cloudinary, html2canvas, Puppeteer, or CSS text-on-image techniques for every page's share card.

Updated March 2026

Why Text Overlays Matter

A static background image shared across your whole site is a missed opportunity. Unique OG images with the article title, author name, or category overlaid as text dramatically increase click-through rates on social media — because users know exactly what they're clicking before they arrive.

This guide covers five approaches to generate OG images with dynamic text, ordered from easiest to most custom.

Approach 1: @vercel/og (Recommended for Next.js)

Vercel's @vercel/og library lets you write React JSX and render it as a PNG image at the edge. It uses satori under the hood to convert HTML/CSS to SVG, then converts to PNG.

// app/og/route.tsx (Next.js App Router)
import { ImageResponse } from "next/og";
import { NextRequest } from "next/server";

export const runtime = "edge";

export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url);
  const title = searchParams.get("title") ?? "My Article Title";
  const description = searchParams.get("description") ?? "";

  return new ImageResponse(
    (
      <div
        style={{
          width: "1200px",
          height: "630px",
          background: "linear-gradient(135deg, #0f0f0f 0%, #1a1a2e 100%)",
          display: "flex",
          flexDirection: "column",
          justifyContent: "center",
          padding: "80px",
          fontFamily: "sans-serif",
        }}
      >
        <div style={{ color: "#a78bfa", fontSize: "20px", marginBottom: "16px" }}>
          My Blog
        </div>
        <div
          style={{
            color: "white",
            fontSize: "60px",
            fontWeight: 900,
            lineHeight: 1.1,
            marginBottom: "24px",
          }}
        >
          {title}
        </div>
        <div style={{ color: "#a1a1aa", fontSize: "28px", lineHeight: 1.4 }}>
          {description}
        </div>
      </div>
    ),
    { width: 1200, height: 630 }
  );
}

Then reference it in your page metadata:

// app/blog/[slug]/page.tsx
export async function generateMetadata({ params }) {
  const post = await getPost(params.slug);
  const ogImageUrl = `https://mysite.com/og?title=${encodeURIComponent(post.title)}&description=${encodeURIComponent(post.excerpt)}`;

  return {
    openGraph: {
      images: [{ url: ogImageUrl, width: 1200, height: 630 }],
    },
  };
}

Approach 2: Satori (Framework-Agnostic)

satori by Vercel converts HTML/CSS to SVG directly, without a headless browser. You can use it in any Node.js environment.

import satori from "satori";
import sharp from "sharp";
import fs from "fs";

async function generateOgImage(title: string, outputPath: string) {
  const svg = await satori(
    {
      type: "div",
      props: {
        style: {
          width: 1200,
          height: 630,
          background: "#0f0f0f",
          display: "flex",
          alignItems: "center",
          justifyContent: "center",
          padding: 80,
        },
        children: {
          type: "span",
          props: {
            style: { color: "white", fontSize: 64, fontWeight: 900 },
            children: title,
          },
        },
      },
    },
    {
      width: 1200,
      height: 630,
      fonts: [
        {
          name: "Inter",
          data: fs.readFileSync("./fonts/Inter-Bold.ttf"),
          weight: 900,
          style: "normal",
        },
      ],
    }
  );

  await sharp(Buffer.from(svg)).png().toFile(outputPath);
}

Approach 3: Cloudinary URL-Based Text Overlays

Cloudinary lets you add text overlays to images by encoding parameters directly in the image URL — no server-side code required. This is the fastest approach if you're already using Cloudinary for image storage.

// Base template: https://res.cloudinary.com/YOUR_CLOUD/image/upload/
// Add text overlay parameters:

const title = "My Article Title";
const encodedTitle = encodeURIComponent(title);

const ogImageUrl = [
  "https://res.cloudinary.com/YOUR_CLOUD/image/upload",
  "w_1200,h_630,c_fill",           // dimensions
  "b_rgb:0f0f0f",                  // background
  `l_text:Arial_64_bold:${encodedTitle},co_white,g_west,x_80,y_-60`, // title
  "v1/og-template.png",             // your base template image
].join("/");

The entire transformation happens at Cloudinary's CDN — no computation on your server. URLs are cacheable and can be embedded directly in og:image tags.

Approach 4: Puppeteer / Playwright Screenshot

The most flexible but slowest approach: build an HTML template page with your text overlay design, then screenshot it with Puppeteer or Playwright to produce a PNG.

import puppeteer from "puppeteer";

async function generateOgImage(title: string, outputPath: string) {
  const browser = await puppeteer.launch({ args: ["--no-sandbox"] });
  const page = await browser.newPage();
  await page.setViewport({ width: 1200, height: 630 });

  await page.setContent(`
    <!DOCTYPE html>
    <html>
      <head>
        <style>
          body { margin: 0; background: #0f0f0f; font-family: sans-serif; }
          .card {
            width: 1200px; height: 630px;
            display: flex; align-items: center; justify-content: center; padding: 80px;
            box-sizing: border-box;
          }
          h1 { color: white; font-size: 64px; font-weight: 900; margin: 0; }
        </style>
      </head>
      <body><div class="card"><h1>${title}</h1></div></body>
    </html>
  `);

  await page.screenshot({ path: outputPath, clip: { x: 0, y: 0, width: 1200, height: 630 } });
  await browser.close();
}

This approach is best for build-time generation (e.g., a static site generator script). For on-demand generation in production, use @vercel/og or satori instead — Puppeteer is too slow for edge functions.

Approach 5: Build-Time Generation with Sharp

For static sites that know all their pages at build time (Jekyll, Hugo, Eleventy, Gatsby), you can generate OG images during the build using sharp and a canvas library.

import sharp from "sharp";
import { createCanvas } from "@napi-rs/canvas";

async function generateOgImage(title: string, outputPath: string) {
  const canvas = createCanvas(1200, 630);
  const ctx = canvas.getContext("2d");

  // Background
  ctx.fillStyle = "#0f0f0f";
  ctx.fillRect(0, 0, 1200, 630);

  // Title text
  ctx.fillStyle = "white";
  ctx.font = "bold 60px sans-serif";
  ctx.fillText(title, 80, 340, 1040); // wrap at 1040px wide

  const buffer = canvas.toBuffer("image/png");
  await sharp(buffer).png().toFile(outputPath);
}

Text Overlay Design Best Practices

  • Font size: 60–80px for titles. Anything smaller is unreadable on mobile link previews.
  • Contrast ratio: At least 4.5:1 between text and background. White text on dark backgrounds is safest.
  • Text truncation: Long titles will overflow. Truncate at ~80 characters or use auto-wrap with max-width constraints.
  • Brand elements: Include your logo or domain name — makes previews recognizable even before users read the title.
  • Dimensions: Always generate at exactly 1200×630px. Never scale up a smaller image.
  • File size: Keep under 300KB. Use JPEG compression (quality 85) for image-heavy cards.

Verify Your Generated OG Images

Once your dynamic OG images are live, check them with OGFixer to see exactly how they render in Twitter, LinkedIn, Discord, and Slack card previews.

Preview your generated OG images →

Paste your URL and see how the text overlay looks across all major platforms before you share.

Preview my OG image →