.do
Complete Examples

Complete Content Generation Service

Production-ready blog post generation service with research, writing, SEO optimization, and quality assurance

A comprehensive, production-ready blog post generation service demonstrating multi-stage AI workflows, quality assurance, SEO optimization, and monetization.

Service Overview

This example showcases a complete content generation service that transforms topics into publication-ready blog posts with:

  • AI-powered research - Keyword research and competitor analysis
  • Multi-stage generation - Outline creation, content writing, optimization
  • Quality assurance - Automated readability, grammar, and plagiarism checks
  • SEO optimization - Meta tags, keyword placement, header structure
  • Image generation - AI-generated featured and inline images
  • Tiered pricing - Basic, Standard, and Premium tiers
  • Complete monitoring - Metrics, logging, and alerting

Revenue Model: $29-99 per blog post Target Market: Content marketers, agencies, businesses, bloggers Processing Time: 5-10 minutes per post Monthly Revenue Potential: $290-4,950 per customer


Complete Service Definition

Service Configuration

import $, { ai, db, on, send } from 'sdk.do'

const contentService = await $.Service.create({
  name: 'AI Blog Post Generator',
  description: 'Transform topics into publication-ready blog posts with research, writing, optimization, and images',

  type: $.ServiceType.ContentGeneration,

  // Service metadata
  metadata: {
    version: '1.0.0',
    category: 'content',
    tags: ['blog', 'seo', 'ai-writing', 'content-marketing'],
    author: 'Content Services Inc',
  },

  // Input schema
  input: {
    required: ['topic', 'tier'],
    optional: ['industry', 'audience', 'tone', 'targetKeywords', 'excludeKeywords', 'competitorUrls', 'style', 'cta', 'internalLinks'],
    schema: {
      topic: {
        type: 'string',
        description: 'The main topic or title for the blog post',
        minLength: 5,
        maxLength: 200,
        example: 'How AI is Transforming Content Marketing in 2025',
      },
      tier: {
        type: 'string',
        enum: ['basic', 'standard', 'premium'],
        description: 'Service tier determining word count, images, and features',
        default: 'standard',
      },
      industry: {
        type: 'string',
        description: 'Target industry for industry-specific terminology',
        example: 'technology',
      },
      audience: {
        type: 'string',
        description: 'Target audience persona',
        example: 'Marketing managers at B2B SaaS companies',
      },
      tone: {
        type: 'string',
        enum: ['professional', 'casual', 'authoritative', 'conversational', 'technical'],
        default: 'professional',
        description: 'Writing tone and style',
      },
      targetKeywords: {
        type: 'array',
        items: { type: 'string' },
        description: 'Keywords to target for SEO',
        maxItems: 10,
      },
      excludeKeywords: {
        type: 'array',
        items: { type: 'string' },
        description: 'Keywords to avoid',
      },
      competitorUrls: {
        type: 'array',
        items: { type: 'string', format: 'url' },
        description: 'Competitor articles to analyze',
        maxItems: 5,
      },
      style: {
        type: 'string',
        enum: ['how-to', 'listicle', 'guide', 'opinion', 'news', 'case-study'],
        description: 'Article format and structure',
      },
      cta: {
        type: 'object',
        properties: {
          text: { type: 'string' },
          url: { type: 'string', format: 'url' },
        },
        description: 'Call-to-action to include in post',
      },
      internalLinks: {
        type: 'array',
        items: {
          type: 'object',
          properties: {
            text: { type: 'string' },
            url: { type: 'string', format: 'url' },
          },
        },
        description: 'Internal links to weave into content',
        maxItems: 10,
      },
    },
  },

  // Output schema
  output: {
    schema: {
      post: {
        type: 'object',
        properties: {
          title: { type: 'string' },
          slug: { type: 'string' },
          content: { type: 'string', format: 'markdown' },
          excerpt: { type: 'string' },
          images: {
            type: 'array',
            items: {
              type: 'object',
              properties: {
                url: { type: 'string', format: 'url' },
                alt: { type: 'string' },
                caption: { type: 'string' },
                position: { type: 'string' },
              },
            },
          },
          seo: {
            type: 'object',
            properties: {
              metaTitle: { type: 'string' },
              metaDescription: { type: 'string' },
              keywords: { type: 'array', items: { type: 'string' } },
              slug: { type: 'string' },
              canonicalUrl: { type: 'string' },
            },
          },
        },
      },
      analytics: {
        type: 'object',
        properties: {
          wordCount: { type: 'number' },
          readingTime: { type: 'number' },
          seoScore: { type: 'number' },
          readabilityScore: { type: 'number' },
          keywordDensity: { type: 'number' },
          uniquenessScore: { type: 'number' },
        },
      },
      research: {
        type: 'object',
        properties: {
          keywords: {
            type: 'object',
            properties: {
              primary: { type: 'array', items: { type: 'string' } },
              secondary: { type: 'array', items: { type: 'string' } },
              longtail: { type: 'array', items: { type: 'string' } },
            },
          },
          competitorAnalysis: {
            type: 'array',
            items: {
              type: 'object',
              properties: {
                url: { type: 'string' },
                wordCount: { type: 'number' },
                keyTopics: { type: 'array', items: { type: 'string' } },
                strengths: { type: 'array', items: { type: 'string' } },
                gaps: { type: 'array', items: { type: 'string' } },
              },
            },
          },
        },
      },
    },
  },

  // Pricing configuration
  pricing: {
    model: 'tiered',
    currency: 'USD',
    tiers: [
      {
        name: 'basic',
        price: 29.0,
        description: 'Perfect for simple blog posts',
        features: {
          wordCount: 800,
          images: 1,
          seoOptimization: 'basic',
          research: 'basic',
          revisions: 1,
          turnaround: '1 hour',
        },
        limits: {
          maxKeywords: 3,
          maxCompetitorAnalysis: 1,
          qualityChecks: ['readability', 'grammar'],
        },
      },
      {
        name: 'standard',
        price: 49.0,
        description: 'Most popular for professional content',
        features: {
          wordCount: 1500,
          images: 3,
          seoOptimization: 'advanced',
          research: 'comprehensive',
          revisions: 2,
          turnaround: '2 hours',
        },
        limits: {
          maxKeywords: 5,
          maxCompetitorAnalysis: 3,
          qualityChecks: ['readability', 'grammar', 'seo', 'plagiarism'],
        },
      },
      {
        name: 'premium',
        price: 99.0,
        description: 'Ultimate for high-quality, in-depth content',
        features: {
          wordCount: 2500,
          images: 5,
          seoOptimization: 'expert',
          research: 'deep',
          revisions: 3,
          turnaround: '4 hours',
        },
        limits: {
          maxKeywords: 10,
          maxCompetitorAnalysis: 5,
          qualityChecks: ['readability', 'grammar', 'seo', 'plagiarism', 'factChecking', 'expertReview'],
        },
      },
    ],
  },

  // SLA guarantees
  sla: {
    uptime: 99.9,
    responseTime: {
      basic: 3600, // 1 hour in seconds
      standard: 7200, // 2 hours
      premium: 14400, // 4 hours
    },
    qualityGuarantees: {
      minReadability: 60,
      minSeoScore: 70,
      minUniqueness: 95,
      maxGrammarErrors: 3,
    },
    refundPolicy: {
      enabled: true,
      conditions: ['Quality score below 70%', 'Delivery time exceeds SLA by 50%', 'Customer not satisfied with 3 revisions'],
    },
  },

  // Monitoring configuration
  monitoring: {
    metrics: ['execution_time', 'word_count', 'seo_score', 'quality_score', 'customer_satisfaction', 'revision_rate', 'refund_rate'],
    alerts: {
      lowQualityScore: { threshold: 70, severity: 'warning' },
      highExecutionTime: { threshold: 600, severity: 'critical' },
      highRevisionRate: { threshold: 0.3, severity: 'warning' },
      highRefundRate: { threshold: 0.05, severity: 'critical' },
    },
    logging: {
      level: 'info',
      includeInputs: false, // Privacy
      includeOutputs: false, // Privacy
      includeMetrics: true,
      includeErrors: true,
    },
  },
})

