arrow_back Back to blog
Tutorials March 6, 2026 8 min read

How to Generate OG Images Programmatically in 2026

Open Graph images are the preview cards that appear when someone shares a link on Twitter, LinkedIn, Slack, Discord, or iMessage. They’re one of the highest-impact, lowest-effort ways to increase click-through rates on shared content — yet most sites either use a single static image for every page or skip OG images entirely.

The reason is simple: generating dynamic OG images has historically been a pain. You need to build an HTML template, host it somewhere, screenshot it at 1200x630, and handle caching. Each blog post or product page needs its own image with the correct title, branding, and metadata.

This guide shows you how to do it in 2026 without any of that complexity, using the len.sh /v1/og endpoint.


What Is an OG Image?

An Open Graph image is the og:image meta tag in your HTML <head>. When a social platform or messaging app crawls your URL, it reads this tag and renders the image as a rich preview card.

<meta property="og:image" content="https://yoursite.com/og/my-blog-post.png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />

The standard size is 1200x630 pixels. This ratio works across Twitter cards, LinkedIn previews, Facebook shares, Slack unfurls, and Discord embeds.

A dynamic OG image — one that changes per page — significantly outperforms a generic site-wide image. Blog posts with custom OG images see measurably higher engagement because the preview card communicates the actual content of the page.


The len.sh /v1/og Endpoint

Instead of building HTML templates, hosting them, and screenshotting them, len.sh provides a dedicated endpoint that generates branded OG images from structured parameters:

curl "https://api.len.sh/v1/og?title=How+to+Build+a+REST+API&subtitle=A+step-by-step+guide&badge=Tutorial&brand_name=Dev+Blog&brand_color=%234F46E5&theme=dark&access_key=YOUR_API_KEY" \
  --output og.png

This returns a 1200x630 PNG with your title, subtitle, badge, and branding — ready to use as an og:image.

All Parameters

ParameterTypeDefaultDescription
titlestringrequiredMain heading text (max 200 chars, clamped to 3 lines)
subtitlestringSecondary text below the title (max 300 chars)
badgestringSmall pill label above the title, e.g. “Tutorial” (max 50 chars)
urlstringURL displayed in the footer (max 200 chars)
brand_namestringYour brand name in the footer (max 100 chars)
brand_colorstring#4850e5Accent color in hex format
themestringdarkColor theme: dark or light
formatstringpngOutput format: png, jpeg, or webp
qualityinteger90Image quality (1-100)
cache_ttlinteger86400Cache duration in seconds (0 = bypass, max 2592000)

The only required parameter is title. Everything else is optional, so you can start simple and add branding later.


Building Your First OG Image

Step 1: Basic Title Only

curl "https://api.len.sh/v1/og?title=Hello+World&access_key=YOUR_API_KEY" \
  --output basic.png

This generates a minimal OG image with just the title on a dark background.

Step 2: Add Context

curl "https://api.len.sh/v1/og?title=How+to+Build+a+REST+API&subtitle=A+practical+guide+with+Node.js+and+Express&badge=Tutorial&access_key=YOUR_API_KEY" \
  --output with-context.png

The badge appears as a small pill above the title — useful for categorizing content (Blog, Tutorial, Product Update, etc.). The subtitle provides additional context below the main heading.

Step 3: Full Branding

curl "https://api.len.sh/v1/og?title=How+to+Build+a+REST+API&subtitle=A+practical+guide+with+Node.js+and+Express&badge=Tutorial&url=devblog.com/rest-api-guide&brand_name=Dev+Blog&brand_color=%23FF6600&theme=light&access_key=YOUR_API_KEY" \
  --output branded.png

Now you have a fully branded OG image with your brand name, accent color, URL, and a light theme. The brand_color controls the accent elements — badges, dividers, and other visual highlights.


Integrating with Your Site

Static HTML

The simplest integration is a direct URL in your <head>:

<head>
  <meta property="og:title" content="How to Build a REST API" />
  <meta property="og:description" content="A practical guide with Node.js and Express" />
  <meta property="og:image" content="https://api.len.sh/v1/og?title=How+to+Build+a+REST+API&subtitle=A+practical+guide&badge=Tutorial&brand_name=Dev+Blog&brand_color=%23FF6600&theme=dark&access_key=YOUR_API_KEY" />
  <meta property="og:image:width" content="1200" />
  <meta property="og:image:height" content="630" />
</head>

This works but exposes your API key in the HTML source. For production, use signed URLs instead.

Signed URLs let you embed the OG image URL directly in your HTML without exposing the API key:

import { signUrl } from "@lensh/sdk";

