How to Generate Dynamic OG Images with an API

Rendex Team··6 min read
og-imagestutorialapi
TypeScript code generating a dynamic OG image with the Rendex API, showing html parameter and 1200x630 viewport for generate og images api

Every link shared on Slack, X, or LinkedIn pulls an OG image from the target URL. If that image is missing or static, engagement drops. If it is dynamic (title, description, or branding that changes per page), it requires a rendering service.

The standard solution is @vercel/og, which works great if you deploy on Vercel. For Next.js on Netlify, Railway, or bare VMs, and for Astro or Nuxt projects, you need a different approach. This tutorial shows how to generate dynamic OG images with the Rendex API across all three frameworks.

Prerequisites

  • A Rendex API key (free tier, 100 calls/month). Get one here.
  • Node.js 18+ or Bun (for the JS SDK). Python 3.9+ if using the Python SDK.
  • An existing Next.js, Astro, or Nuxt project to add OG images to.

How It Works

The Rendex API accepts raw HTML in the request body and renders it to a PNG. You define an HTML template for your OG image (title, author, branding), pass it to the API with a 1200×630px viewport, and receive back a PNG ready to serve as an og:image.

No headless browser to manage. No Vercel lock-in. The rendered image comes back as binary data you can stream directly to the client or store in a CDN.

og-image-basic.ts
import { Rendex } from "@copperline/rendex"
// npm install @copperline/rendex
// Get your API key at https://rendex.dev/login

const rendex = new Rendex("rdx_your_key")

// Generate an OG image from raw HTML
const { image } = await rendex.screenshot({
  html: `<!DOCTYPE html>
<html>
<body style="margin:0;background:#0f172a;display:flex;align-items:center;
             justify-content:center;width:1200px;height:630px;
             font-family:sans-serif;color:white">
  <div style="padding:80px;text-align:left">
    <p style="color:#94a3b8;font-size:18px;margin:0 0 12px">rendex.dev</p>
    <h1 style="font-size:64px;font-weight:700;margin:0 0 20px;line-height:1.1">
      How to generate OG images with an API
    </h1>
    <p style="color:#64748b;font-size:22px;margin:0">5 min read</p>
  </div>
</body>
</html>`,
  width: 1200,
  height: 630,
  format: "png",
})

// image is Uint8Array — write to disk or stream as a response
// Uint8ArrayToBuffer: Buffer.from(image)

The html parameter accepts any valid HTML including inline CSS, web fonts via @import, and CSS Grid or Flexbox layouts. For the full parameter reference see the API reference.

Step 1: Next.js (App Router)

In Next.js App Router, create a Route Handler that accepts a page title as a query parameter and returns a PNG:

app/og/route.ts
// app/og/route.ts
import { Rendex } from "@copperline/rendex"

const rendex = new Rendex(process.env.RENDEX_API_KEY!)
// Add RENDEX_API_KEY to your .env.local
// Get your key at https://rendex.dev/login

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url)
  const title = searchParams.get("title") ?? "My Site"
  const description = searchParams.get("description") ?? ""

  const { image } = await rendex.screenshot({
    html: `<!DOCTYPE html>
<html>
<body style="margin:0;background:#0f172a;width:1200px;height:630px;
             display:flex;align-items:center;font-family:system-ui,sans-serif;color:white">
  <div style="padding:80px">
    <p style="color:#94a3b8;font-size:18px;margin:0 0 16px">My Site</p>
    <h1 style="font-size:56px;font-weight:700;margin:0 0 20px;line-height:1.15">
      ${title.replace(/</g, "&lt;")}
    </h1>
    <p style="color:#64748b;font-size:22px;margin:0">
      ${description.replace(/</g, "&lt;")}
    </p>
  </div>
</body>
</html>`,
    width: 1200,
    height: 630,
    format: "png",
  })

  return new Response(image, {
    headers: {
      "Content-Type": "image/png",
      "Cache-Control": "public, max-age=86400, immutable",
    },
  })
}

Reference this endpoint from your page metadata:

app/blog/[slug]/page.tsx
// app/blog/[slug]/page.tsx
import type { Metadata } from "next"

export async function generateMetadata({ params }: { params: { slug: string } }): Promise<Metadata> {
  const post = await getPost(params.slug)

  return {
    openGraph: {
      images: [
        {
          url: `/og?title=${encodeURIComponent(post.title)}&description=${encodeURIComponent(post.excerpt)}`,
          width: 1200,
          height: 630,
        },
      ],
    },
  }
}

