•8 min read

How I Generate Hero and OG Images for My Technical Blog Without Losing My Mind

How I Generate Hero and OG Images for My Technical Blog Without Losing My Mind

Every blog post I wrote started the same way: I'd finish the article, then spend 30 minutes in Figma creating a hero image and OG image. The article was done, but these "minor" tasks kept piling up.

Sound familiar?

The breaking point came when I needed to update my blog's branding. I changed one color and suddenly had 40+ images to recreate. That's when I decided to automate this entire workflow.

Let me show you how I generate consistent, professional hero and OG images automatically.

Audio Briefing
0:00 / 0:00

The Problem With Manual Image Creation

Manually creating images for every post means you're constantlycontext-switching between writing and design. My Figma tabs multiplied. My blog consistency suffered. Different posts had slightly different fonts, colors, and layouts because I was always "just tweaking" things.

Worse, I kept postponing image creation. My draft folder grew. I'd published articles without images just because I didn't want to open Figma again.

Did I mention OG images need to work at 1200x630 pixels? That's a specific aspect ratio that's hard to eyeball.

Advertisement

My Automated Solution

I built a system using Next.js API routes, Puppeteer, and a template image. Now when I need an image, I call a URL with my post title and other parameters. The server renders a full HTML page, captures it as an image, and returns the PNG.

No Figma. No manual work. Just function calls.

Here's the core endpoint I created:

// app/api/og/route.jsx
import puppeteer from 'puppeteer';

export async function GET(request) {
  const { searchParams } = new URL(request.url);
  const title = searchParams.get('title') || 'Untitled Post';
  const category = searchParams.get('category') || 'Tech';

  const browser = await puppeteer.launch({ headless: true });
  const page = await browser.newPage();

  await page.setViewport({ width: 1200, height: 630 });
  await page.setContent(renderTemplate({ title, category }));

  const imageBuffer = await page.screenshot({ type: 'png' });
  await browser.close();

  return new Response(imageBuffer, {
    headers: { 'Content-Type': 'image/png' },
  });
}

The renderTemplate function builds the HTML. I use CSS Grid and Flexbox for layout. The styling matches my blog's design system exactly.

Puppeteer launches a real Chrome browser on the server. Make sure your hosting environment supports this. Vercel, Railway, and Render all work fine with the right configuration.

Building the HTML Template

My template is just HTML and CSS styled to look like a designed card. Here's the simplified structure:

<div class="og-card">
  <div class="category-badge">{category}</div>
  <h1 class="title">{title}</h1>
  <div class="footer">
    <span class="date">{formattedDate}</span>
    <span class="site">mysite.com</span>
  </div>
</div>

The CSS is where the magic happens. I use Google Fonts embedded as base64 or loaded via @import. I stick to one or two fonts maximum. The more your template needs to load remotely, the more flaky your image generation becomes.

.og-card {
  background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
  color: #ffffff;
  padding: 60px;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  height: 100%;
  box-sizing: border-box;
}

I learned through painful trial that gradients render differently across systems. Now I always test my templates by generating 10 images and checking them on different devices.

Making It Deterministic

Here's something nobody tells you: Puppeteer screenshots can vary slightly based on rendering timing. I added explicit wait conditions to ensure consistency:

await page.setContent(renderTemplate({ title, category }));
await page.evaluateHandle('document.fonts.ready'); // Wait for fonts
await page.waitForTimeout(500); // Buffer for rendering
const imageBuffer = await page.screenshot({ type: 'png' });

The document.fonts.ready promise ensures web fonts have loaded before capture. That 500ms buffer handles transition animations if your template uses them.

Advertisement

Generating Hero Images

OG images are straightforward because they're a fixed 1200x630 pixels. Hero images are trickier because my blog displays them at various sizes. I use the same system but call it with different dimensions.

My approach: generate two or three sizes at build time and let the browser pick the right one via srcset.

Are multiple sizes worth the extra build time? For me, yes. Readers on mobile get appropriately sized images, and my Lighthouse scores stayed high.

const sizes = [
  { width: 800, height: 450, name: 'small' },
  { width: 1600, height: 900, name: 'large' },
];

for (const size of sizes) {
  await page.setViewport({ width: size.width, height: size.height });
  // capture and save
}

Caching Strategy

Generating images on every request is slow. I added a simple caching layer using my deployment platform's edge cache. The image URL includes a hash of the content, so identical requests hit the cache.

# Example: cached image URL
https://api.mysite.com/og?title=My%20Post&category=Tech&hash=abc123

When I update my template's design, I invalidate the cache by changing a version parameter. Old images serve until they expire, new images generate with the fresh design.

The Results

After implementing this system, I haven't opened Figma for a blog image in six months. My posting frequency doubled. Every image matches my brand exactly because they're all generated from the same template.

My build time increased by 8 seconds per post. That's acceptable given the time saved.

Are there downsides? Sure. If Puppeteer has a bug or my template has a CSS error, all affected images break simultaneously. I mitigate this by testing templates thoroughly before deploying updates.

Getting Started

Want to build your own system? Here's the minimum viable setup:

  1. Create a Next.js project with an API route for image generation
  2. Build a static HTML template with inline styles
  3. Add Puppeteer as a dependency
  4. Test with your actual post titles and categories
  5. Cache aggressively

The template is the most important part. Spend time making it look right because you'll be using it for every single post.

Wrapping Up

I avoided automating images for years because I thought it would be complex. It was. But the payoff justified the initial investment. Now I write the article, add frontmatter, and the images generate themselves.

If you're spending more than 10 minutes per post on images, you should automate this. Your future self will thank you.

What workflow tasks are you still doing manually? Are any of them candidates for automation?

Puppeteer Next.js Automation Blog

You Might Also Like

Test Your Knowledge

Share this article:

Stay Updated

Get the latest posts delivered straight to your inbox.

Free Developer Utilities

Free In-Browser Developer Tools

Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.

Explore Tools
Advertisement