← 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 })
  }
}