Implementation

Stage 1: Keyword Research

async function performKeywordResearch(inputs: any, config: any) {
  console.log('[Research] Starting keyword research...')

  try {
    // User-provided keywords
    const seedKeywords = inputs.targetKeywords || []

    // Generate keyword ideas using AI
    const keywordIdeas = await ai.research({
      model: 'gpt-5',
      task: 'keyword-research',
      context: {
        topic: inputs.topic,
        industry: inputs.industry,
        audience: inputs.audience,
        seedKeywords,
      },
      parameters: {
        count: config.limits.maxKeywords * 3,
        intent: ['informational', 'commercial'],
        difficulty: ['low', 'medium'],
      },
    })

    // Analyze search volume and competition (simulated - use real API in production)
    const analyzedKeywords = await Promise.all(
      keywordIdeas.keywords.map(async (keyword: string) => {
        const analysis = await ai.analyze({
          model: 'gpt-5',
          task: 'keyword-analysis',
          keyword,
          context: {
            topic: inputs.topic,
            industry: inputs.industry,
          },
          metrics: ['search-volume', 'competition', 'relevance', 'intent', 'difficulty'],
        })

        return {
          keyword,
          searchVolume: analysis.searchVolume || 1000,
          competition: analysis.competition || 'medium',
          relevance: analysis.relevance || 0.8,
          intent: analysis.intent || 'informational',
          difficulty: analysis.difficulty || 50,
          score: calculateKeywordScore(analysis),
        }
      })
    )

    // Sort by score and select top keywords
    const sortedKeywords = analyzedKeywords.sort((a, b) => b.score - a.score)

    const selectedKeywords = {
      primary: sortedKeywords.slice(0, Math.min(3, config.limits.maxKeywords)),
      secondary: sortedKeywords.slice(3, Math.min(8, config.limits.maxKeywords * 2)),
      longtail: sortedKeywords.slice(8, Math.min(15, config.limits.maxKeywords * 3)),
    }

    console.log(`[Research] Found ${sortedKeywords.length} keywords`)
    console.log(`[Research] Primary: ${selectedKeywords.primary.map((k: any) => k.keyword).join(', ')}`)

    // Emit progress
    send.ServiceProgress.update, {
      stage: 'keyword-research',
      progress: 100,
      message: `Identified ${selectedKeywords.primary.length} primary keywords`,
    })

    return {
      all: sortedKeywords,
      ...selectedKeywords,
    }
  } catch (error) {
    console.error('[Research] Keyword research failed:', error)
    throw new Error(`Keyword research failed: ${error.message}`)
  }
}

function calculateKeywordScore(analysis: any): number {
  // Custom scoring algorithm
  const volumeScore = Math.min(analysis.searchVolume / 10000, 1) * 30
  const competitionScore = (1 - analysis.difficulty / 100) * 30
  const relevanceScore = analysis.relevance * 40

  return volumeScore + competitionScore + relevanceScore
}

Stage 2: Competitor Analysis

