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

Table of Contents(11 sections)
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.
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.
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.
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:
- Create a Next.js project with an API route for image generation
- Build a static HTML template with inline styles
- Add Puppeteer as a dependency
- Test with your actual post titles and categories
- 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 BlogYou Might Also Like
- How to Build a Custom Playwright Reporter for Next.js Dashboards
- Next.js Middleware: Building Robust Authentication and Authorization
- TypeScript Alone Isn''t Enough: End-to-End Type Safety with Zod in Next.js
- Configuring Vite with ESLint and Prettier for React Projects
Test Your Knowledge
Free In-Browser Developer Tools
Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.
Related Articles

Mastering Next.js 14+ Metadata & Open Graph: Dynamic Social Cards at Scale
Turn social media shares into massive organic traffic drivers. Master Next.js generateMetadata, Open Graph tags, Twitter Cards, and dynamic Edge OG image generation.
Read more
How I Built a TIL Microblog to Save the Things I'd Otherwise Forget
I built a 'Today I Learned' section on my site to capture quick discoveries without the pressure of writing full blog posts. This is how I set up separate RSS feeds, automated post creation, and made it easy enough to stick with.
Read more
Handling Forms in Next.js 14: Server Actions vs Client Components
A deep dive into managing forms in Next.js 14 using Server Actions, useFormState, useFormStatus, and Zod for schema validation. Learn to build modern, progressive-enhanced forms without client-side JavaScript fatigue.
Read more