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:
- Review AI model performance - may need to switch models
- Check if quality criteria are too strict
- Add more context in inputs (audience, industry)
- Enable content improvement stage
- 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:
- Parallelize independent stages (keywords + competitors)
- Cache frequently used resources
- Optimize AI model parameters
- Use faster models for non-critical stages
- 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:
- Optimize token usage in prompts
- Use cheaper models for initial drafts
- Implement caching for repeated content
- Adjust pricing tiers
- 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
- A/B Testing - Generate multiple versions for testing
- Multi-Language - Support content in multiple languages
- Voice/Tone Cloning - Match brand voice from examples
- Automated Publishing - Direct CMS integration
- Performance Tracking - Monitor published content performance
- Revision Workflows - Human-in-the-loop for revisions
- Batch Processing - Generate multiple posts efficiently
- Custom Templates - Customer-specific content templates
Related Documentation
- Building Services → - Detailed service development guide
- API Reference → - Complete API documentation
- Monetization → - Pricing strategies
- Best Practices → - Production guidelines
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.