Outlook strips your CSS. Gmail ignores your media queries. But every email client renders images. So render your content as an image.
Hey Jordan, here's your weekly progress:
Keep it up! You're on a 12-day streak.
You want personalized stats or a progress bar in your email. Outlook doesn't support flexbox. Gmail strips your media queries. Yahoo wraps your columns. What looked right in your editor is broken in every inbox.
The standard fix is nested tables with inline styles. It barely works, it's painful to maintain, and you still can't do real visual personalization. Just "Hi {name}" and hope for the best.
Build your personalized content as a JSX template. Pass each recipient's data as variables. Mint a signed URL per user with the batch endpoint (up to 25 per request).
Drop the URL in an <img> tag. The image renders on first open, caches after that, and looks identical in every email client. No tables, no CSS hacks, no cross-client testing.
1// Mint personalized image URLs for each recipient2const recipients = [3 { name: "Jordan", steps: "2,847", streak: "12", goalRate: "89%" },4 { name: "Alex", steps: "4,102", streak: "28", goalRate: "94%" },5];67const urls = await fetch("https://api.htmlpix.com/v1/urls", {8 method: "POST",9 headers: {10 Authorization: `Bearer ${process.env.HTMLPIX_KEY}`,11 "Content-Type": "application/json",12 },13 body: JSON.stringify({14 items: recipients.map((r) => ({15 templateId: EMAIL_STATS_TEMPLATE_ID,16 variables: {17 userName: r.name,18 stat1Value: r.steps,19 stat1Label: "Steps Today",20 stat2Value: r.streak,21 stat2Label: "Day Streak",22 stat3Value: r.goalRate,23 stat3Label: "Goal Hit Rate",24 },25 width: 600,26 height: 300,27 })),28 }),29});3031// Each URL is unique per recipient32// Embed: <img src="https://image.htmlpix.com/v1/image?..." />interface Recipient {
email: string;
name: string;
stats: { steps: string; streak: string; goalRate: string };
}
export async function mintEmailImages(recipients: Recipient[]) {
const res = await fetch("https://api.htmlpix.com/v1/urls", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.HTMLPIX_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
items: recipients.map((r) => ({
templateId: process.env.EMAIL_STATS_TEMPLATE_ID,
variables: {
userName: r.name,
stat1Value: r.stats.steps,
stat1Label: "Steps Today",
stat2Value: r.stats.streak,
stat2Label: "Day Streak",
stat3Value: r.stats.goalRate,
stat3Label: "Goal Hit Rate",
},
width: 600,
height: 300,
})),
}),
});
const { urls } = await res.json();
return recipients.map((r, i) => ({
email: r.email,
imageUrl: urls[i].url,
}));
}Nested tables with inline CSS. Hours of cross-client testing per campaign. Visual personalization basically impossible.
Build template once, mint a URL per recipient. Images look the same everywhere. Starts at $8/month.
An image tag works the same in Outlook 2016, Gmail on Android, Apple Mail, and Yahoo. No rendering differences.
Each recipient gets their own stats, progress bar, or dashboard. Not a merge tag with their name. A unique image with their data.
POST /v1/urls takes up to 25 items per request. Mint a unique image URL for each recipient in your send list.
Some clients block images by default. Keep important text outside the image and use the image for the visual content that CSS can't handle.
A unique social preview for every page on your site. One template, one API call.
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.