One template, every platform's exact size.
A social media card API that renders the same HTML at Twitter, LinkedIn, Facebook, and Instagram dimensions in one call. No cropping surprises, no Figma exports, no per-platform redesigns.
The problem
Each platform wants a different size. Twitter/X large image is 1200×675 (16:9). Facebook and LinkedIn Open Graph want 1200×630 (1.91:1). Instagram square posts are 1080×1080. LinkedIn's in-feed post image is 1200×627.
Design one card at 1200×630 in Figma and let networks crop it, and your headline gets clipped on at least one platform — usually the one you cared about most. The fix is mechanical: render the same content at each platform's exact size, with the layout re-flowing to fit.
Three approaches, one API
1. API loop per platform
For batch publishingLoop through a size table, POST to /v1/render per platform, save each PNG with a predictable filename. Run once per post, upload the set to your CDN.
2. CLI in your publishing pipeline
For static sitesDrop @codetoimage/cli into a webhook handler, regenerate the full social card set every time a post is updated. No manual exports.
3. Templated per-content (CMS-triggered)
For dynamic CMSCMS publish event fires a webhook, the webhook renders all sizes via output: "url", writes the hosted URLs back into the post record. Your share buttons pick the right URL per platform.
One template, six renders — Node.js example
Take one HTML template, one post payload, and produce a full social media preview image set in parallel. Each render goes to /v1/renderwith the platform's native width and height:
// scripts/render-social.ts
import { writeFile } from "node:fs/promises";
const SIZES = [
{ name: "twitter", width: 1200, height: 675 },
{ name: "facebook-og", width: 1200, height: 630 },
{ name: "linkedin-og", width: 1200, height: 630 },
{ name: "linkedin-post",width: 1200, height: 627 },
{ name: "instagram-sq", width: 1080, height: 1080 },
{ name: "instagram-pt", width: 1080, height: 1350 },
];
function template(post: { title: string; author: string; tag: string },
w: number, h: number) {
return `
<div style="
width: ${w}px; height: ${h}px;
display: flex; flex-direction: column; justify-content: space-between;
padding: ${Math.round(w * 0.06)}px;
background: linear-gradient(135deg, #0f172a 0%, #1e1b4b 100%);
color: white; font-family: 'Inter', sans-serif;
">
<div style="font-size: ${Math.round(w * 0.02)}px; opacity: 0.6;">
${post.tag}
</div>
<h1 style="font-size: ${Math.round(w * 0.06)}px; line-height: 1.1;
font-weight: 700;">
${post.title}
</h1>
<div style="font-size: ${Math.round(w * 0.018)}px;">
${post.author}
</div>
</div>`;
}
const post = { title: "Ship social cards from one template",
author: "@jakub", tag: "Engineering" };
await Promise.all(SIZES.map(async ({ name, width, height }) => {
const res = await fetch("https://api.codetoimage.app/v1/render", {
method: "POST",
headers: {
"X-API-Key": process.env.CODETOIMAGE_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify({
html: template(post, width, height),
width, height, format: "png",
}),
});
const buf = Buffer.from(await res.arrayBuffer());
await writeFile(`out/social-${post.title.slice(0,20)}-${name}.png`, buf);
}));Six renders fire in parallel, each at the platform's native resolution. Filenames follow social-[slug]-[platform].png, so your share buttons can pick the right asset deterministically.
Recommended sizes by platform
| Platform | Size | Ratio |
|---|---|---|
| Twitter / X — large image card | 1200×675 | 16:9 |
| Twitter / X — summary card | 800×418 | 1.91:1 |
| Facebook — Open Graph | 1200×630 | 1.91:1 |
| LinkedIn — Open Graph (shared link) | 1200×630 | 1.91:1 |
| LinkedIn — in-feed post image | 1200×627 | 1.91:1 |
| Instagram — square post | 1080×1080 | 1:1 |
| Instagram — portrait post | 1080×1350 | 4:5 |
| Pinterest — standard pin | 1000×1500 | 2:3 |
Reference current as of 2026. Platforms occasionally adjust display sizes — these dimensions stay safe because they meet or exceed documented minimums.
What you get
One template, per-platform sizing
Render the same HTML at six sizes in parallel. Layout re-flows from the width — padding and font sizes scale automatically.
Per-post personalization
Slug, title, author, cover hash → unique render. Every post gets a full social card set without you touching Figma.
Dark/light variants per audience
Pass a theme flag, branch the CSS. Ship one version for X (mostly dark UIs) and another for LinkedIn (mostly light) from the same template.
Localized for language audiences
Swap the HTML strings per locale, layout stays identical. One set of social media preview images per language, no design rework.
Auto-regen on content change
Webhook from your CMS hits /v1/render with output: "url". The hosted URL goes straight into the post record, your twitter:image meta tag picks it up.
Pixel-perfect typography
System fonts don't render the same on Figma vs your readers' browsers. Use real web fonts via @font-face in the HTML — what we render is what they see.
FAQ
Why not just use one 1200×630 image everywhere?▾
Because every platform crops differently. Twitter crops a 1200×630 image to 16:9, which slices off the top and bottom edges — any headline near those edges gets clipped. Instagram crops to square, which loses the sides. LinkedIn renders it at 1.91:1 but at a smaller display size, so dense text becomes unreadable. Rendering each platform at its native size keeps your content centered and readable.
Do platforms cache social card images?▾
Yes, aggressively. Twitter/X caches scraped images for up to 7 days. Facebook caches indefinitely — you have to run the URL through their Sharing Debugger to force a re-scrape. LinkedIn caches for around 7 days and exposes a Post Inspector tool. The reliable fix is to change the image URL itself: append a content hash query param (?v=abc123) or rotate the slug. The scraper sees a new URL, fetches fresh.
How do I update social cards after a post has already been published?▾
Change the image URL — append a content hash, version param, or rotate the path segment. Then submit the canonical post URL to each platform's debugger (Facebook Sharing Debugger, Twitter Card Validator, LinkedIn Post Inspector) to force a re-scrape. Without changing the URL, you'll wait days or weeks for the cache to expire on its own.
Can I generate the image dynamically per visitor or per share?▾
Yes via the runtime API with output: "url", but with a caveat. Social platforms don't pass referrer or user context to their scrapers — when Twitterbot or facebookexternalhit fetches your meta tags, it's a clean unauthenticated request. So the dynamic key has to live in the URL itself: /og/[post-slug]/[variant] or a query param. Then your route handler renders the right variant server-side and returns the PNG.
Stop exporting one image at a time.
Free Sandbox tier — 50 renders/month, no credit card. Generate a full Twitter, LinkedIn, Facebook, and Instagram set from one template in a single script.