Generate Open Graph images with one API call, no Next.js required
Your blog post gets shared on Twitter. Instead of a rich preview with a title card, the link shows a blank gray box. Or worse, a stretched favicon. Every share without a proper OG image is a missed click.
The popular solution is Vercel's @vercel/og library. It works well, but it's locked to
Next.js and Vercel's infrastructure. Satori (the engine behind it) needs a Node.js runtime with specific
font-loading capabilities. If you're building with Astro, Django, Rails, Laravel, Hugo, or any other
framework, you're on your own.
There's a simpler approach: send one POST request to an API and get back a finished 1200x630 PNG. No Node.js dependency. No framework lock-in. No image rendering pipeline to maintain.
One POST request, one PNG back
The botoi OG image API accepts a JSON body with your title, description, and theme preference. It returns raw PNG binary data.
curl -X POST https://api.botoi.com/v1/og/generate \
-H "Content-Type: application/json" \
-d '{
"title": "How to build a REST API in Go",
"description": "A step-by-step guide with net/http and no frameworks",
"theme": "dark"
}' \
--output og-image.png That's it. The response is a PNG file you can save, serve, or upload to a CDN:
HTTP/1.1 200 OK
Content-Type: image/png
Content-Length: 48271
(binary PNG data, 1200x630px) The API handles typography, layout, and proper sizing. You pass text in, you get an image out.
Astro integration: generate OG images at build time
Astro's static site generation makes this especially clean. Create a dynamic API route that fetches
OG images during the build and serves them as static .png files.
// src/pages/og/[slug].png.ts
import type { APIRoute, GetStaticPaths } from 'astro';
// Your posts data source (markdown, CMS, database, etc.)
import { getAllPosts } from '@/lib/posts';
export const getStaticPaths: GetStaticPaths = async () => {
const posts = await getAllPosts();
return posts.map((post) => ({
params: { slug: post.slug },
props: { title: post.title, description: post.description },
}));
};
export const GET: APIRoute = async ({ props }) => {
const res = await fetch('https://api.botoi.com/v1/og/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title: props.title,
description: props.description,
theme: 'dark',
}),
});
const imageBuffer = await res.arrayBuffer();
return new Response(imageBuffer, {
headers: {
'Content-Type': 'image/png',
'Cache-Control': 'public, max-age=31536000, immutable',
},
});
};
During astro build, this route generates a PNG for every post. The output files land in
your dist/og/ directory as static assets. No runtime image generation, no serverless
function, no cold starts.
Reference the images in your page head:
<!-- In your BaseLayout.astro or page head -->
<meta property="og:image" content={`https://yoursite.com/og/${slug}.png`} />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:image" content={`https://yoursite.com/og/${slug}.png`} /> Every page gets a unique social card, generated once during the build and cached forever by CDNs.
Django and Rails integration
Server-rendered frameworks can generate OG images on demand and cache the result.
Django
# views.py
import requests
from django.http import HttpResponse
from django.views.decorators.cache import cache_page
@cache_page(60 * 60 * 24) # Cache for 24 hours
def og_image(request, slug):
post = get_object_or_404(Post, slug=slug)
response = requests.post(
'https://api.botoi.com/v1/og/generate',
json={
'title': post.title,
'description': post.description,
'theme': 'dark',
},
timeout=5,
)
return HttpResponse(
response.content,
content_type='image/png',
) # urls.py
urlpatterns = [
path('og/<slug:slug>.png', views.og_image, name='og_image'),
]
The @cache_page decorator stores the generated image for 24 hours. After the first
request, Django serves the cached PNG directly without hitting the API again.
Rails
# app/controllers/og_images_controller.rb
class OgImagesController < ApplicationController
def show
post = Post.find_by!(slug: params[:slug])
response = HTTP.post(
'https://api.botoi.com/v1/og/generate',
json: {
title: post.title,
description: post.description,
theme: 'dark'
}
)
expires_in 24.hours, public: true
send_data response.body.to_s, type: 'image/png', disposition: 'inline'
end
end # config/routes.rb
get 'og/:slug.png', to: 'og_images#show' Both examples follow the same pattern: look up the post by slug, POST the title and description to the API, and return the PNG with caching headers. The framework handles caching; the API handles image generation.
Headless CMS integration: generate on publish
If your content lives in a headless CMS like Strapi, Contentful, or Sanity, you can generate OG images whenever an editor publishes or updates a post. Wire up a webhook that fires on content changes.
Generic webhook handler
// Webhook handler (any Node.js server or serverless function)
import { writeFileSync } from 'fs';
export async function handleCmsPublish(payload) {
const { title, description, slug } = payload.entry;
const res = await fetch('https://api.botoi.com/v1/og/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, description, theme: 'light' }),
});
const buffer = Buffer.from(await res.arrayBuffer());
// Save to your public/static directory or upload to S3/R2
writeFileSync(`./public/og/${slug}.png`, buffer);
console.log(`Generated OG image for: ${slug}`);
} Strapi lifecycle hook
// Strapi lifecycle hook: src/api/post/content-types/post/lifecycles.js
module.exports = {
async afterUpdate(event) {
const { result } = event;
const res = await fetch('https://api.botoi.com/v1/og/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title: result.title,
description: result.description,
theme: 'dark',
}),
});
// Upload to your CDN or save locally
const buffer = Buffer.from(await res.arrayBuffer());
await strapi.plugins.upload.services.upload.upload({
data: { path: `og/${result.slug}.png` },
files: { buffer, name: `${result.slug}.png`, type: 'image/png' },
});
},
}; This approach is useful for teams where content editors publish through a CMS and developers don't want to run a build pipeline for every text change. The OG image regenerates automatically when the post title or description changes.
Bonus: extract OG metadata from any URL
The botoi API also provides /v1/url-metadata, which does the reverse: given a URL, it fetches
the page and extracts its Open Graph tags. This is useful for building link previews, social card
validators, or SEO audit tools.
curl -X POST https://api.botoi.com/v1/url-metadata \
-H "Content-Type: application/json" \
-d '{"url": "https://github.com/astro-community/astro-embed"}' Response:
{
"success": true,
"data": {
"url": "https://github.com/astro-community/astro-embed",
"status": 200,
"title": "GitHub - astro-community/astro-embed",
"description": "Components to embed third-party media in Astro projects",
"og": {
"title": "astro-embed",
"description": "Components to embed third-party media in Astro projects",
"image": "https://opengraph.githubassets.com/...",
"type": "object",
"url": "https://github.com/astro-community/astro-embed",
"site_name": "GitHub"
}
}
} Use this to build link preview components in your app:
async function getLinkPreview(url: string) {
const res = await fetch('https://api.botoi.com/v1/url-metadata', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url }),
});
const { data } = await res.json();
return {
title: data.og?.title ?? data.title,
description: data.og?.description ?? data.description,
image: data.og?.image,
siteName: data.og?.site_name,
};
}
Combine both endpoints: use /v1/url-metadata to check how your pages appear on social
platforms, and /v1/og/generate to create the images that make them look good.
Why not run Satori yourself?
You can. Satori is open source and produces quality output. But running it yourself means:
- Installing and bundling custom fonts (Satori doesn't use system fonts)
- Setting up a Node.js runtime or Edge Function for image rendering
- Writing JSX-to-SVG templates and converting SVG to PNG with resvg
- Handling memory limits on serverless platforms when rendering large images
- Maintaining the pipeline as Satori's API evolves
An API call replaces all of that with a single HTTP request. If your OG image needs are straightforward (title + description + branding), the API approach saves hours of setup and ongoing maintenance.
Frequently asked questions
- What size are the generated OG images?
- Every image is 1200x630 pixels, the standard Open Graph image size recommended by Facebook, Twitter/X, LinkedIn, and Slack. The output is a PNG file.
- Do I need an API key?
- No. The free tier allows 5 requests per minute with no API key. For build pipelines or high-traffic sites that generate images on every page view, grab a key from the botoi API docs page to unlock higher rate limits.
- Can I customize fonts, colors, or layout?
- The theme parameter accepts "light" or "dark" to control the overall color scheme. The API handles typography and layout automatically based on your title and description length. For full custom designs, generate your base image with the API and post-process it with your own overlay logic.
- How fast is the image generation?
- The API runs on Cloudflare Workers at the edge. Most requests return a finished PNG in under 500ms. If you cache the result (recommended for static sites), the generation only happens once per unique combination of title, description, and theme.
- What does the /v1/url-metadata endpoint do?
- POST a URL to /v1/url-metadata and the API fetches that page, parses its HTML, and returns structured Open Graph metadata: og:title, og:description, og:image, og:type, and Twitter Card tags. Use it to preview how any URL appears when shared on social media, or to pull metadata into your own link previews.
Try this API
OG Image Generator API — interactive playground and code examples
More tutorial posts
Start building with botoi
150+ API endpoints for lookup, text processing, image generation, and developer utilities. Free tier, no credit card.