How to Automate OG Image Generation for Every Blog Post
You hand-craft social images for every blog post. Title, author, date, custom background. Hours spent in Figma. Then you update the post, forget to update the image, and Twitter shows the old version.
Automate it: generate a unique OG image for each post using the PageBolt API. One API call, one PNG, done.
The pattern
- Create an OG image template (HTML)
- Hook into your build pipeline (Next.js, Astro, Hugo, static site)
- For each blog post, POST the template + post metadata to PageBolt's
/og-imageendpoint - Save the PNG to your
public/directory - Reference it in your blog post metadata
All OG images are generated fresh from the same template. Consistency. No design work.
Next.js example
Step 1: Create your OG image template
<!-- public/og-template.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
* { margin: 0; padding: 0; }
body {
width: 1200px;
height: 630px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: white;
padding: 60px;
box-sizing: border-box;
}
h1 {
font-size: 60px;
font-weight: bold;
text-align: center;
margin-bottom: 20px;
line-height: 1.2;
}
.meta {
font-size: 24px;
opacity: 0.9;
text-align: center;
}
.author { font-weight: 600; }
.date { opacity: 0.8; }
</style>
</head>
<body>
<h1>{{TITLE}}</h1>
<div class="meta">
<span class="author">{{AUTHOR}}</span>
<span class="date"> • {{DATE}}</span>
</div>
</body>
</html>
Step 2: Build script that generates images
// scripts/generate-og-images.js
const fs = require('fs');
const path = require('path');
const fetch = require('node-fetch');
const PAGEBOLT_API_KEY = process.env.PAGEBOLT_API_KEY;
const template = fs.readFileSync('public/og-template.html', 'utf8');
async function generateOGImage(post) {
// Replace placeholders
const html = template
.replace('{{TITLE}}', post.title)
.replace('{{AUTHOR}}', post.author || 'PageBolt')
.replace('{{DATE}}', new Date(post.date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
}));
const res = await fetch('https://pagebolt.dev/api/v1/screenshot', {
method: 'POST',
headers: {
'x-api-key': PAGEBOLT_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
html: html,
width: 1200,
height: 630,
format: 'png',
}),
timeout: 30000,
});
if (!res.ok) {
throw new Error(`Failed to generate OG image: ${res.status}`);
}
const buffer = await res.buffer();
const filename = `og-${post.slug}.png`;
fs.writeFileSync(path.join('public/og', filename), buffer);
console.log(`✓ Generated ${filename}`);
return filename;
}
async function main() {
// Create directory if needed
if (!fs.existsSync('public/og')) {
fs.mkdirSync('public/og', { recursive: true });
}
// Get all blog posts (adjust path based on your structure)
const postsDir = 'content/blog';
const files = fs.readdirSync(postsDir).filter(f => f.endsWith('.mdx') || f.endsWith('.md'));
for (const file of files) {
const content = fs.readFileSync(path.join(postsDir, file), 'utf8');
// Extract frontmatter (simplified — use gray-matter in production)
const match = content.match(/^---\n([\s\S]*?)\n---/);
if (!match) continue;
const frontmatter = match[1];
const post = {
title: frontmatter.match(/title:\s*"([^"]+)"/)?.[1],
author: frontmatter.match(/author:\s*"([^"]+)"/)?.[1],
date: frontmatter.match(/date:\s*"([^"]+)"/)?.[1],
slug: file.replace(/\.(md|mdx)$/, ''),
};
if (!post.title) continue;
await generateOGImage(post);
}
}
main();
Step 3: Add to build pipeline
In package.json:
{
"scripts": {
"build": "npm run generate-og-images && next build",
"generate-og-images": "node scripts/generate-og-images.js"
}
}
Or in next.config.js:
const { execSync } = require('child_process');
module.exports = {
onBuildComplete: async () => {
console.log('Generating OG images...');
execSync('node scripts/generate-og-images.js');
},
};
Step 4: Reference in your blog layout
// pages/blog/[slug].jsx
import Head from 'next/head';
export default function BlogPost({ post }) {
const ogImageUrl = `https://yoursite.com/og/og-${post.slug}.png`;
return (
<>
<Head>
<meta property="og:image" content={ogImageUrl} />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta name="twitter:image" content={ogImageUrl} />
</Head>
<article>{post.content}</article>
</>
);
}
GitHub Actions alternative
If you want OG images generated on deploy instead of locally:
name: Generate OG Images
on:
push:
branches: [main]
paths:
- 'content/blog/**'
jobs:
og-images:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Generate OG images
env:
PAGEBOLT_API_KEY: ${{ secrets.PAGEBOLT_API_KEY }}
run: npm run generate-og-images
- name: Commit and push
run: |
git config user.name "OG Image Bot"
git config user.email "bot@example.com"
git add public/og/
git commit -m "chore: regenerate OG images" || exit 0
git push
Why this works
- Consistent branding — all images use the same template
- Always fresh — update post metadata, OG image updates automatically
- No design tool needed — HTML + CSS is faster than Figma
- Scales — 100 blog posts, 100 images, no extra work
- Cheap — $29/mo PageBolt plan covers 5,000+ images
Try it free — 100 requests/month, no credit card. → Get started in 2 minutes
Top comments (0)