Generate Open Graph Images from HTML with a Free API
When someone shares your link on Twitter, LinkedIn, or Slack, the preview image is your Open Graph image. A good one gets clicks. A missing one gets ignored.
Most OG image generators use templates with limited customization. With an HTML-to-image API, your OG image is whatever you can build with HTML and CSS.
Quick Start
curl -X POST "https://hermesforge.dev/api/html2image" \
-H "Content-Type: application/json" \
-d '{"html": "<div style=\"width:1200px;height:630px;background:linear-gradient(135deg,#667eea,#764ba2);display:flex;align-items:center;justify-content:center;font-family:system-ui;color:white;font-size:48px;padding:60px;text-align:center\">How to Build APIs That People Actually Use</div>", "width": 1200, "height": 630, "format": "png"}' \
-o og-image.png
That's a 1200x630 OG image with a gradient background and centered title. No Puppeteer, no canvas, no server-side rendering setup.
Python: Dynamic OG Images for Blog Posts
import requests
import json
def generate_og_image(title, author="", date="", output="og.png"):
"""Generate an OG image for a blog post."""
html = f"""
<div style="
width: 1200px; height: 630px;
background: linear-gradient(135deg, #0f0c29, #302b63, #24243e);
display: flex; flex-direction: column;
justify-content: center; padding: 80px;
font-family: system-ui, -apple-system, sans-serif;
color: white;
">
<h1 style="font-size: 52px; margin: 0 0 20px 0; line-height: 1.2;">{title}</h1>
<div style="font-size: 24px; color: #a0a0ff;">
{f'{author} · ' if author else ''}{date}
</div>
</div>
"""
response = requests.post(
"https://hermesforge.dev/api/html2image",
json={"html": html, "width": 1200, "height": 630, "format": "png"},
)
with open(output, "wb") as f:
f.write(response.content)
print(f"Generated {output} ({len(response.content)} bytes)")
# Generate OG images for your blog posts
posts = [
{"title": "Building a REST API in 30 Minutes", "date": "March 2026"},
{"title": "Why Your CI/CD Pipeline Is Slower Than It Should Be", "date": "April 2026"},
{"title": "The Case for Boring Technology", "date": "May 2026"},
]
for i, post in enumerate(posts):
generate_og_image(
title=post["title"],
author="Your Name",
date=post["date"],
output=f"og-{i+1}.png",
)
Social Card Templates
Twitter Card (Summary Large Image)
html = """
<div style="
width: 1200px; height: 628px;
background: #1a1a2e; color: white;
font-family: system-ui; display: flex;
">
<div style="flex: 1; padding: 60px; display: flex; flex-direction: column; justify-content: center;">
<h1 style="font-size: 44px; margin: 0 0 16px 0;">Your Article Title</h1>
<p style="font-size: 22px; color: #888; margin: 0;">A brief description that makes people want to click</p>
</div>
<div style="width: 300px; background: linear-gradient(180deg, #e94560, #c81e45); display: flex; align-items: center; justify-content: center;">
<span style="font-size: 120px;">🚀</span>
</div>
</div>
"""
LinkedIn Post Image
html = """
<div style="
width: 1200px; height: 627px;
background: white; color: #333;
font-family: system-ui; padding: 80px;
display: flex; flex-direction: column;
justify-content: space-between;
">
<div>
<h1 style="font-size: 48px; margin: 0 0 20px 0; color: #0a66c2;">
5 Lessons from Scaling to 1M Requests
</h1>
<p style="font-size: 24px; color: #666; line-height: 1.5;">
What we learned about caching, rate limiting, and graceful degradation.
</p>
</div>
<div style="font-size: 20px; color: #999; border-top: 2px solid #eee; padding-top: 20px;">
yoursite.com · 5 min read
</div>
</div>
"""
Retina Quality
Generate 2x resolution images for crisp display on high-DPI screens:
curl -X POST "https://hermesforge.dev/api/html2image" \
-H "Content-Type: application/json" \
-d '{"html": "<div style=\"width:600px;height:315px;background:#1a1a2e;color:white;display:flex;align-items:center;justify-content:center;font-size:32px;font-family:system-ui\">Retina Sharp</div>", "width": 600, "height": 315, "scale": 2, "format": "webp", "quality": 90}' \
-o og-retina.webp
The output is 1200x630 pixels at 2x density — perfect for retina displays, served as WebP for smaller file sizes.
Batch Generation for Static Sites
Generate OG images for every page in a static site at build time:
import requests
import json
import os
POSTS = [
{"slug": "getting-started", "title": "Getting Started Guide"},
{"slug": "api-reference", "title": "API Reference"},
{"slug": "deployment", "title": "Deployment Guide"},
{"slug": "changelog", "title": "Changelog"},
]
OUTPUT_DIR = "public/og"
os.makedirs(OUTPUT_DIR, exist_ok=True)
TEMPLATE = """
<div style="
width:1200px; height:630px;
background: linear-gradient(135deg, {bg1}, {bg2});
display:flex; align-items:center; justify-content:center;
font-family: system-ui; color: white; padding: 80px;
text-align: center;
">
<h1 style="font-size: 56px; margin: 0;">{title}</h1>
</div>
"""
GRADIENTS = [
("#667eea", "#764ba2"),
("#f093fb", "#f5576c"),
("#4facfe", "#00f2fe"),
("#43e97b", "#38f9d7"),
]
for i, post in enumerate(POSTS):
gradient = GRADIENTS[i % len(GRADIENTS)]
html = TEMPLATE.format(title=post["title"], bg1=gradient[0], bg2=gradient[1])
resp = requests.post(
"https://hermesforge.dev/api/html2image",
json={"html": html, "width": 1200, "height": 630, "format": "webp", "quality": 85},
)
path = os.path.join(OUTPUT_DIR, f"{post['slug']}.webp")
with open(path, "wb") as f:
f.write(resp.content)
print(f"Generated {path}")
Add to your HTML head:
<meta property="og:image" content="https://yoursite.com/og/getting-started.webp" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
Dynamic OG Images with a Serverless Function
Generate OG images on-demand by proxying through a serverless function:
// Vercel/Netlify serverless function
export default async function handler(req) {
const title = req.query.title || 'My Site';
const html = `
<div style="width:1200px;height:630px;background:#1a1a2e;color:white;
display:flex;align-items:center;justify-content:center;
font-family:system-ui;font-size:48px;padding:80px;text-align:center">
${title}
</div>
`;
const response = await fetch('https://hermesforge.dev/api/html2image', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ html, width: 1200, height: 630, format: 'png' }),
});
return new Response(await response.arrayBuffer(), {
headers: { 'Content-Type': 'image/png', 'Cache-Control': 'public, max-age=86400' },
});
}
Then use it as your OG image URL:
<meta property="og:image" content="https://yoursite.com/api/og?title=My+Article+Title" />
Email Headers
Generate branded email header images:
html = """
<div style="
width: 600px; height: 200px;
background: linear-gradient(90deg, #2c3e50, #3498db);
display: flex; align-items: center; padding: 0 40px;
font-family: system-ui; color: white;
">
<div>
<h1 style="margin: 0; font-size: 28px;">Weekly Newsletter</h1>
<p style="margin: 8px 0 0; font-size: 16px; color: #bdc3c7;">Issue #42 · March 2026</p>
</div>
</div>
"""
resp = requests.post(
"https://hermesforge.dev/api/html2image",
json={"html": html, "width": 600, "height": 200, "format": "png"},
)
with open("email-header.png", "wb") as f:
f.write(resp.content)
API Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
html |
string | required | HTML content to render |
width |
int | 800 | Viewport width (max 1920) |
height |
int | 600 | Viewport height (max 1080) |
format |
string | png | Output: png, jpeg, webp |
scale |
int | 1 | Pixel ratio: 1, 2, or 3 |
background |
string | white | CSS background color |
quality |
int | 80 | JPEG/WebP quality (1-100) |
Rate Limits
| Tier | Limit | Cost |
|---|---|---|
| Anonymous | 5/min, 10/day | Free |
| API Key | 50/day | Free |
| Pro | 1000/day | $9 / 30 days |
| Ultra | 5000/day | $29 / 30 days |
Get a free API key:
curl -X POST "https://hermesforge.dev/api/keys" -H "Content-Type: application/json" -d '{}'