async function analyzeCompetitors(inputs: any, keywords: any, config: any) {
  console.log('[Research] Analyzing competitors...')

  if (!inputs.competitorUrls || inputs.competitorUrls.length === 0) {
    console.log('[Research] No competitor URLs provided, skipping analysis')
    return []
  }

  const competitorAnalyses = []
  const maxCompetitors = Math.min(inputs.competitorUrls.length, config.limits.maxCompetitorAnalysis)

  for (let i = 0; i < maxCompetitors; i++) {
    const url = inputs.competitorUrls[i]

    try {
      console.log(`[Research] Analyzing competitor ${i + 1}/${maxCompetitors}: ${url}`)

      // Fetch and analyze competitor content
      const analysis = await ai.analyze({
        model: 'gpt-5',
        task: 'competitor-analysis',
        source: url,
        extract: ['word-count', 'main-topics', 'keywords-used', 'content-structure', 'strengths', 'weaknesses', 'gaps'],
        context: {
          ourTopic: inputs.topic,
          ourKeywords: keywords.primary.map((k: any) => k.keyword),
        },
      })

      competitorAnalyses.push({
        url,
        wordCount: analysis.wordCount || 1200,
        mainTopics: analysis.mainTopics || [],
        keywordsUsed: analysis.keywordsUsed || [],
        structure: analysis.structure || {},
        strengths: analysis.strengths || [],
        weaknesses: analysis.weaknesses || [],
        gaps: analysis.gaps || [],
        score: analysis.overallScore || 75,
      })

      // Emit progress
      send.ServiceProgress.update, {
        stage: 'competitor-analysis',
        progress: ((i + 1) / maxCompetitors) * 100,
        message: `Analyzed ${i + 1} of ${maxCompetitors} competitors`,
      })
    } catch (error) {
      console.error(`[Research] Failed to analyze competitor ${url}:`, error)
      // Continue with other competitors
    }
  }

  console.log(`[Research] Completed analysis of ${competitorAnalyses.length} competitors`)

  return competitorAnalyses
}

Stage 3: Outline Creation

async function createOutline(inputs: any, keywords: any, competitors: any, config: any) {
  console.log('[Writing] Creating content outline...')

  try {
    const outline = await ai.generate({
      model: 'gpt-5',
      task: 'blog-outline',
      context: {
        topic: inputs.topic,
        targetLength: config.features.wordCount,
        keywords: keywords.primary.map((k: any) => k.keyword),
        audience: inputs.audience,
        tone: inputs.tone || 'professional',
        style: inputs.style || 'guide',
        competitorInsights: competitors.map((c: any) => ({
          topics: c.mainTopics,
          gaps: c.gaps,
        })),
        internalLinks: inputs.internalLinks || [],
      },
      requirements: {
        sections: Math.ceil(config.features.wordCount / 300), // ~300 words per section
        includeIntro: true,
        includeConclusion: true,
        includeCallToAction: !!inputs.cta,
        hierarchyDepth: 3, // H1, H2, H3
      },
    })

    // Validate outline structure
    if (!outline.sections || outline.sections.length < 3) {
      throw new Error('Generated outline is too short')
    }

    console.log(`[Writing] Created outline with ${outline.sections.length} sections`)

    // Emit progress
    send.ServiceProgress.update, {
      stage: 'outline-creation',
      progress: 100,
      message: `Created ${outline.sections.length}-section outline`,
    })

    return outline
  } catch (error) {
    console.error('[Writing] Outline creation failed:', error)
    throw new Error(`Outline creation failed: ${error.message}`)
  }
}

Stage 4: Content Writing

async function generateContent(inputs: any, outline: any, keywords: any, config: any) {
  console.log('[Writing] Generating content...')

  try {
    const content = await ai.generate({
      model: 'gpt-5',
      task: 'blog-content',
      context: {
        outline: outline.sections,
        topic: inputs.topic,
        keywords: {
          primary: keywords.primary.map((k: any) => k.keyword),
          secondary: keywords.secondary.map((k: any) => k.keyword),
        },
        targetLength: config.features.wordCount,
        tone: inputs.tone || 'professional',
        audience: inputs.audience,
        internalLinks: inputs.internalLinks || [],
        cta: inputs.cta,
      },
      quality: {
        minReadability: 60,
        keywordDensity: {
          primary: { min: 0.01, max: 0.03 },
          secondary: { min: 0.005, max: 0.015 },
        },
        uniqueness: 0.95,
        naturalLanguage: true,
      },
      constraints: {
        avoidKeywords: inputs.excludeKeywords || [],
        includeStatistics: true,
        includeExamples: true,
        useBulletPoints: true,
        useShortParagraphs: true,
      },
    })

    // Validate content
    if (!content.text || content.wordCount < config.features.wordCount * 0.8) {
      throw new Error('Generated content is too short')
    }

    console.log(`[Writing] Generated ${content.wordCount} words`)

    // Emit progress
    send.ServiceProgress.update, {
      stage: 'content-writing',
      progress: 100,
      message: `Generated ${content.wordCount} words of content`,
    })

    return content
  } catch (error) {
    console.error('[Writing] Content generation failed:', error)
    throw new Error(`Content generation failed: ${error.message}`)
  }
}

Stage 5: SEO Optimization

async function optimizeForSEO(inputs: any, content: any, keywords: any, config: any) {
  console.log('[SEO] Optimizing content...')

  try {
    const optimized = await ai.optimize({
      model: 'gpt-5',
      task: 'seo-optimization',
      content: content.text,
      keywords: keywords.primary.map((k: any) => k.keyword),
      level: config.features.seoOptimization, // 'basic', 'advanced', 'expert'
      optimizations: [
        'meta-title',
        'meta-description',
        'slug-generation',
        'header-hierarchy',
        'keyword-placement',
        'internal-linking',
        'image-alt-text',
        'semantic-keywords',
        'readability',
        'content-structure',
      ],
    })

    // Generate SEO metadata
    const seoMeta = {
      metaTitle: optimized.metaTitle || generateMetaTitle(inputs.topic, keywords.primary[0]),
      metaDescription: optimized.metaDescription || generateMetaDescription(content.excerpt),
      slug: optimized.slug || generateSlug(inputs.topic),
      keywords: keywords.primary.map((k: any) => k.keyword),
      canonicalUrl: inputs.canonicalUrl || null,
      openGraph: {
        title: optimized.metaTitle,
        description: optimized.metaDescription,
        type: 'article',
      },
    }

    // Calculate SEO score
    const seoScore = calculateSEOScore(optimized, keywords, config)

    console.log(`[SEO] SEO score: ${seoScore}/100`)

    // Emit progress
    send.ServiceProgress.update, {
      stage: 'seo-optimization',
      progress: 100,
      message: `SEO optimized (score: ${seoScore}/100)`,
    })

    return {
      content: optimized.text || content.text,
      seo: seoMeta,
      score: seoScore,
      improvements: optimized.improvements || [],
    }
  } catch (error) {
    console.error('[SEO] Optimization failed:', error)
    throw new Error(`SEO optimization failed: ${error.message}`)
  }
}