Step 2: Astro

In Astro, create an API endpoint at src/pages/og/[...slug].png.ts:

src/pages/og/[...slug].png.ts
// src/pages/og/[...slug].png.ts
import type { APIRoute } from "astro"
import { Rendex } from "@copperline/rendex"

const rendex = new Rendex(import.meta.env.RENDEX_API_KEY)
// Add RENDEX_API_KEY to your .env
// Get your key at https://rendex.dev/login

export const GET: APIRoute = async ({ params, request }) => {
  const url = new URL(request.url)
  const title = url.searchParams.get("title") ?? "My Site"

  const { image } = await rendex.screenshot({
    html: `<!DOCTYPE html>
<html>
<body style="margin:0;background:#0f172a;width:1200px;height:630px;
             display:flex;align-items:center;font-family:sans-serif;color:white">
  <div style="padding:80px">
    <h1 style="font-size:56px;font-weight:700;margin:0;line-height:1.15">
      ${title.replace(/</g, "&lt;")}
    </h1>
  </div>
</body>
</html>`,
    width: 1200,
    height: 630,
    format: "png",
  })

  return new Response(image, {
    headers: {
      "Content-Type": "image/png",
      "Cache-Control": "public, max-age=86400",
    },
  })
}

Step 3: Nuxt

In Nuxt, add a server route at server/routes/og.get.ts:

server/routes/og.get.ts
// server/routes/og.get.ts
import { Rendex } from "@copperline/rendex"

export default defineEventHandler(async (event) => {
  const query = getQuery(event)
  const title = String(query.title ?? "My Site")

  // RENDEX_API_KEY in your .env file
  // Get your key at https://rendex.dev/login
  const rendex = new Rendex(process.env.RENDEX_API_KEY!)

  const { image } = await rendex.screenshot({
    html: `<!DOCTYPE html>
<html>
<body style="margin:0;background:#0f172a;width:1200px;height:630px;
             display:flex;align-items:center;font-family:sans-serif;color:white">
  <div style="padding:80px">
    <h1 style="font-size:56px;font-weight:700;margin:0;line-height:1.15">
      ${title.replace(/</g, "&lt;")}
    </h1>
  </div>
</body>
</html>`,
    width: 1200,
    height: 630,
    format: "png",
  })

  setResponseHeader(event, "Content-Type", "image/png")
  setResponseHeader(event, "Cache-Control", "public, max-age=86400")
  return image
})

Quick Test with cURL

Before wiring any framework, confirm the Rendex API works with a direct call:

test-og-image.sh
curl -X POST https://api.rendex.dev/v1/screenshot \
  -H "Authorization: Bearer rdx_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "html": "<body style=\"margin:0;background:#0f172a;width:1200px;height:630px;display:flex;align-items:center\"><h1 style=\"color:white;padding:80px;font-size:64px;font-family:sans-serif\">Hello OG</h1></body>",
    "width": 1200,
    "height": 630,
    "format": "png"
  }' --output og-test.png

open og-test.png  # macOS — should show a 1200x630 dark image

Troubleshooting

Image is blank or all black: The body element needs an explicit width and height in the HTML itself when using flex layout. Set width: 1200px; height: 630px on the body style.

Text is clipped: The viewport dimensions in your API call (width: 1200, height: 630) must match the HTML body dimensions exactly. A mismatch causes Chromium to crop the output.

Fonts not rendering correctly: System fonts (sans-serif, serif) render as the Chromium default, which varies. For consistent cross-platform rendering, embed a web font via @import url(...) in the HTML <style> block.

HTML entities in dynamic content: Always escape user content before injecting it into the HTML template. At minimum, replace < with &lt; and & with &amp; to prevent template injection.

Next Steps

The same approach works for social cards with custom branding, email header images, and preview thumbnails for any content type. See the OG image generation use case for more patterns.

For a no-code test, try the OG image preview toolto inspect how a URL's existing OG image renders across different platforms, or the HTML-to-image tool to render any HTML template to a PNG in your browser.

Ready to generate OG images in production? Get a free API key (100 calls/month, no credit card) and have the first image rendering in under five minutes.

Try Rendex Free

100 screenshots/month. No credit card required.

Get API Key