Blog Image Generator
[To Build] ·
agent__blog-image-generator· Azure OpenAI GPT Image (DALL-E 3)
Generates brand-matched hero and inline section images for blog posts. Uses the post title, primary keyword, tenant brand colours, and chosen image style to produce images, then resizes and uploads them to DigitalOcean Spaces, embedding the URLs directly into the blog post Markdown.
Related: Blog Image Generation · Blog Writer · Social Post Designer
Overview
| Function | Generate and embed hero + inline images for blog posts |
| Type | Worker — Content |
| Model | Azure OpenAI GPT Image (DALL-E 3) |
| Queue | agent__blog-image-generator |
| Concurrency | 2 |
| Timeout | 5 min |
| Est. cost / task | ~$0.08 hero · ~$0.04 inline (Azure standard quality) |
| Credits | 0.5 cr hero image · 0.25 cr per inline image |
| Plan | Pro+ |
Triggers
| Trigger type | When | Who initiates |
|---|---|---|
| Manual | DM clicks “Generate Images” on blog detail page | Tenant admin / DM reviewer |
| Automatic | BlogPost.status transitions to published and tenant has auto_generate_hero_image = true | Activity Planner |
Input
interface BlogImageGeneratorInput {
tenantId: string;
blogPostId: string;
imageType: 'hero' | 'inline_section';
sectionTitle?: string; // required for inline_section
postTitle: string;
primaryKeyword: string;
imageStyle: ImageStyle;
primaryColor?: string; // hex e.g. "#1A73E8" — from BrandAsset
secondaryColor?: string;
}
type ImageStyle =
| 'photo_realistic'
| 'flat_illustration'
| 'minimal_infographic'
| 'abstract';Output
interface BlogImageGeneratorOutput {
tenantId: string;
blogPostId: string;
imageType: 'hero' | 'inline_section';
sectionTitle?: string;
imageUrl: string; // DigitalOcean Spaces public URL
altText: string; // auto-generated for accessibility
width: number;
height: number;
}How It Works
- Constructs an image prompt from the post title, primary keyword, image style, and brand colours
- Calls Azure OpenAI Images.generate with
model: dall-e-3,size: 1024x1024,quality: standard - Downloads the generated image
- Resizes using
sharp:- Hero: 1200 × 630px
- Inline section: 800 × 450px
- Uploads to DigitalOcean Spaces at path
tenants/{tenantId}/blog/{blogPostId}/{imageType}-{timestamp}.jpg - Generates descriptive alt text from the post title and section context
- Returns
{ imageUrl, altText, width, height } - API layer inserts image into
BlogPost.bodyMarkdown:- Hero: prepended as
\n\n - Inline: inserted after the target H2 as
\n\n\n\n
- Hero: prepended as
- If post is already published to WordPress, syncs: hero → set as
featured_media; inline → update WP post content
Image Prompt Construction
const styleDescriptions: Record<ImageStyle, string> = {
photo_realistic: 'Professional photorealistic image, high quality photography, commercial aesthetic, no text overlays',
flat_illustration: 'Modern flat design illustration, clean lines, simple shapes, minimal shadows, digital art style, no text',
minimal_infographic: 'Simple clean diagram with minimal icons and shapes, white background, minimal colour palette, no text labels',
abstract: 'Abstract geometric composition, smooth gradients, modern and sophisticated, no text or recognizable objects',
};
function buildImagePrompt(input: BlogImageGeneratorInput): string {
const styleDesc = styleDescriptions[input.imageStyle];
const colorInstruction = (input.primaryColor && input.secondaryColor)
? `Use ${input.primaryColor} as the dominant colour and ${input.secondaryColor} as an accent.`
: input.primaryColor
? `Dominant colour: ${input.primaryColor}.`
: '';
const subject = input.imageType === 'hero'
? `Representing the concept of: ${input.postTitle}. Related to: ${input.primaryKeyword}.`
: `Illustrating the section: "${input.sectionTitle}". Context: ${input.primaryKeyword}.`;
return [styleDesc, colorInstruction, subject, '16:9 wide banner format. No text overlays.']
.filter(Boolean)
.join(' ');
}RAG Usage
None. The blog image generator does not query RAG. It relies entirely on the post metadata (title, keyword, section title) and brand asset data passed directly in the input.
HITL Gates
None. Image generation is a production step after the blog post has already been approved. The DM can regenerate or reject individual images from the dashboard without a formal approval gate.
Guardrails
| Rule | Enforcement |
|---|---|
| Never generate images with text overlays | Prompt explicitly instructs “no text overlays”; content policy check on Azure side |
| Never generate images with recognisable real people | Prompt uses abstract/conceptual subjects only; real person references stripped from post title before prompt construction |
| Image must be resized before upload | sharp resize step is mandatory; raw 1024×1024 is never uploaded |
| Alt text is always populated | If no alt text is generated, fallback: ${postTitle} — blog post image |
| Credit deducted before job enqueue | API checks credit balance before enqueuing; returns 402 if insufficient |
Storage Path Convention
DigitalOcean Spaces bucket: leadmetrics-media
Path: tenants/{tenantId}/blog/{blogPostId}/hero-{timestamp}.jpg
tenants/{tenantId}/blog/{blogPostId}/inline-{sectionSlug}-{timestamp}.jpgAll images are public-read. CDN URL format: https://cdn.leadmetrics.io/tenants/{tenantId}/blog/{blogPostId}/{filename}