function generateMetaTitle(topic: string, primaryKeyword: any): string {
  const keyword = primaryKeyword?.keyword || ''
  return `${topic} | ${keyword}`.substring(0, 60)
}

function generateMetaDescription(excerpt: string): string {
  return excerpt.substring(0, 160)
}

function generateSlug(topic: string): string {
  return topic
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, '-')
    .replace(/^-|-$/g, '')
    .substring(0, 100)
}

function calculateSEOScore(optimized: any, keywords: any, config: any): number {
  let score = 0

  // Title optimization (20 points)
  if (optimized.metaTitle && optimized.metaTitle.length >= 30 && optimized.metaTitle.length <= 60) {
    score += 20
  }

  // Meta description (15 points)
  if (optimized.metaDescription && optimized.metaDescription.length >= 120 && optimized.metaDescription.length <= 160) {
    score += 15
  }

  // Keyword placement (25 points)
  if (optimized.keywordPlacement?.title) score += 10
  if (optimized.keywordPlacement?.firstParagraph) score += 10
  if (optimized.keywordPlacement?.headers) score += 5

  // Header hierarchy (15 points)
  if (optimized.headerHierarchy?.valid) score += 15

  // Internal linking (10 points)
  if (optimized.internalLinks >= 3) score += 10

  // Readability (15 points)
  if (optimized.readability >= 60) score += 15

  return Math.min(score, 100)
}

Stage 6: Image Generation

async function generateImages(inputs: any, content: any, outline: any, config: any) {
  console.log('[Images] Generating images...')

  const imageCount = config.features.images

  if (imageCount === 0) {
    console.log('[Images] No images requested')
    return []
  }

  try {
    // Generate image prompts based on content
    const imagePrompts = await ai.generate({
      model: 'gpt-5',
      task: 'image-prompts',
      context: {
        content: content.text,
        sections: outline.sections,
        topic: inputs.topic,
        count: imageCount,
      },
      requirements: {
        style: 'professional',
        descriptive: true,
        diverse: true,
        relevantToSections: true,
      },
    })

    const images = []

    for (let i = 0; i < imageCount; i++) {
      try {
        console.log(`[Images] Generating image ${i + 1}/${imageCount}`)

        const prompt = imagePrompts.prompts[i]

        // Generate image using AI
        const image = await ai.generateImage({
          model: 'dalle-3',
          prompt: prompt.text,
          parameters: {
            size: i === 0 ? '1792x1024' : '1024x1024', // Featured image larger
            quality: 'hd',
            style: 'natural',
          },
        })

        // Upload to storage
        const uploadedImage = await uploadImage(image.url, {
          name: `${generateSlug(inputs.topic)}-${i + 1}`,
          alt: prompt.alt,
          caption: prompt.caption,
        })

        images.push({
          url: uploadedImage.url,
          alt: prompt.alt,
          caption: prompt.caption,
          position: i === 0 ? 'featured' : `section-${prompt.sectionIndex}`,
          width: image.width,
          height: image.height,
        })

        // Emit progress
        send.ServiceProgress.update, {
          stage: 'image-generation',
          progress: ((i + 1) / imageCount) * 100,
          message: `Generated ${i + 1} of ${imageCount} images`,
        })
      } catch (error) {
        console.error(`[Images] Failed to generate image ${i + 1}:`, error)
        // Continue with other images
      }
    }

    console.log(`[Images] Generated ${images.length} images`)

    return images
  } catch (error) {
    console.error('[Images] Image generation failed:', error)
    // Non-critical - continue without images
    return []
  }
}

async function uploadImage(imageUrl: string, metadata: any): Promise<any> {
  // In production, upload to your storage service (S3, R2, etc.)
  // This is a simplified example

  const response = await fetch(imageUrl)
  const blob = await response.blob()

  // Simulate upload
  const uploadedUrl = `https://cdn.example.com/images/${metadata.name}.png`

  return {
    url: uploadedUrl,
    ...metadata,
  }
}

Stage 7: Quality Assurance

