From 0a3b00424302fc14ac486635b0405c290c843c87 Mon Sep 17 00:00:00 2001 From: Ajax Davis Date: Wed, 17 Dec 2025 11:06:57 +1000 Subject: [PATCH] docs: add PRD for AI-generated OG images --- docs/prd-dynamic-og-images.md | 445 ++++++++++++++++++++++++++++++++++ 1 file changed, 445 insertions(+) create mode 100644 docs/prd-dynamic-og-images.md diff --git a/docs/prd-dynamic-og-images.md b/docs/prd-dynamic-og-images.md new file mode 100644 index 0000000..3cf2f54 --- /dev/null +++ b/docs/prd-dynamic-og-images.md @@ -0,0 +1,445 @@ +# 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 + +1. **Unique visual identity** - Every page gets a custom OG image, not generic templates +2. **Content-aware generation** - Images reflect the actual page content (tool descriptions, categories, etc.) +3. **Performance-optimized** - Pre-generate for static pages, on-demand with caching for dynamic +4. **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: + +```typescript +// 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: + +```typescript +// 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 + +```typescript +// 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(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 + +```typescript +// apps/web/src/app/tool/[slug]/page.tsx + +export async function generateMetadata({ params }): Promise { + 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 + +```typescript +// 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 + +1. **Vercel Edge Cache** - CDN caching of the redirect response +2. **Vercel KV** - URL mapping cache (path → blob URL) +3. **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 | + +```typescript +// 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 + +1. **Aggressive caching** - 24-48 hour TTL minimum +2. **Batch generation** - Build-time for predictable pages +3. **Lazy generation** - On-demand for long-tail pages +4. **Image optimization** - Compress before storage + +## Fallback Strategy + +If Gemini API fails or rate limits: + +1. **Primary**: Return cached image if available (even if stale) +2. **Secondary**: Return pre-designed template image with text overlay +3. **Tertiary**: Return generic TPMJS branded fallback image + +```typescript +async function generateOGImage(pagePath: string): Promise { + 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: + +```typescript +// Using @vercel/og or sharp for text overlay + +async function compositeOGImage( + baseImageUrl: string, + content: PageContent +): Promise { + 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 + +1. **Coverage**: 100% of pages have unique OG images +2. **Performance**: OG image response < 500ms (cache hit) +3. **Quality**: Manual review score > 4/5 on sample of images +4. **Cost**: < $50/month total (API + storage) + +## Open Questions + +1. **Model choice**: Gemini Imagen 3 vs DALL-E 3 vs Stable Diffusion? +2. **Text rendering**: Overlay vs bake into prompt? +3. **Style consistency**: How to maintain brand while varying per-page? +4. **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.*