16 KiB
16 KiB
PRD: AI-Generated OG Images for TPMJS
Overview
Implement a system where every page on tpmjs.com (both static and dynamic) has a unique, AI-generated Open Graph image created by Gemini. The OG images should be contextually relevant, visually distinctive, and reflect the actual content of each page.
Goals
- Unique visual identity - Every page gets a custom OG image, not generic templates
- Content-aware generation - Images reflect the actual page content (tool descriptions, categories, etc.)
- Performance-optimized - Pre-generate for static pages, on-demand with caching for dynamic
- Cost-efficient - Minimize redundant API calls through intelligent caching
Technical Architecture
Page Types & Generation Strategy
| Page Type | Example | Generation Strategy |
|---|---|---|
| Static | /, /about, /playground |
Build-time generation, stored in CDN |
| Semi-static | /tools, /categories |
Build-time with ISR revalidation |
| Dynamic | /tool/[slug] |
On-demand with aggressive caching |
| User-generated | Future: /user/[id] |
On-demand with TTL cache |
System Components
┌─────────────────────────────────────────────────────────────────┐
│ Request Flow │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Browser/Crawler │
│ │ │
│ ▼ │
│ ┌─────────────┐ ┌──────────────┐ ┌─────────────────────┐ │
│ │ Next.js │───▶│ OG Image │───▶│ Vercel KV/Redis │ │
│ │ Metadata │ │ Route │ │ Cache Layer │ │
│ │ Generator │ │ /api/og/[..] │ │ │ │
│ └─────────────┘ └──────────────┘ └─────────────────────┘ │
│ │ │ │
│ │ cache miss │ cache hit │
│ ▼ ▼ │
│ ┌──────────────┐ Return cached │
│ │ Content │ image URL │
│ │ Extractor │ │
│ └──────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ Prompt │ │
│ │ Builder │ │
│ └──────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ Gemini API │ │
│ │ (Imagen 3) │ │
│ └──────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ Vercel Blob │ │
│ │ Storage │ │
│ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Implementation Details
1. Content Extraction Service
Each page type needs a content extractor that produces structured data for the prompt:
// packages/og-generator/src/extractors/types.ts
interface PageContent {
pageType: 'home' | 'tool' | 'category' | 'search' | 'playground' | 'about';
title: string;
description: string;
keywords: string[];
primaryColor?: string;
// Page-specific data
toolData?: {
name: string;
category: string;
tier: 'minimal' | 'rich';
capabilities: string[];
npmDownloads: number;
};
categoryData?: {
name: string;
toolCount: number;
topTools: string[];
};
}
2. Prompt Engineering
The prompt should be carefully crafted to produce consistent, on-brand images:
// packages/og-generator/src/prompt-builder.ts
function buildPrompt(content: PageContent): string {
const baseStyle = `
Create a 1200x630 pixel Open Graph image with these constraints:
- Modern, clean tech aesthetic
- Dark background (#0a0a0a to #1a1a1a gradient)
- Accent colors: cyan (#00d4ff), purple (#8b5cf6)
- Abstract geometric shapes or flowing lines
- NO text in the image (text added via overlay)
- Professional, minimalist design
- Subtle depth and dimensionality
`;
const pageContext = buildPageContext(content);
return `${baseStyle}\n\nContext for this specific page:\n${pageContext}`;
}
function buildPageContext(content: PageContent): string {
switch (content.pageType) {
case 'tool':
return `
This image is for a developer tool called "${content.toolData.name}".
Category: ${content.toolData.category}
Capabilities: ${content.toolData.capabilities.join(', ')}
Visual motif: Represent the concept of "${content.toolData.category}"
abstractly using shapes, gradients, or symbolic elements.
Mood: Professional, powerful, trustworthy.
`;
case 'category':
return `
This image represents a category of developer tools: "${content.categoryData.name}".
Contains ${content.categoryData.toolCount} tools.
Visual motif: Abstract representation of "${content.categoryData.name}"
as a concept - use symbolic shapes and patterns.
Mood: Organized, comprehensive, discoverable.
`;
case 'home':
return `
This is the homepage for TPMJS - a registry of AI/LLM tools for developers.
Visual motif: Interconnected nodes, flowing data streams, AI neural patterns.
Mood: Cutting-edge, innovative, developer-focused.
`;
// ... other page types
}
}
3. API Route Implementation
// apps/web/src/app/api/og/[...path]/route.ts
import { ImageResponse } from 'next/og';
import { kv } from '@vercel/kv';
import { put } from '@vercel/blob';
export const runtime = 'edge';
export const maxDuration = 60;
export async function GET(
request: Request,
{ params }: { params: { path: string[] } }
) {
const pagePath = '/' + params.path.join('/');
const cacheKey = `og:${pagePath}`;
// 1. Check cache
const cached = await kv.get<string>(cacheKey);
if (cached) {
return Response.redirect(cached);
}
// 2. Extract page content
const content = await extractPageContent(pagePath);
// 3. Check if we have a pre-generated image (build-time)
const preGenerated = await checkPreGeneratedImage(pagePath);
if (preGenerated) {
await kv.set(cacheKey, preGenerated, { ex: 86400 }); // 24hr cache
return Response.redirect(preGenerated);
}
// 4. Generate new image via Gemini
const prompt = buildPrompt(content);
const imageBuffer = await generateWithGemini(prompt);
// 5. Upload to Vercel Blob
const { url } = await put(`og/${pagePath}.png`, imageBuffer, {
access: 'public',
contentType: 'image/png',
});
// 6. Cache the URL
await kv.set(cacheKey, url, { ex: 86400 });
// 7. Return redirect to blob URL
return Response.redirect(url);
}
4. Metadata Integration
// apps/web/src/app/tool/[slug]/page.tsx
export async function generateMetadata({ params }): Promise<Metadata> {
const tool = await getTool(params.slug);
return {
title: tool.name,
description: tool.description,
openGraph: {
title: tool.name,
description: tool.description,
images: [{
url: `https://tpmjs.com/api/og/tool/${params.slug}`,
width: 1200,
height: 630,
alt: `${tool.name} - TPMJS Tool`,
}],
},
twitter: {
card: 'summary_large_image',
images: [`https://tpmjs.com/api/og/tool/${params.slug}`],
},
};
}
5. Build-Time Pre-Generation
// scripts/generate-og-images.ts
async function preGenerateOGImages() {
// Static pages - always regenerate on build
const staticPages = ['/', '/about', '/playground', '/tools'];
// Semi-static - regenerate if content changed
const categories = await prisma.category.findMany();
const categoryPages = categories.map(c => `/category/${c.slug}`);
// High-traffic tools - pre-generate top 100
const topTools = await prisma.tool.findMany({
orderBy: { qualityScore: 'desc' },
take: 100,
});
const toolPages = topTools.map(t => `/tool/${t.slug}`);
const allPages = [...staticPages, ...categoryPages, ...toolPages];
for (const page of allPages) {
await generateAndStoreOGImage(page);
await sleep(1000); // Rate limiting
}
}
Caching Strategy
Cache Layers
- Vercel Edge Cache - CDN caching of the redirect response
- Vercel KV - URL mapping cache (path → blob URL)
- Vercel Blob - Permanent image storage
Cache Invalidation
| Trigger | Action |
|---|---|
| Tool updated | Invalidate /tool/[slug] cache key |
| New tool added | Pre-generate OG image via webhook |
| Build/deploy | Regenerate static page images |
| Manual | Admin endpoint to force regeneration |
// apps/web/src/app/api/admin/invalidate-og/route.ts
export async function POST(request: Request) {
const { path, secret } = await request.json();
if (secret !== process.env.ADMIN_SECRET) {
return Response.json({ error: 'Unauthorized' }, { status: 401 });
}
await kv.del(`og:${path}`);
// Optionally trigger regeneration
await fetch(`https://tpmjs.com/api/og${path}`);
return Response.json({ success: true });
}
Cost Considerations
Gemini API Costs
- Imagen 3 pricing: ~$0.02-0.04 per image (estimate)
- Initial generation: ~500 pages × $0.03 = $15
- Monthly regeneration: ~100 new tools × $0.03 = $3
Storage Costs
- Vercel Blob: $0.15/GB/month
- Estimated: 500 images × 500KB = 250MB = ~$0.04/month
Optimization Strategies
- Aggressive caching - 24-48 hour TTL minimum
- Batch generation - Build-time for predictable pages
- Lazy generation - On-demand for long-tail pages
- Image optimization - Compress before storage
Fallback Strategy
If Gemini API fails or rate limits:
- Primary: Return cached image if available (even if stale)
- Secondary: Return pre-designed template image with text overlay
- Tertiary: Return generic TPMJS branded fallback image
async function generateOGImage(pagePath: string): Promise<string> {
try {
return await generateWithGemini(pagePath);
} catch (error) {
console.error('Gemini generation failed:', error);
// Fallback to template
return await generateTemplateImage(pagePath);
}
}
Text Overlay System
Since AI-generated images shouldn't contain text (unreliable), add text via a compositing layer:
// Using @vercel/og or sharp for text overlay
async function compositeOGImage(
baseImageUrl: string,
content: PageContent
): Promise<Buffer> {
const baseImage = await fetch(baseImageUrl).then(r => r.arrayBuffer());
// Use sharp or canvas to overlay:
// - Page title (top-left or centered)
// - TPMJS logo (bottom-right)
// - Optional: category badge, stats, etc.
return await sharp(baseImage)
.composite([
{ input: titleBuffer, top: 50, left: 50 },
{ input: logoBuffer, top: 530, left: 1050 },
])
.png()
.toBuffer();
}
Implementation Phases
Phase 1: Foundation (Week 1)
- Set up Vercel Blob storage
- Set up Vercel KV for caching
- Create basic
/api/og/[...path]route - Implement content extractors for each page type
Phase 2: Gemini Integration (Week 2)
- Integrate Gemini Imagen 3 API
- Design and test prompts for each page type
- Implement text overlay compositing
- Add fallback image generation
Phase 3: Optimization (Week 3)
- Build-time pre-generation script
- Cache invalidation webhooks
- Monitoring and alerting
- Cost tracking dashboard
Phase 4: Polish (Week 4)
- A/B test different prompt styles
- Refine prompts based on results
- Admin UI for manual regeneration
- Documentation
Success Metrics
- Coverage: 100% of pages have unique OG images
- Performance: OG image response < 500ms (cache hit)
- Quality: Manual review score > 4/5 on sample of images
- Cost: < $50/month total (API + storage)
Open Questions
- Model choice: Gemini Imagen 3 vs DALL-E 3 vs Stable Diffusion?
- Text rendering: Overlay vs bake into prompt?
- Style consistency: How to maintain brand while varying per-page?
- A/B testing: How to measure OG image effectiveness?
Appendix: Example Prompts
Homepage
Create a 1200x630 Open Graph image. Dark gradient background (#0a0a0a to #1a1a1a).
Abstract visualization of an AI neural network with glowing cyan (#00d4ff) and
purple (#8b5cf6) nodes connected by flowing data streams. Minimalist, professional,
futuristic. No text. Subtle grid pattern overlay. Represents a developer tool registry
for AI and LLM integrations.
Tool Page (e.g., file-search tool)
Create a 1200x630 Open Graph image. Dark gradient background. Abstract representation
of file searching - interconnected folder icons, magnifying glass motif, flowing
document shapes. Color scheme: cyan and purple accents on dark. Minimalist style,
no text. Professional developer tool aesthetic. Represents a powerful file search
capability for AI assistants.
Category Page (e.g., "Database" category)
Create a 1200x630 Open Graph image. Dark gradient background. Abstract database
visualization - cylindrical shapes, connected nodes, data flow patterns. Organized
grid structure suggesting multiple tools. Cyan and purple accent colors. Minimalist,
no text. Professional aesthetic representing a collection of database tools for
AI development.
Note: "Gemini 3 nanobanana" interpreted as Google Gemini with Imagen 3 for image generation. Actual model availability and pricing should be verified.