DEV Community

toolzip
toolzip

Posted on Originally published at toolzip.app

Generating Dynamic OG Images in Next.js Without @vercel/og

Generating Dynamic OG Images in Next.js Without @vercel/og

Open Graph images (the preview image when you share a URL on social media) should be dynamic — showing the actual page title. Here's how to do it in Next.js without relying on a heavy library.

The Goal

When someone shares toolzip.app/tools/image-compress, the OG image should say "Image Compress — ToolZip" instead of a generic site logo.

Option 1: Static OG Images Per Page

The simplest approach: generate images at build time.

// In your page component
export const metadata = {
  openGraph: {
    title: 'Image Compress — ToolZip',
    images: [{ url: '/og/image-compress.png', width: 1200, height: 630 }],
  },
};
Enter fullscreen mode Exit fullscreen mode

This works but requires generating and committing hundreds of images.

Option 2: Canvas-Based OG Image API

A serverless function that generates images on demand:

// /app/api/og/route.ts
import { NextRequest } from 'next/server';

export async function GET(req: NextRequest) {
  const title = req.nextUrl.searchParams.get('title') ?? 'ToolZip';

  // Create canvas
  const { createCanvas } = await import('@napi-rs/canvas');
  const canvas = createCanvas(1200, 630);
  const ctx = canvas.getContext('2d');

  // Background gradient
  const grad = ctx.createLinearGradient(0, 0, 1200, 630);
  grad.addColorStop(0, '#6366f1');
  grad.addColorStop(1, '#ec4899');
  ctx.fillStyle = grad;
  ctx.fillRect(0, 0, 1200, 630);

  // Title text
  ctx.fillStyle = '#ffffff';
  ctx.font = 'bold 64px sans-serif';
  ctx.textAlign = 'center';
  ctx.fillText(title, 600, 280);

  // Site name
  ctx.font = '32px sans-serif';
  ctx.fillStyle = 'rgba(255,255,255,0.7)';
  ctx.fillText('toolzip.app', 600, 360);

  const buffer = canvas.toBuffer('image/png');
  return new Response(buffer, {
    headers: { 'Content-Type': 'image/png', 'Cache-Control': 'public, max-age=86400' },
  });
}
Enter fullscreen mode Exit fullscreen mode

Using the OG Image

// In each tool page
export function generateMetadata({ params }) {
  const tool = getToolById(params.id);
  return {
    openGraph: {
      images: [`/api/og?title=${encodeURIComponent(tool.name)}`],
    },
  };
}
Enter fullscreen mode Exit fullscreen mode

Caching

OG images should be cached aggressively — they rarely change. The Cache-Control: public, max-age=86400 header caches them for 24 hours at the CDN level.

Text Wrapping

Long titles need wrapping. Canvas doesn't do this automatically:

function wrapText(ctx, text, maxWidth, lineHeight, x, y) {
  const words = text.split(' ');
  let line = '';
  let currentY = y;

  for (const word of words) {
    const test = line + word + ' ';
    if (ctx.measureText(test).width > maxWidth && line) {
      ctx.fillText(line, x, currentY);
      line = word + ' ';
      currentY += lineHeight;
    } else {
      line = test;
    }
  }
  ctx.fillText(line, x, currentY);
}
Enter fullscreen mode Exit fullscreen mode

All 76 tools on toolzip.app have dynamic OG images generated this way.

Top comments (0)