docs: add PRD for AI-generated OG images
This commit is contained in:
parent
0e5d4d6e3b
commit
00718382fc
1 changed files with 445 additions and 0 deletions
445
docs/prd-dynamic-og-images.md
Normal file
445
docs/prd-dynamic-og-images.md
Normal file
|
|
@ -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<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
|
||||
|
||||
```typescript
|
||||
// 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
|
||||
|
||||
```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<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:
|
||||
|
||||
```typescript
|
||||
// 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
|
||||
|
||||
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.*
|
||||
Loading…
Add table
Add a link
Reference in a new issue