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
| Parameter | Type | Default | Description |
|---|---|---|---|
title | string | required | Main heading text (max 200 chars, clamped to 3 lines) |
subtitle | string | — | Secondary text below the title (max 300 chars) |
badge | string | — | Small pill label above the title, e.g. “Tutorial” (max 50 chars) |
url | string | — | URL displayed in the footer (max 200 chars) |
brand_name | string | — | Your brand name in the footer (max 100 chars) |
brand_color | string | #4850e5 | Accent color in hex format |
theme | string | dark | Color theme: dark or light |
format | string | png | Output format: png, jpeg, or webp |
quality | integer | 90 | Image quality (1-100) |
cache_ttl | integer | 86400 | Cache 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.
Using Signed URLs (Recommended)
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:
- Twitter Card Validator — preview how your card will look on Twitter/X
- LinkedIn Post Inspector — check your preview on LinkedIn
- Facebook Sharing Debugger — validate and clear Facebook’s OG cache
- 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.
- Sign up for a free len.sh account
- Generate your first OG image with a curl command
- Integrate the URL into your site’s meta tags using signed URLs or a server-side proxy
- 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.