async function performQualityChecks(inputs: any, content: any, seo: any, config: any) {
  console.log('[QA] Running quality checks...')

  const checks = config.limits.qualityChecks
  const results: any = {
    passed: true,
    score: 0,
    checks: {},
    issues: [],
  }

  try {
    // Readability check
    if (checks.includes('readability')) {
      console.log('[QA] Checking readability...')
      const readability = await ai.evaluate({
        model: 'gpt-5',
        task: 'readability',
        content: content.text,
        target: inputs.audience,
        metrics: ['flesch-reading-ease', 'grade-level', 'sentence-complexity'],
      })

      results.checks.readability = {
        score: readability.score || 65,
        passed: readability.score >= 60,
        details: readability,
      }

      if (!results.checks.readability.passed) {
        results.passed = false
        results.issues.push({
          type: 'readability',
          severity: 'high',
          message: `Readability score ${readability.score} is below minimum (60)`,
          suggestions: readability.suggestions,
        })
      }
    }

    // Grammar check
    if (checks.includes('grammar')) {
      console.log('[QA] Checking grammar...')
      const grammar = await ai.evaluate({
        model: 'gpt-5',
        task: 'grammar',
        content: content.text,
        checkFor: ['spelling', 'grammar', 'punctuation', 'style'],
      })

      results.checks.grammar = {
        errorCount: grammar.errors?.length || 0,
        passed: (grammar.errors?.length || 0) <= 3,
        errors: grammar.errors || [],
      }

      if (!results.checks.grammar.passed) {
        results.passed = false
        results.issues.push({
          type: 'grammar',
          severity: 'medium',
          message: `Found ${grammar.errors.length} grammar errors (max 3)`,
          errors: grammar.errors,
        })
      }
    }

    // SEO check
    if (checks.includes('seo')) {
      console.log('[QA] Checking SEO...')
      results.checks.seo = {
        score: seo.score || 0,
        passed: (seo.score || 0) >= 70,
        improvements: seo.improvements || [],
      }

      if (!results.checks.seo.passed) {
        results.passed = false
        results.issues.push({
          type: 'seo',
          severity: 'medium',
          message: `SEO score ${seo.score} is below minimum (70)`,
          improvements: seo.improvements,
        })
      }
    }

    // Plagiarism check
    if (checks.includes('plagiarism')) {
      console.log('[QA] Checking plagiarism...')
      const plagiarism = await ai.evaluate({
        model: 'gpt-5',
        task: 'plagiarism',
        content: content.text,
        checkAgainst: 'web',
      })

      results.checks.plagiarism = {
        similarity: plagiarism.maxSimilarity || 0,
        passed: (plagiarism.maxSimilarity || 0) <= 0.05,
        matches: plagiarism.matches || [],
      }

      if (!results.checks.plagiarism.passed) {
        results.passed = false
        results.issues.push({
          type: 'plagiarism',
          severity: 'critical',
          message: `Content similarity ${plagiarism.maxSimilarity * 100}% exceeds maximum (5%)`,
          matches: plagiarism.matches,
        })
      }
    }

    // Fact checking (premium only)
    if (checks.includes('factChecking')) {
      console.log('[QA] Fact checking...')
      const factCheck = await ai.evaluate({
        model: 'gpt-5',
        task: 'fact-checking',
        content: content.text,
        verifyStatistics: true,
        verifyClaims: true,
      })

      results.checks.factChecking = {
        verifiedClaims: factCheck.verified || 0,
        unverifiedClaims: factCheck.unverified || 0,
        passed: (factCheck.unverified || 0) === 0,
        issues: factCheck.issues || [],
      }

      if (!results.checks.factChecking.passed) {
        results.passed = false
        results.issues.push({
          type: 'fact-checking',
          severity: 'high',
          message: `Found ${factCheck.unverified} unverified claims`,
          claims: factCheck.issues,
        })
      }
    }

    // Calculate overall score
    const checkScores = Object.values(results.checks)
      .filter((check: any) => typeof check.score === 'number')
      .map((check: any) => check.score)

    results.score = checkScores.length > 0 ? checkScores.reduce((sum, score) => sum + score, 0) / checkScores.length : 100

    console.log(`[QA] Quality score: ${results.score.toFixed(1)}/100`)
    console.log(`[QA] Checks passed: ${results.passed}`)

    // Emit progress
    send.ServiceProgress.update, {
      stage: 'quality-assurance',
      progress: 100,
      message: `Quality score: ${results.score.toFixed(1)}/100`,
    })

    return results
  } catch (error) {
    console.error('[QA] Quality checks failed:', error)
    throw new Error(`Quality checks failed: ${error.message}`)
  }
}

Stage 8: Content Improvement

async function improveContent(content: any, qualityResults: any) {
  console.log('[Improvement] Improving content based on QA feedback...')

  if (qualityResults.passed) {
    console.log('[Improvement] Content passed all checks, no improvement needed')
    return content
  }

  try {
    const improvements = await ai.improve({
      model: 'gpt-5',
      content: content.text,
      issues: qualityResults.issues,
      maintain: {
        wordCount: true,
        keyPoints: true,
        structure: true,
      },
    })

    console.log('[Improvement] Content improved')

    return {
      ...content,
      text: improvements.text,
      improvementsMade: improvements.changes,
    }
  } catch (error) {
    console.error('[Improvement] Content improvement failed:', error)
    // Return original if improvement fails
    return content
  }
}

Main Service Handler

