DEV Community

Cover image for How to Auto-Generate Instagram Images with HTML and a Render API
Özgür S.
Özgür S.

Posted on

How to Auto-Generate Instagram Images with HTML and a Render API

You've got a content pipeline. Posts go out every day. Someone — maybe you — is manually opening Canva, editing a template, exporting a PNG, uploading it.

There's a better way.

In this tutorial I'll show you how to build a fully automated image generation pipeline using HTML + a rendering API. The same approach I use to generate hundreds of social images programmatically.

What we're building

A scheduled script (or n8n/Make.com workflow) that:

  1. Takes dynamic content (title, subtitle, colors)
  2. Injects it into an HTML template
  3. Sends it to a rendering API
  4. Gets back a pixel-perfect PNG — ready to upload or post

No Puppeteer. No headless Chrome to maintain. No design tools.

Why HTML for image generation?

HTML + CSS is actually the most powerful "design tool" most developers already know. You get:

  • Full layout control with flexbox/grid
  • Any Google Font
  • Tailwind CSS (via CDN)
  • Dynamic data injection with template literals
  • Pixel-perfect consistency every time

The missing piece is rendering that HTML server-side into an image. That's what a render API does.

The API call

We'll use RenderPix — a server-side Chromium rendering API. Free tier includes 100 renders/month, no credit card needed.

curl -X POST https://renderpix.dev/v1/render \
  -H "X-API-Key: your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "html": "<div style=\"background:#1a1a2e;color:white;width:1080px;height:1350px;display:flex;align-items:center;justify-content:center;font-size:72px;font-family:sans-serif\">Hello</div>",
    "width": 1080,
    "height": 1350,
    "format": "png"
  }' \
  --output image.png
Enter fullscreen mode Exit fullscreen mode

Returns a binary PNG. That's it.

Building the HTML template

Here's a production-ready Instagram portrait template (1080×1350):

function buildPostHTML({ title, subtitle, bgColor = '#0f172a', accentColor = '#22d3ee' }) {
  return `<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700;900&display=swap" rel="stylesheet">
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body {
      width: 1080px;
      height: 1350px;
      background: ${bgColor};
      font-family: 'Inter', sans-serif;
      color: white;
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
      padding: 100px;
      position: relative;
    }
    .accent-bar {
      width: 80px;
      height: 6px;
      background: ${accentColor};
      border-radius: 3px;
      margin-bottom: 48px;
    }
    h1 {
      font-size: 80px;
      font-weight: 900;
      line-height: 1.05;
      text-align: center;
      margin-bottom: 36px;
      letter-spacing: -0.02em;
    }
    p {
      font-size: 36px;
      font-weight: 400;
      text-align: center;
      color: rgba(255,255,255,0.65);
      line-height: 1.5;
      max-width: 800px;
    }
    .brand {
      position: absolute;
      bottom: 70px;
      font-size: 28px;
      font-weight: 700;
      color: ${accentColor};
      letter-spacing: 0.05em;
    }
  </style>
</head>
<body>
  <div class="accent-bar"></div>
  <h1>${title}</h1>
  <p>${subtitle}</p>
  <span class="brand">@yourbrand</span>
</body>
</html>`;
}
Enter fullscreen mode Exit fullscreen mode

The render function

async function renderToImage(html, width = 1080, height = 1350) {
  const response = await fetch('https://renderpix.dev/v1/render', {
    method: 'POST',
    headers: {
      'X-API-Key': process.env.RENDERPIX_KEY,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ html, width, height, format: 'png' }),
  });

  if (!response.ok) {
    throw new Error(`Render failed: ${response.status}`);
  }

  return Buffer.from(await response.arrayBuffer());
}
Enter fullscreen mode Exit fullscreen mode

Putting it together — scheduled pipeline

import { writeFile } from 'fs/promises';

const posts = [
  {
    title: "Ship fast.\nLearn faster.",
    subtitle: "The only way to build great products is to talk to real users.",
    bgColor: "#0f172a",
    accentColor: "#22d3ee"
  },
  {
    title: "Done is better\nthan perfect.",
    subtitle: "Launch early. Iterate often. That's the whole playbook.",
    bgColor: "#1a0a2e",
    accentColor: "#a78bfa"
  }
];

async function generateAll() {
  for (const [i, post] of posts.entries()) {
    const html = buildPostHTML(post);
    const buffer = await renderToImage(html);
    await writeFile(`post-${i + 1}.png`, buffer);
    console.log(`Generated post-${i + 1}.png`);
  }
}

generateAll();
Enter fullscreen mode Exit fullscreen mode

Run this with a cron job, GitHub Actions, or any scheduler.

Common social media sizes

Platform Width Height Notes
Instagram Portrait 1080 1350 Best for feed reach
Instagram Square 1080 1080 Classic format
Instagram Story 1080 1920 Full screen
Twitter/X Card 1280 720 Link preview
LinkedIn Post 1200 627 Feed image

To generate multiple sizes in parallel:

const sizes = [
  { width: 1080, height: 1350, name: 'instagram-portrait' },
  { width: 1080, height: 1080, name: 'instagram-square' },
  { width: 1280, height: 720,  name: 'twitter' },
];

const buffers = await Promise.all(
  sizes.map(({ width, height }) => renderToImage(html, width, height))
);
Enter fullscreen mode Exit fullscreen mode

Using Tailwind CSS

Works out of the box — just add the CDN script to your HTML head:

<head>
  <script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="w-[1080px] h-[1350px] bg-slate-900 flex flex-col items-center justify-center p-24">
  <h1 class="text-8xl font-black text-white text-center leading-tight">
    Your title here
  </h1>
</body>
Enter fullscreen mode Exit fullscreen mode

RenderPix uses server-side Chromium so the CDN script loads and executes before the screenshot is taken.

n8n workflow (no-code)

If you prefer no-code, here's the n8n setup:

Nodes: Schedule Trigger → Code → HTTP Request → Write Binary File

Code node — builds the HTML (same template as above, using $input.first().json for data)

HTTP Request node:

  • Method: POST
  • URL: https://renderpix.dev/v1/render
  • Authentication: Header Auth → X-API-Key: your_key
  • Body: JSON with html, width, height, format
  • Response format: File (binary)

Make.com scenario (no-code)

Modules: Schedule → Google Sheets: Get Row → HTTP: Make a Request → Google Drive: Upload

HTTP module config:

  • URL: https://renderpix.dev/v1/render
  • Method: POST
  • Headers: X-API-Key: your_key
  • Body type: Raw / application/json
  • Response type: Binary

Why not Puppeteer?

You could self-host Puppeteer or Playwright. But:

  • Chromium binary is ~300MB
  • Cold starts are slow (1-3s)
  • Memory crashes on small servers
  • You maintain it when it breaks

For a content pipeline that runs on schedule, a managed render API is simpler and more reliable. The free tier covers most personal projects. Paid plans start at $9/month for 2,000 renders.

Wrapping up

The full pattern:

  1. Build HTML with dynamic data injected via template literals
  2. POST to render API → get PNG buffer
  3. Save to disk, upload to S3, or pass to your posting tool

Works with any scheduler — cron, GitHub Actions, n8n, Make.com, Zapier.

If you build something with this, I'd love to see it. Drop a comment or find me at @renderpixdev.


RenderPix is what I built to solve this exact problem. Free tier available at renderpix.dev.


Top comments (0)