Hero image for "Building an AI-Powered Image Pipeline for My Blog with Claude Code": Abstract digital assembly line with glowing nodes transforming text into vibrant images, streams of
6 min read

Building an AI-Powered Image Pipeline for My Blog with Claude Code

How I used Claude Code on the web to design and implement automatic AI-generated featured images for my Nuxt blog, complete with Cloudflare Pages integration.

How This Blog Generates Its Own Images

Every post on this blog has a hero image. None of them were made by hand. A GitHub Action generates them automatically whenever a new post is pushed, using AI for both the prompt and the image itself. Here is how it works.

The Problem

A blog without images looks flat. But manually creating featured images for every post is tedious, and stock photos feel generic. I wanted something that actually reflects the content of each post, without any manual effort.

Two Layers

The system has two layers, each serving a different purpose.

Layer 1: Programmatic OG Images. Using nuxt-og-image with Satori, every post gets a text-based social card automatically. These are the fallback. If anything goes wrong with the AI images, social sharing still looks good.

Layer 2: AI-Generated Hero Images. A TypeScript script generates a unique hero image for each post through two steps: first a text model writes an image prompt based on the post content, then an image model turns that prompt into an actual image.

Both steps run through OpenRouter, which provides access to many models through a single API. The current setup uses Gemini 2.5 Flash for prompt generation and Gemini 3.1 Flash Image for the actual image. Cost is roughly $0.07 per image.

How It Works

The Content Schema

The blog uses Nuxt Content with three image-related fields in the frontmatter:

schema: z.object({
  published: z.boolean(),
  created_at: z.date(),
  tags: z.array(z.string()).optional(),
  image: z.string().optional(),
  imageAlt: z.string().optional(),
  imagePrompt: z.string().optional(),
})

The imagePrompt field stores the prompt that was used to generate the image. This makes regeneration straightforward and keeps things transparent.

Prompt Generation

A system prompt defines the visual style for the blog: abstract, modern, deep blues and purples, no literal depictions of laptops or code screens. The text model receives the blog post content along with this system prompt and returns a short image prompt (under 200 characters).

const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${OPENROUTER_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    model: 'google/gemini-2.5-flash',
    messages: [
      { role: 'system', content: systemPrompt },
      { role: 'user', content: userPrompt },
    ],
    max_tokens: 200,
  }),
})

If a post already has an imagePrompt in its frontmatter, the script skips this step and reuses the existing prompt.

Image Generation

The image model receives the prompt and returns a base64-encoded image, which gets saved to public/images/blog/. The frontmatter is then updated with the image path, alt text, and the prompt that was used.

const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${OPENROUTER_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    model: 'google/gemini-3.1-flash-image-preview',
    modalities: ['image', 'text'],
    messages: [{
      role: 'user',
      content: [{ type: 'text', text: `Generate a 1200x630 blog hero image: ${prompt}` }],
    }],
  }),
})

The Style Guide

The quality of the images depends on the system prompt. Here is the gist of what works well:

  • Abstract geometric patterns, flowing light trails, data streams
  • Deep blues, purples, and teals with accent colors
  • Enough negative space for text overlay
  • No literal depictions, no stock photo cliches, no faces

Each image ends up feeling like it belongs to the same blog while still being distinct.

The Pipeline

Everything runs in a GitHub Action. No local tooling required.

name: Generate Blog Images

on:
  push:
    branches: [main]
    paths:
      - 'content/blog/**'
  workflow_dispatch: {}

jobs:
  generate-images:
    runs-on: ubuntu-latest
    if: github.actor != 'github-actions[bot]'
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
      - run: pnpm install --frozen-lockfile
      - name: Generate missing blog images
        env:
          OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
        run: npx tsx scripts/generate-blog-images.ts
      - name: Commit generated images
        run: |
          git add public/images/blog/ content/blog/
          git diff --cached --quiet || git commit -m "Generate blog hero images" && git push

The flow:

  1. Push a new blog post to main
  2. The action detects the change, generates the prompt and image
  3. It commits the image and updated frontmatter back to the repo
  4. Cloudflare Pages picks up the new commit and deploys

The github.actor check prevents the action from running on its own commits, avoiding an infinite loop. Posts that already have an image field are skipped, so each image is only generated once.

The Result

ScenarioWhat Happens
New post, no imagePromptText model generates a prompt, image model generates the image
New post, has imagePromptSkips prompt generation, generates the image from the existing prompt
Existing post with imageSkipped entirely
Need to regenerateRemove the image field from frontmatter, push

The whole pipeline runs in about 30 seconds per image. For a blog that publishes occasionally, the cost is negligible.

Takeaways

Store your prompts. Keeping the prompt in frontmatter means you can always see what generated an image, tweak it, and regenerate. It also means the image model step can run independently from the prompt generation step.

Separate prompt generation from image generation. Text models are good at understanding content and writing prompts. Image models are good at generating visuals. Letting each do what it is best at produces better results than trying to do everything in one step.

Always have a fallback. The site works fine without hero images. The programmatic OG images ensure social sharing always looks professional, even if the AI pipeline has a bad day.

Keep it boring in CI. The script checks for an existing image field before doing anything. No unnecessary API calls, no surprise costs. Write a post, push, and forget about it.