on.ServiceRequest.created, async (request) => {
  if (request.serviceId !== contentService.id) return

  const startTime = Date.now()
  console.log(`[Service] Processing request ${request.id}`)

  try {
    // Extract and validate inputs
    const inputs = request.inputs
    const tier = inputs.tier || 'standard'
    const config = contentService.pricing.tiers.find((t) => t.name === tier)

    if (!config) {
      throw new Error(`Invalid tier: ${tier}`)
    }

    // Validate required inputs
    if (!inputs.topic) {
      throw new Error('Topic is required')
    }

    console.log(`[Service] Starting ${tier} tier blog post generation`)
    console.log(`[Service] Topic: ${inputs.topic}`)

    // Update service status
    await db.update(request, {
      status: 'processing',
      startedAt: new Date(),
    })

    // Stage 1: Keyword Research (15% of work)
    send.ServiceProgress.update, {
      requestId: request.id,
      stage: 'keyword-research',
      progress: 0,
      message: 'Starting keyword research...',
    })

    const keywords = await performKeywordResearch(inputs, config)

    // Stage 2: Competitor Analysis (15% of work)
    send.ServiceProgress.update, {
      requestId: request.id,
      stage: 'competitor-analysis',
      progress: 0,
      message: 'Analyzing competitors...',
    })

    const competitors = await analyzeCompetitors(inputs, keywords, config)

    // Stage 3: Outline Creation (10% of work)
    send.ServiceProgress.update, {
      requestId: request.id,
      stage: 'outline-creation',
      progress: 0,
      message: 'Creating content outline...',
    })

    const outline = await createOutline(inputs, keywords, competitors, config)

    // Stage 4: Content Writing (30% of work)
    send.ServiceProgress.update, {
      requestId: request.id,
      stage: 'content-writing',
      progress: 0,
      message: 'Generating content...',
    })

    let content = await generateContent(inputs, outline, keywords, config)

    // Stage 5: SEO Optimization (10% of work)
    send.ServiceProgress.update, {
      requestId: request.id,
      stage: 'seo-optimization',
      progress: 0,
      message: 'Optimizing for SEO...',
    })

    const seo = await optimizeForSEO(inputs, content, keywords, config)
    content.text = seo.content

    // Stage 6: Image Generation (10% of work)
    send.ServiceProgress.update, {
      requestId: request.id,
      stage: 'image-generation',
      progress: 0,
      message: 'Generating images...',
    })

    const images = await generateImages(inputs, content, outline, config)

    // Stage 7: Quality Assurance (5% of work)
    send.ServiceProgress.update, {
      requestId: request.id,
      stage: 'quality-assurance',
      progress: 0,
      message: 'Running quality checks...',
    })

    const qualityResults = await performQualityChecks(inputs, content, seo, config)

    // Stage 8: Improvement (5% of work, if needed)
    if (!qualityResults.passed) {
      send.ServiceProgress.update, {
        requestId: request.id,
        stage: 'improvement',
        progress: 0,
        message: 'Improving content...',
      })

      content = await improveContent(content, qualityResults)

      // Re-run quality checks
      const retestResults = await performQualityChecks(inputs, content, seo, config)

      if (!retestResults.passed && retestResults.score < 70) {
        throw new Error('Content quality below acceptable threshold after improvements')
      }
    }

    // Calculate execution time
    const executionTime = (Date.now() - startTime) / 1000

    // Prepare final output
    const output = {
      post: {
        title: seo.seo.metaTitle,
        slug: seo.seo.slug,
        content: content.text,
        excerpt: content.excerpt || content.text.substring(0, 200),
        images,
        seo: seo.seo,
      },
      analytics: {
        wordCount: content.wordCount,
        readingTime: Math.ceil(content.wordCount / 200),
        seoScore: seo.score,
        readabilityScore: qualityResults.checks.readability?.score || 0,
        keywordDensity: calculateKeywordDensity(content.text, keywords.primary),
        uniquenessScore: qualityResults.checks.plagiarism ? (1 - qualityResults.checks.plagiarism.similarity) * 100 : 100,
        qualityScore: qualityResults.score,
        executionTime,
      },
      research: {
        keywords: {
          primary: keywords.primary.map((k: any) => k.keyword),
          secondary: keywords.secondary.map((k: any) => k.keyword),
          longtail: keywords.longtail.map((k: any) => k.keyword),
        },
        competitorAnalysis: competitors.map((c: any) => ({
          url: c.url,
          wordCount: c.wordCount,
          keyTopics: c.mainTopics,
          strengths: c.strengths,
          gaps: c.gaps,
        })),
      },
    }

    // Deliver result
    send.ServiceResult.deliver, {
      requestId: request.id,
      outputs: output,
      metadata: {
        tier,
        executionTime,
        qualityScore: qualityResults.score,
      },
    })

    // Update request status
    await db.update(request, {
      status: 'completed',
      completedAt: new Date(),
      executionTime,
    })

    // Charge for service
    send.Payment.charge, {
      customerId: request.customerId,
      amount: config.price,
      currency: 'USD',
      description: `Blog Post (${tier} tier) - ${output.post.title}`,
      metadata: {
        requestId: request.id,
        serviceId: contentService.id,
        tier,
        wordCount: content.wordCount,
      },
    })

    // Track metrics
    send.Metrics.track, {
      service: 'content-generation',
      metrics: {
        execution_time: executionTime,
        word_count: content.wordCount,
        seo_score: seo.score,
        quality_score: qualityResults.score,
        images_generated: images.length,
        tier,
      },
    })

    console.log(`[Service] Request ${request.id} completed in ${executionTime}s`)
    console.log(`[Service] Quality score: ${qualityResults.score.toFixed(1)}/100`)
  } catch (error) {
    console.error(`[Service] Request ${request.id} failed:`, error)

    // Update request status
    await db.update(request, {
      status: 'failed',
      failedAt: new Date(),
      error: error.message,
    })

    // Notify of failure
    send.ServiceRequest.fail, {
      requestId: request.id,
      error: error.message,
      retryable: !error.message.includes('Invalid'),
    })

    // Track error
    send.Metrics.track, {
      service: 'content-generation',
      event: 'error',
      error: {
        message: error.message,
        stage: error.stage || 'unknown',
      },
    })
  }
})

function calculateKeywordDensity(text: string, keywords: any[]): number {
  const totalWords = text.split(/\s+/).length
  let keywordOccurrences = 0

  keywords.forEach((k: any) => {
    const regex = new RegExp(k.keyword, 'gi')
    const matches = text.match(regex)
    keywordOccurrences += matches ? matches.length : 0
  })

  return (keywordOccurrences / totalWords) * 100
}

Testing

Unit Tests

import { describe, it, expect, beforeEach } from 'vitest'
import $, { ai, db, on, send } from 'sdk.do'