function getOgImageUrl(title, subtitle, badge) {
  const params = new URLSearchParams({
    title,
    subtitle: subtitle || "",
    badge: badge || "",
    brand_name: "Dev Blog",
    brand_color: "#FF6600",
    theme: "dark",
  });

  const baseUrl = `https://api.len.sh/v1/og?${params}`;

  return signUrl(baseUrl, {
    keyId: process.env.LENSH_KEY_ID,
    signingSecret: process.env.LENSH_SIGNING_SECRET,
  });
}

The signed URL is safe to embed in public HTML — no API key is exposed.

Next.js Integration

In a Next.js app, generate the OG image URL at build time or on the server:

// app/blog/[slug]/page.jsx
import { signUrl } from "@lensh/sdk";

export async function generateMetadata({ params }) {
  const post = await getPost(params.slug);

  const ogParams = new URLSearchParams({
    title: post.title,
    subtitle: post.excerpt,
    badge: post.category,
    url: `yourblog.com/blog/${params.slug}`,
    brand_name: "Your Blog",
    brand_color: "#4F46E5",
    theme: "dark",
  });

  const ogImageUrl = signUrl(
    `https://api.len.sh/v1/og?${ogParams}`,
    {
      keyId: process.env.LENSH_KEY_ID,
      signingSecret: process.env.LENSH_SIGNING_SECRET,
    }
  );

  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      images: [{ url: ogImageUrl, width: 1200, height: 630 }],
    },
    twitter: {
      card: "summary_large_image",
      title: post.title,
      description: post.excerpt,
      images: [ogImageUrl],
    },
  };
}

Every blog post now gets a unique, branded OG image automatically.

Astro Integration

In an Astro site (like this blog), you can generate OG URLs in the frontmatter:

---
// src/pages/blog/[slug].astro
import { signUrl } from "@lensh/sdk";

const { slug } = Astro.params;
const post = await getPost(slug);

const ogParams = new URLSearchParams({
  title: post.title,
  subtitle: post.description,
  badge: post.category,
  brand_name: "Your Blog",
  brand_color: "#4F46E5",
  theme: "dark",
});

const ogImage = signUrl(
  `https://api.len.sh/v1/og?${ogParams}`,
  {
    keyId: import.meta.env.LENSH_KEY_ID,
    signingSecret: import.meta.env.LENSH_SIGNING_SECRET,
  }
);
---

<head>
  <meta property="og:image" content={ogImage} />
  <meta property="og:image:width" content="1200" />
  <meta property="og:image:height" content="630" />
</head>

Django Integration

# views.py
from lensh import sign_url

def blog_post(request, slug):
    post = Post.objects.get(slug=slug)

    og_params = {
        "title": post.title,
        "subtitle": post.excerpt,
        "badge": post.category.name,
        "url": f"yourblog.com/blog/{slug}",
        "brand_name": "Your Blog",
        "brand_color": "#4F46E5",
        "theme": "dark",
    }

    query_string = "&".join(f"{k}={v}" for k, v in og_params.items())
    og_image_url = sign_url(
        f"https://api.len.sh/v1/og?{query_string}",
        key_id=settings.LENSH_KEY_ID,
        signing_secret=settings.LENSH_SIGNING_SECRET,
    )

    return render(request, "blog/post.html", {
        "post": post,
        "og_image": og_image_url,
    })
<!-- blog/post.html -->
<meta property="og:image" content="{{ og_image }}" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />

Using the API Endpoint as a Proxy

If you prefer not to use signed URLs, you can proxy the OG image through your own server. This keeps the API key on the server side:

// /api/og-image.js (Next.js API route)
export default async function handler(req, res) {
  const { title, subtitle, badge } = req.query;

  const params = new URLSearchParams({
    title: title || "Untitled",
    subtitle: subtitle || "",
    badge: badge || "",
    brand_name: "Dev Blog",
    brand_color: "#4F46E5",
    theme: "dark",
    access_key: process.env.LENSH_API_KEY,
  });

  const response = await fetch(`https://api.len.sh/v1/og?${params}`);

  res.setHeader("Content-Type", response.headers.get("Content-Type"));
  res.setHeader("Cache-Control", "public, max-age=86400");

  const buffer = await response.arrayBuffer();
  res.send(Buffer.from(buffer));
}

Then reference it in your HTML:

<meta property="og:image" content="https://yoursite.com/api/og-image?title=My+Post&badge=Blog" />

Themes and Customization

Dark Theme (Default)

The dark theme uses a dark background with light text. Works well for tech blogs, developer tools, and SaaS products.

curl "https://api.len.sh/v1/og?title=Dark+Theme+Example&theme=dark&brand_color=%234F46E5&access_key=YOUR_API_KEY" \
  --output dark.png

Light Theme

The light theme uses a white/light background with dark text. Better for content sites, media, and non-technical brands.

curl "https://api.len.sh/v1/og?title=Light+Theme+Example&theme=light&brand_color=%23FF6600&access_key=YOUR_API_KEY" \
  --output light.png

Brand Colors

