← back to Dear Bubbe Nextjs
app/api/generate/blog/route.ts
238 lines
import { NextRequest, NextResponse } from 'next/server'
import Anthropic from '@anthropic-ai/sdk'
import { updateStat } from '../../stats/route'
import fs from 'fs'
import path from 'path'
import { chat as ollamaChat } from '@/lib/llm-local'
// Anthropic SDK kept for optional fallback (USE_ANTHROPIC=1).
const anthropic = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY || ''
})
interface BlogGenerationRequest {
id?: string
sku?: string
title: string
imageUrl?: string
description?: string
category?: string
vendor?: string
price?: string
}
interface BlogPost {
title: string
content: string
excerpt: string
tags: string[]
sku?: string
productId?: string
status: 'draft' | 'published' | 'generated_only'
createdAt: string
wordCount: number
readingTime: number
}
// Ensure blogs directory exists
function ensureBlogsDir() {
const blogsDir = path.join(process.cwd(), 'data', 'generated-blogs')
if (!fs.existsSync(blogsDir)) {
fs.mkdirSync(blogsDir, { recursive: true })
}
return blogsDir
}
// Save blog post to file
function saveBlogPost(blog: BlogPost): string {
const blogsDir = ensureBlogsDir()
const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
const slug = blog.title.toLowerCase()
.replace(/[^a-z0-9\s-]/g, '')
.replace(/\s+/g, '-')
.substring(0, 50)
const filename = `${timestamp}-${slug}.json`
const filepath = path.join(blogsDir, filename)
try {
fs.writeFileSync(filepath, JSON.stringify(blog, null, 2))
console.log(`[BLOG] Saved blog post to: ${filepath}`)
return filepath
} catch (error) {
console.error('[BLOG] Error saving blog post:', error)
throw error
}
}
// Calculate reading time (approximate)
function calculateReadingTime(content: string): number {
const wordsPerMinute = 200
const words = content.split(/\s+/).length
return Math.ceil(words / wordsPerMinute)
}
// Generate blog post using Claude
async function generateBlogContent(product: BlogGenerationRequest): Promise<BlogPost> {
const prompt = `Create a compelling blog post for this product:
Product Title: ${product.title}
${product.sku ? `SKU: ${product.sku}` : ''}
${product.description ? `Description: ${product.description}` : ''}
${product.category ? `Category: ${product.category}` : ''}
${product.vendor ? `Brand: ${product.vendor}` : ''}
${product.price ? `Price: ${product.price}` : ''}
Write an engaging blog post (400-600 words) that includes:
1. An attention-grabbing headline
2. Introduction highlighting the product's unique features
3. Benefits and use cases
4. Style suggestions and trending applications
5. Call-to-action
Format:
TITLE: [Your headline here]
EXCERPT: [2-3 sentence summary]
TAGS: [5-8 relevant tags, comma-separated]
CONTENT:
[Full blog post content in markdown format]
Make it SEO-friendly, engaging, and professional. Focus on benefits and lifestyle appeal.`
let content = ''
if (process.env.USE_ANTHROPIC === '1') {
const response = await anthropic.messages.create({
model: 'claude-opus-4-8',
max_tokens: 2048,
messages: [{ role: 'user', content: prompt }]
})
content = response.content[0].type === 'text' ? response.content[0].text : ''
} else {
const r = await ollamaChat(
[{ role: 'user', content: prompt }],
{ maxTokens: 2048, temperature: 0.7 }
)
content = r.content
}
// Parse the structured response
const titleMatch = content.match(/TITLE:\s*(.+)/i)
const excerptMatch = content.match(/EXCERPT:\s*(.+)/i)
const tagsMatch = content.match(/TAGS:\s*(.+)/i)
const contentMatch = content.match(/CONTENT:\s*([\s\S]+)/i)
const title = titleMatch ? titleMatch[1].trim() : product.title
const excerpt = excerptMatch ? excerptMatch[1].trim() : 'A compelling new product offering.'
const tags = tagsMatch ? tagsMatch[1].split(',').map(tag => tag.trim()) : ['product', 'new', 'featured']
const blogContent = contentMatch ? contentMatch[1].trim() : content
const wordCount = blogContent.split(/\s+/).length
const readingTime = calculateReadingTime(blogContent)
const blog: BlogPost = {
title,
content: blogContent,
excerpt,
tags,
sku: product.sku,
productId: product.id,
status: 'generated_only',
createdAt: new Date().toISOString(),
wordCount,
readingTime
}
return blog
}
export async function POST(request: NextRequest) {
try {
const startTime = Date.now()
const body = await request.json()
if (!body.title) {
return NextResponse.json({
error: 'Product title is required'
}, { status: 400 })
}
const product: BlogGenerationRequest = {
id: body.id,
sku: body.sku,
title: body.title,
imageUrl: body.imageUrl,
description: body.description,
category: body.category,
vendor: body.vendor,
price: body.price
}
console.log(`[BLOG] Generating blog post for: ${product.title}`)
// Generate blog content
const blog = await generateBlogContent(product)
// Save to file
const filepath = saveBlogPost(blog)
// Update stats
updateStat('blogPostsCreated', 1)
updateStat('tasksCompleted', 1)
const responseTime = Date.now() - startTime
console.log(`[BLOG] Blog post generated in ${responseTime}ms`)
return NextResponse.json({
success: true,
blog: {
...blog,
filepath,
generationTime: responseTime
},
message: 'Blog post generated successfully'
})
} catch (error) {
console.error('[BLOG] Error generating blog post:', error)
// Update error stats
updateStat('errorsToday', 1)
return NextResponse.json({
error: 'Failed to generate blog post',
details: error instanceof Error ? error.message : String(error)
}, { status: 500 })
}
}
export async function GET(request: NextRequest) {
try {
const blogsDir = ensureBlogsDir()
// List all generated blog posts
const files = fs.readdirSync(blogsDir)
.filter(file => file.endsWith('.json'))
.sort()
.reverse() // Most recent first
.slice(0, 20) // Last 20 posts
const blogs = files.map(file => {
try {
const content = fs.readFileSync(path.join(blogsDir, file), 'utf-8')
return JSON.parse(content)
} catch (e) {
return null
}
}).filter(Boolean)
return NextResponse.json({
blogs,
count: blogs.length,
totalFiles: files.length
})
} catch (error) {
console.error('[BLOG] Error listing blog posts:', error)
return NextResponse.json({
error: 'Failed to list blog posts'
}, { status: 500 })
}
}