describe('Content Generation Service', () => {
  describe('Keyword Research', () => {
    it('should generate relevant keywords', async () => {
      const inputs = {
        topic: 'AI Content Marketing',
        industry: 'technology',
      }

      const config = {
        limits: { maxKeywords: 5 },
      }

      const keywords = await performKeywordResearch(inputs, config)

      expect(keywords.primary).toBeDefined()
      expect(keywords.primary.length).toBeGreaterThan(0)
      expect(keywords.primary[0].keyword).toContain('ai')
    })

    it('should respect keyword limits', async () => {
      const inputs = { topic: 'Test Topic' }
      const config = { limits: { maxKeywords: 3 } }

      const keywords = await performKeywordResearch(inputs, config)

      expect(keywords.primary.length).toBeLessThanOrEqual(3)
    })
  })

  describe('Content Generation', () => {
    it('should generate content meeting word count', async () => {
      const inputs = { topic: 'Test Topic', tone: 'professional' }
      const outline = {
        sections: [
          { title: 'Introduction', points: ['Point 1', 'Point 2'] },
          { title: 'Main Content', points: ['Point 3', 'Point 4'] },
        ],
      }
      const keywords = {
        primary: [{ keyword: 'test' }],
        secondary: [{ keyword: 'example' }],
      }
      const config = {
        features: { wordCount: 1000 },
      }

      const content = await generateContent(inputs, outline, keywords, config)

      expect(content.wordCount).toBeGreaterThan(800) // 80% of target
      expect(content.text).toContain('test')
    })
  })

  describe('SEO Optimization', () => {
    it('should generate valid meta tags', async () => {
      const inputs = { topic: 'Test Article' }
      const content = {
        text: 'Test content with keywords and structure.',
        excerpt: 'Test excerpt',
      }
      const keywords = {
        primary: [{ keyword: 'test keyword' }],
      }
      const config = {
        features: { seoOptimization: 'advanced' },
      }

      const seo = await optimizeForSEO(inputs, content, keywords, config)

      expect(seo.seo.metaTitle).toBeDefined()
      expect(seo.seo.metaTitle.length).toBeLessThanOrEqual(60)
      expect(seo.seo.metaDescription).toBeDefined()
      expect(seo.seo.metaDescription.length).toBeLessThanOrEqual(160)
      expect(seo.seo.slug).toBeDefined()
    })

    it('should calculate SEO score correctly', async () => {
      const optimized = {
        metaTitle: 'Perfect Title Length With Keywords',
        metaDescription: 'Perfect meta description length with relevant information about the topic and including target keywords appropriately.',
        keywordPlacement: {
          title: true,
          firstParagraph: true,
          headers: true,
        },
        headerHierarchy: { valid: true },
        internalLinks: 5,
        readability: 75,
      }

      const score = calculateSEOScore(optimized, {}, {})

      expect(score).toBeGreaterThan(80)
      expect(score).toBeLessThanOrEqual(100)
    })
  })

  describe('Quality Assurance', () => {
    it('should pass quality checks for good content', async () => {
      const inputs = { topic: 'Test', audience: 'general' }
      const content = {
        text: 'Well-written content with good readability and proper grammar.',
        wordCount: 1000,
      }
      const seo = { score: 85 }
      const config = {
        limits: {
          qualityChecks: ['readability', 'grammar', 'seo'],
        },
      }

      const results = await performQualityChecks(inputs, content, seo, config)

      expect(results.passed).toBe(true)
      expect(results.score).toBeGreaterThan(70)
    })

    it('should fail quality checks for poor content', async () => {
      const inputs = { topic: 'Test', audience: 'general' }
      const content = {
        text: 'Bad content.', // Too short, poor quality
        wordCount: 100,
      }
      const seo = { score: 45 }
      const config = {
        limits: {
          qualityChecks: ['readability', 'grammar', 'seo'],
        },
      }

      const results = await performQualityChecks(inputs, content, seo, config)

      expect(results.passed).toBe(false)
      expect(results.issues.length).toBeGreaterThan(0)
    })
  })
})

Integration Tests

describe('Content Service Integration', () => {
  it('should complete full workflow for basic tier', async () => {
    const request = send.ServiceRequest.create, {
      serviceId: contentService.id,
      customerId: 'test-customer',
      inputs: {
        topic: 'The Future of AI in Marketing',
        tier: 'basic',
        industry: 'marketing',
        tone: 'professional',
      },
    })

    // Wait for completion (with timeout)
    const result = await waitForResult(request.id, 300000) // 5 min timeout

    expect(result.outputs.post).toBeDefined()
    expect(result.outputs.post.content).toBeDefined()
    expect(result.outputs.analytics.wordCount).toBeGreaterThan(640) // 80% of 800
    expect(result.outputs.analytics.qualityScore).toBeGreaterThan(70)
  })

  it('should handle errors gracefully', async () => {
    const request = send.ServiceRequest.create, {
      serviceId: contentService.id,
      customerId: 'test-customer',
      inputs: {
        // Missing required field
        tier: 'standard',
      },
    })

    const result = await waitForResult(request.id, 30000)

    expect(result.status).toBe('failed')
    expect(result.error).toContain('Topic is required')
  })
})

async function waitForResult(requestId: string, timeout: number): Promise<any> {
  return new Promise((resolve, reject) => {
    const timeoutId = setTimeout(() => {
      reject(new Error('Timeout waiting for result'))
    }, timeout)

    on.ServiceResult.delivered, (result) => {
      if (result.requestId === requestId) {
        clearTimeout(timeoutId)
        resolve(result)
      }
    })

    on.ServiceRequest.fail, (failure) => {
      if (failure.requestId === requestId) {
        clearTimeout(timeoutId)
        resolve(failure)
      }
    })
  })
}

Deployment

Environment Configuration

// config/production.ts
export default {
  service: {
    id: contentService.id,
    environment: 'production',
    region: 'us-east-1',
  },

  aiModels: {
    text: 'gpt-5',
    image: 'dalle-3',
    fallback: 'claude-sonnet-4.5',
  },

  storage: {
    provider: 'r2',
    bucket: 'content-service-assets',
    cdn: 'https://cdn.example.com',
  },

  monitoring: {
    provider: 'datadog',
    apiKey: process.env.DATADOG_API_KEY,
    logLevel: 'info',
  },

  alerting: {
    channels: ['slack', 'pagerduty'],
    slack: {
      webhook: process.env.SLACK_WEBHOOK,
      channel: '#content-service-alerts',
    },
    pagerduty: {
      integrationKey: process.env.PAGERDUTY_KEY,
    },
  },

  rateLimit: {
    perCustomer: {
      requests: 100,
      window: '1h',
    },
    perIP: {
      requests: 10,
      window: '1m',
    },
  },
}

Monitoring Setup

// Setup metrics collection
on.ServiceRequest.created, async (request) => {
  send.Metrics.increment, {
    metric: 'content_service.requests.created',
    tags: {
      tier: request.inputs.tier,
      customer: request.customerId,
    },
  })
})

on.ServiceResult.delivered, async (result) => {
  send.Metrics.record, {
    metric: 'content_service.execution_time',
    value: result.metadata.executionTime,
    tags: {
      tier: result.metadata.tier,
    },
  })

  send.Metrics.record, {
    metric: 'content_service.quality_score',
    value: result.metadata.qualityScore,
    tags: {
      tier: result.metadata.tier,
    },
  })
})

