DEV Community

Custodia-Admin
Custodia-Admin

Posted on

Automate Open Graph images on every blog post

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

  1. Create an OG image template (HTML)
  2. Hook into your build pipeline (Next.js, Astro, Hugo, static site)
  3. For each blog post, POST the template + post metadata to PageBolt's /og-image endpoint
  4. Save the PNG to your public/ directory
  5. 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>
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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');
  },
};
Enter fullscreen mode Exit fullscreen mode

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>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)