You write a JSX template once. Every blog post, doc page, and landing page gets its own social preview. One API call per page.
New on the blog: How we scaled to 1M users without adding a single server.
Links without OG images get scrolled past. But making a unique image for every page means either a designer doing it by hand, or running Puppeteer on a server.
If you've tried Puppeteer, you know the deal: memory leaks, Chrome version mismatches, cold starts, zombie processes. It works until it doesn't, and then you're debugging headless Chrome at 2am.
Create a JSX template with variables for title, author, date, whatever you need. Upload it to HTMLPix. When you want an image, POST your variables and get back a signed URL.
The image renders on the first request and caches from there. The URL is valid for 5 years. You don't manage a server, update Chrome, or think about rendering.
1# Step 1: Mint a signed image URL2curl -X POST https://api.htmlpix.com/v1/url \3 -H "Authorization: Bearer $HTMLPIX_KEY" \4 -H "Content-Type: application/json" \5 -d '{6 "templateId": "blog-post-template-id",7 "variables": {8 "title": "How We Scaled to 1M Users",9 "author": "Jane Smith",10 "date": "Mar 15, 2026",11 "category": "Engineering"12 },13 "width": 1200,14 "height": 63015 }'1617# Response:18# { "url": "https://image.htmlpix.com/v1/image?templateId=...&sig=...", "expiresAt": ... }19#20# Step 2: Use the URL directly in your <meta> tags21# <meta property="og:image" content="https://image.htmlpix.com/v1/image?..." />import type { Metadata } from "next";
async function mintOgUrl(variables: Record<string, string>) {
const res = await fetch("https://api.htmlpix.com/v1/url", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.HTMLPIX_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
templateId: process.env.BLOG_OG_TEMPLATE_ID,
variables,
width: 1200,
height: 630,
}),
});
const { url } = await res.json();
return url;
}
export async function generateMetadata({ params }): Promise<Metadata> {
const post = await getPost(params.slug);
const ogUrl = await mintOgUrl({
title: post.title,
author: post.author,
date: post.date,
category: post.category,
});
return {
openGraph: { images: [{ url: ogUrl, width: 1200, height: 630 }] },
twitter: { card: "summary_large_image", images: [ogUrl] },
};
}Set up Puppeteer or Playwright. Maintain a server. Debug Chrome memory leaks. Handle cold starts. Budget $50-200/mo for infra alone.
POST your variables, get a URL. First render takes 200-500ms, then it's cached. Starts at $8/month for 500 images.
The same template generates thousands of different images. Change the title, author, or date per page. The template does the layout.
You're not hosting Puppeteer. You POST to an API and get a URL back. That's the whole thing.
First render takes 200-500ms. Every request after that is served from edge cache. You pay to mint the URL, not to serve it.
Next.js, Astro, Rails, Django, whatever. It's an HTTP POST. Put the URL in your og:image meta tag.
Personalized stats, dashboards, and banners rendered as images. Works in every email client.
Badges, year-in-review cards, and certificates your users can post on social media.
Post your key metrics to Slack or email as a clean dashboard image. Runs on a cron job.
One template, a spreadsheet of copy, and you have all your ad variants in minutes.
Just migrated our OG images to @htmlpix. Should have done this months ago.
Show customer quotes on your site as styled cards. No embed scripts, no API keys.
Branded product cards generated from your database. For social, email, and marketplaces.