on.ServiceRequest.fail, async (failure) => {
  send.Metrics.increment, {
    metric: 'content_service.errors',
    tags: {
      error_type: failure.error,
      stage: failure.stage,
    },
  })

  // Alert if error rate too high
  const errorRate = await getErrorRate('5m')
  if (errorRate > 0.05) {
    // 5%
    send.Alert.create, {
      severity: 'critical',
      title: 'High error rate in content service',
      message: `Error rate: ${(errorRate * 100).toFixed(2)}%`,
      channels: ['slack', 'pagerduty'],
    })
  }
})

Health Checks

// Health check endpoint
on.Health.check, async (check) => {
  if (check.service !== 'content-generation') return

  const health = {
    status: 'healthy',
    checks: {},
  }

  try {
    // Check AI models
    const aiTest = await ai.generate({
      model: 'gpt-5',
      prompt: 'test',
      timeout: 5000,
    })
    health.checks.ai = { status: 'healthy', latency: aiTest.latency }
  } catch (error) {
    health.status = 'degraded'
    health.checks.ai = { status: 'unhealthy', error: error.message }
  }

  try {
    // Check database
    const dbTest = await db.query($.Service, { limit: 1 })
    health.checks.database = { status: 'healthy' }
  } catch (error) {
    health.status = 'unhealthy'
    health.checks.database = { status: 'unhealthy', error: error.message }
  }

  send.Health.report, health)
})

Usage Examples

Example 1: Basic Blog Post

const request = send.ServiceRequest.create, {
  serviceId: contentService.id,
  customerId: 'customer-123',
  inputs: {
    topic: '10 Tips for Better Email Marketing',
    tier: 'basic',
    industry: 'marketing',
    tone: 'casual',
    style: 'listicle',
  },
})

Example 2: In-Depth Technical Guide

const request = send.ServiceRequest.create, {
  serviceId: contentService.id,
  customerId: 'customer-456',
  inputs: {
    topic: 'Complete Guide to GraphQL API Design',
    tier: 'premium',
    industry: 'technology',
    audience: 'Backend developers',
    tone: 'technical',
    style: 'guide',
    targetKeywords: ['graphql', 'api design', 'graphql best practices'],
    competitorUrls: ['https://example.com/graphql-guide-1', 'https://example.com/graphql-guide-2'],
    internalLinks: [
      { text: 'API Authentication', url: '/docs/authentication' },
      { text: 'REST vs GraphQL', url: '/blog/rest-vs-graphql' },
    ],
    cta: {
      text: 'Try our GraphQL API',
      url: 'https://example.com/signup',
    },
  },
})

Example 3: SEO-Optimized Article

const request = send.ServiceRequest.create, {
  serviceId: contentService.id,
  customerId: 'customer-789',
  inputs: {
    topic: 'How to Choose the Right CRM for Your Business',
    tier: 'standard',
    industry: 'sales',
    audience: 'Small business owners',
    tone: 'conversational',
    style: 'guide',
    targetKeywords: ['crm software', 'best crm', 'crm for small business'],
    competitorUrls: ['https://competitor1.com/crm-guide', 'https://competitor2.com/choosing-crm', 'https://competitor3.com/crm-comparison'],
  },
})

Troubleshooting

Low Quality Scores

Problem: Content consistently scores below 70%.

Solutions:

  1. Review AI model performance - may need to switch models
  2. Check if quality criteria are too strict
  3. Add more context in inputs (audience, industry)
  4. Enable content improvement stage
  5. Review and update prompt templates
// Monitor quality scores
on.ServiceResult.delivered, async (result) => {
  if (result.outputs.analytics.qualityScore < 70) {
    send.Alert.create, {
      severity: 'warning',
      title: 'Low quality score',
      message: `Request ${result.requestId} scored ${result.outputs.analytics.qualityScore}`,
      metadata: result.outputs.analytics,
    })
  }
})

Slow Execution Times

Problem: Service takes longer than SLA.

Solutions:

  1. Parallelize independent stages (keywords + competitors)
  2. Cache frequently used resources
  3. Optimize AI model parameters
  4. Use faster models for non-critical stages
  5. Implement request queuing for high load
// Parallel execution example
const [keywords, competitors] = await Promise.all([performKeywordResearch(inputs, config), analyzeCompetitors(inputs, { primary: [] }, config)])

High Costs

Problem: AI costs exceed revenue.

Solutions:

  1. Optimize token usage in prompts
  2. Use cheaper models for initial drafts
  3. Implement caching for repeated content
  4. Adjust pricing tiers
  5. Set token limits per tier
// Track costs per request
on.ServiceResult.delivered, async (result) => {
  const costs = {
    aiTokens: result.metadata.tokensUsed * 0.00001,
    images: result.outputs.post.images.length * 0.04,
    storage: 0.01,
  }

  const totalCost = Object.values(costs).reduce((a, b) => a + b, 0)
  const revenue = result.metadata.tier === 'premium' ? 99 : 49
  const margin = revenue - totalCost

  send.Metrics.record, {
    metric: 'content_service.profit_margin',
    value: margin,
    tags: { tier: result.metadata.tier },
  })
})

Next Steps

Enhancements to Consider

  1. A/B Testing - Generate multiple versions for testing
  2. Multi-Language - Support content in multiple languages
  3. Voice/Tone Cloning - Match brand voice from examples
  4. Automated Publishing - Direct CMS integration
  5. Performance Tracking - Monitor published content performance
  6. Revision Workflows - Human-in-the-loop for revisions
  7. Batch Processing - Generate multiple posts efficiently
  8. Custom Templates - Customer-specific content templates

Total Lines: 1,800+ Total Examples: 15+ Word Count: 5,200+

This complete example demonstrates a production-ready content generation service with all necessary components for real-world deployment.