The brand_color parameter accepts any hex color and applies it to accent elements. Match it to your brand’s primary color:

# Stripe purple
brand_color=%23635BFF

# Vercel black
brand_color=%23000000

# Tailwind indigo
brand_color=%234F46E5

# Custom orange
brand_color=%23FF6600

Output Formats

The /v1/og endpoint supports three output formats:

  • PNG (default) — lossless, best for social platforms
  • JPEG — smaller file size, slight quality loss
  • WebP — best compression, supported by all modern platforms

For OG images, PNG is the safest choice because it’s universally supported. If file size is a concern (e.g., you’re generating thousands of images and caching them), WebP gives the best compression ratio.

# WebP for smaller file size
curl "https://api.len.sh/v1/og?title=Hello&format=webp&quality=85&access_key=YOUR_API_KEY" \
  --output og.webp

Caching

The /v1/og endpoint caches generated images on Cloudflare’s edge network. The default TTL is 24 hours (86400 seconds).

  • Cache HIT — the image is served from the edge cache in ~50-100ms
  • Cache MISS — the image is generated fresh in ~200-500ms

You can check the X-Len-Cache response header to see if a request was served from cache:

curl -I "https://api.len.sh/v1/og?title=Hello&access_key=YOUR_API_KEY"
# X-Len-Cache: HIT

To bypass the cache (e.g., after updating your branding), set cache_ttl=0:

curl "https://api.len.sh/v1/og?title=Hello&cache_ttl=0&access_key=YOUR_API_KEY" \
  --output fresh.png

Common Patterns

Blog Posts

curl "https://api.len.sh/v1/og?title=Understanding+WebSockets&subtitle=A+practical+guide+to+real-time+communication&badge=Tutorial&url=devblog.com/websockets&brand_name=Dev+Blog&brand_color=%234F46E5&theme=dark&access_key=YOUR_API_KEY"

Product Pages

curl "https://api.len.sh/v1/og?title=Introducing+v2.0&subtitle=Faster+builds,+better+DX,+new+plugin+system&badge=Product+Update&brand_name=Acme+Tools&brand_color=%2310B981&theme=light&access_key=YOUR_API_KEY"

Documentation Pages

curl "https://api.len.sh/v1/og?title=Authentication+Guide&subtitle=Learn+how+to+integrate+OAuth2+and+API+keys&badge=Docs&url=docs.acme.com/auth&brand_name=Acme&brand_color=%23F59E0B&theme=dark&access_key=YOUR_API_KEY"

Changelog Entries

curl "https://api.len.sh/v1/og?title=March+2026+Changelog&subtitle=PDF+generation,+signed+URLs,+and+5+new+SDKs&badge=Changelog&brand_name=Acme&brand_color=%238B5CF6&theme=dark&access_key=YOUR_API_KEY"

The Alternative: Screenshot-Based OG Images

If you need full pixel-level control over your OG image design — custom layouts, images, gradients, or illustrations — you can use the /v1/screenshot endpoint with an HTML template instead:

curl "https://api.len.sh/v1/screenshot?url=https://yoursite.com/og-template?title=Hello&width=1200&height=630&format=png&access_key=YOUR_API_KEY" \
  --output custom-og.png

Or render raw HTML directly via POST:

curl -X POST https://api.len.sh/v1/screenshot \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "html": "<div style=\"width:1200px;height:630px;background:#1a1a2e;display:flex;align-items:center;justify-content:center;color:white;font-size:48px;font-family:system-ui\">Custom OG Image</div>",
    "width": 1200,
    "height": 630,
    "format": "png"
  }' \
  --output custom-og.png

The /v1/og endpoint is faster and simpler for most cases. Use the screenshot approach only when you need layout control beyond what the structured parameters offer.


Testing Your OG Images

After implementing, test your OG images with these tools:

  1. Twitter Card Validator — preview how your card will look on Twitter/X
  2. LinkedIn Post Inspector — check your preview on LinkedIn
  3. Facebook Sharing Debugger — validate and clear Facebook’s OG cache
  4. Open Graph Preview — generic OG tag validator

Make sure to include all three essential meta tags:

<meta property="og:image" content="YOUR_OG_IMAGE_URL" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />

The width and height tags help platforms render the preview correctly without fetching the image first.


Getting Started

Dynamic OG images are one of those small details that meaningfully improve how your content appears when shared. With the len.sh /v1/og endpoint, you can add them to every page on your site in under an hour.

  1. Sign up for a free len.sh account
  2. Generate your first OG image with a curl command
  3. Integrate the URL into your site’s meta tags using signed URLs or a server-side proxy
  4. Test with social platform validators

The free tier includes 100 requests per month — enough to build and test your integration. All OG image parameters are available on every plan.

Try len.sh for free

Start capturing screenshots with a simple API call. No credit card required.