How I built a production-ready AI application that generates SEO-optimized blog posts in 30 seconds
Why I Built This
I'll be honest with you - I've spent countless hours staring at blank screens, struggling to write blog posts. As a developer who loves building things but finds writing challenging, I often thought: "What if I could generate high-quality blog drafts in seconds?"
That question led me down a rabbit hole that resulted in building a complete AI-powered blog generator from scratch. This isn't just another AI wrapper - it's a full-stack application with custom prompt engineering, SEO optimization, and a beautiful user interface.
What we're building:
This is Part 1 of a two-part series. Today, we'll dive deep into the development process. Part 2 will cover deployment to production.
GitHub Repository
Tech Stack: Why These Choices?
Let me walk you through my technical decisions and the reasoning behind them.
Backend: FastAPI
I chose FastAPI over Flask or Django for several compelling reasons:
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
# Type-safe, auto-documented, and blazing fast
class BlogRequest(BaseModel):
topic: str
tone: str = "professional"
length: str = "medium"
keywords: str | None = None
@app.post("/generate")
async def generate_blog(request: BlogRequest):
# FastAPI handles validation automatically
return await ai_service.generate(request)
Why FastAPI?
Frontend: React + Vite + Tailwind
React for the component-based architecture and ecosystem.
Vite instead of Create React App because:
Tailwind CSS for rapid, maintainable styling:
// Clean, utility-first styling
Generate Blog Post
AI: Hugging Face Transformers
I chose Hugging Face over OpenAI for several reasons:
Architecture: How It All Fits Together
Here's the high-level architecture:
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ │ │ │ │ │
│ React Frontend │◄───────►│ FastAPI Backend │◄───────►│ PostgreSQL │
│ (Port 5173) │ HTTP │ (Port 8000) │ ORM │ Database │
│ │ │ │ │ │
└─────────────────┘ └──────────────────┘ └─────────────────┘
│
│ HTTPS
▼
┌──────────────────┐
│ Hugging Face │
│ Inference API │
│ (Llama 3.3 70B) │
└──────────────────┘
Data Flow:
Backend Deep Dive
Project Structure
I organized the backend following a clean architecture pattern:
backend/
├── app/
│ ├── main.py # FastAPI app initialization
│ ├── config.py # Environment configuration
│ ├── database.py # Database connection
│ ├── models/
│ │ └── blog.py # SQLAlchemy models
│ ├── schemas/
│ │ └── blog.py # Pydantic schemas
│ ├── api/
│ │ └── routes.py # API endpoints
│ ├── services/
│ │ ├── ai_service.py # Hugging Face integration
│ │ └── seo_service.py # SEO scoring logic
│ └── utils/
│ └── post_processor.py # Content processing
└── requirements.txt

1. Database Models with SQLAlchemy
First, I defined the database schema for storing blog posts:
# app/models/blog.py
from sqlalchemy import Column, Integer, String, Text, DateTime, Float
from sqlalchemy.sql import func
from app.database import Base
class BlogPost(Base):
__tablename__ = "blog_posts"
id = Column(Integer, primary_key=True, index=True)
topic = Column(String(500), nullable=False, index=True)
tone = Column(String(50), nullable=False)
length = Column(String(20), nullable=False)
keywords = Column(Text, nullable=True)
title = Column(String(500), nullable=True)
content = Column(Text, nullable=False)
seo_score = Column(Float, nullable=True)
word_count = Column(Integer, nullable=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
Why this structure?
2. Pydantic Schemas for Validation
Type-safe request/response models:
# app/schemas/blog.py
from pydantic import BaseModel, Field
from typing import Optional
class BlogGenerateRequest(BaseModel):
topic: str = Field(..., min_length=5, max_length=500)
tone: str = Field(default="professional")
length: str = Field(default="medium")
keywords: Optional[str] = Field(None)
class Config:
json_schema_extra = {
"example": {
"topic": "The Future of AI",
"tone": "professional",
"length": "medium",
"keywords": "AI, machine learning, automation"
}
}
class BlogResponse(BaseModel):
id: int
topic: str
title: str
content: str
seo_score: float
word_count: int
created_at: datetime
class Config:
from_attributes = True # Enable ORM mode
The beauty of Pydantic:
3. AI Service: The Heart of the Application
This is where the magic happens. Here's my Hugging Face integration:
# app/services/ai_service.py
import requests
import time
from typing import Dict
class HuggingFaceService:
def __init__(self):
self.api_url = "https://router.huggingface.co/v1/chat/completions"
self.headers = {
"Authorization": f"Bearer {settings.HUGGINGFACE_API_KEY}",
"Content-Type": "application/json"
}
def generate_blog(self, topic: str, tone: str, length: str,
keywords: str = None) -> Dict:
"""Main entry point for blog generation"""
# 1. Build optimized prompt
prompt = self._build_prompt(topic, tone, length, keywords)
# 2. Call Hugging Face API
raw_content = self._call_api(prompt)
# 3. Post-process content
cleaned = self.post_processor.clean_content(raw_content)
title, content = self.post_processor.extract_title_and_content(
cleaned, topic
)
# 4. Calculate metrics
word_count = self.post_processor.count_words(content)
seo_score = self.seo_service.calculate_score(
content, title, keywords
)
return {
"title": title,
"content": content,
"word_count": word_count,
"seo_score": seo_score
}
Prompt Engineering: The Secret Sauce
Building effective prompts was crucial. Here's my approach:
def _build_prompt(self, topic: str, tone: str, length: str,
keywords: str = None) -> str:
"""Construct optimized prompt for blog generation"""
tone_map = {
"professional": "authoritative, polished, business-appropriate",
"casual": "friendly, conversational, relatable",
"technical": "detailed, precise, technically accurate",
"educational": "clear, informative, easy to understand"
}
length_map = {
"short": "600-800 words",
"medium": "1000-1500 words",
"long": "1800-2500 words"
}
return f"""You are an expert blog writer. Write a comprehensive blog post.
**Topic:** {topic}
**Tone:** {tone_map.get(tone)}
**Target Length:** {length_map.get(length)}
{f"**Keywords:** {keywords}" if keywords else ""}
**Requirements:**
1. Create an engaging, SEO-friendly title
2. Write a compelling introduction
3. Use clear subheadings (## for H2)
4. Include practical examples
5. End with a strong conclusion
**Format:**
# [Title]
[Introduction]
## [Section 1]
[Content]
## [Section 2]
[Content]
## Conclusion
[Summary]
Write the blog post now:"""
Why this prompt works:

API Call with Retry Logic
Hugging Face models can take time to load (503 errors). Here's my retry logic:
def _call_api(self, prompt: str, max_retries: int = 3) -> str:
"""Call HF API with retry logic"""
payload = {
"model": "meta-llama/Llama-3.3-70B-Instruct",
"messages": [
{
"role": "system",
"content": "You are an expert blog writer."
},
{
"role": "user",
"content": prompt
}
],
"max_tokens": 2500,
"temperature": 0.7,
"top_p": 0.9
}
for attempt in range(max_retries):
try:
response = requests.post(
self.api_url,
headers=self.headers,
json=payload,
timeout=120 # 2 minute timeout
)
# Model loading, wait and retry
if response.status_code == 503:
print(f"Model loading... waiting 30 seconds")
time.sleep(30)
continue
response.raise_for_status()
result = response.json()
return result["choices"][0]["message"]["content"]
except requests.exceptions.Timeout:
if attempt < max_retries - 1:
time.sleep(10)
continue
raise Exception("Request timed out")
raise Exception("Failed after multiple retries")
Key points:
4. SEO Scoring Algorithm
I built a custom SEO scoring system that evaluates multiple factors:
# app/services/seo_service.py
import re
class SEOService:
@staticmethod
def calculate_score(content: str, title: str,
keywords: str = None) -> float:
"""Calculate SEO score (0-100)"""
score = 0.0
# 1. Word Count Analysis (25 points)
word_count = len(re.findall(r'\b\w+\b', content))
if 800 <= word_count <= 2500:
score += 25
elif 600 <= word_count < 800:
score += 18
elif word_count >= 500:
score += 12
# 2. Title Optimization (15 points)
if title:
title_length = len(title)
if 40 <= title_length <= 70:
score += 15
elif 30 <= title_length < 40:
score += 10
elif title_length > 0:
score += 5
# 3. Heading Structure (20 points)
h2_count = len(re.findall(r'##\s+', content))
h3_count = len(re.findall(r'###\s+', content))
total_headings = h2_count + h3_count
if 3 <= total_headings <= 8:
score += 20
elif 2 <= total_headings <= 10:
score += 15
elif total_headings > 0:
score += 8
# 4. Keyword Optimization (25 points)
if keywords:
keyword_list = [k.strip().lower()
for k in keywords.split(',')]
content_lower = content.lower()
title_lower = title.lower() if title else ""
# Keywords in content
keywords_in_content = sum(
1 for kw in keyword_list if kw in content_lower
)
if keywords_in_content >= len(keyword_list):
score += 15
elif keywords_in_content > 0:
score += 8
# Keywords in title
keywords_in_title = sum(
1 for kw in keyword_list if kw in title_lower
)
if keywords_in_title > 0:
score += 10
else:
score += 12 # Partial credit
# 5. Content Structure (15 points)
has_intro = bool(re.search(
r'(introduction|overview)',
content.lower()[:500]
))
has_conclusion = bool(re.search(
r'(conclusion|summary|final)',
content.lower()[-500:]
))
if has_intro:
score += 7
if has_conclusion:
score += 8
return min(round(score, 2), 100.0)
Scoring breakdown:
This gives us a fair, multi-dimensional quality score.
5. Post-Processing Pipeline
Raw AI output needs cleaning. Here's my post-processor:
# app/utils/post_processor.py
import re
from typing import Tuple
class PostProcessor:
@staticmethod
def clean_content(text: str) -> str:
"""Clean and format generated text"""
if not text:
return ""
# Remove excessive whitespace
text = re.sub(r'\n{3,}', '\n\n', text)
text = re.sub(r' {2,}', ' ', text)
return text.strip()
@staticmethod
def extract_title_and_content(text: str,
fallback_topic: str) -> Tuple[str, str]:
"""Extract title from content"""
if not text:
return f"{fallback_topic}: A Comprehensive Guide", ""
lines = text.split('\n')
title = None
content_lines = []
for line in lines:
line = line.strip()
# Look for title (lines starting with #)
if line.startswith('# ') and not title:
title = line.replace('# ', '').strip()
elif line:
content_lines.append(line)
# Fallback title if none found
if not title:
title = f"{fallback_topic}: A Comprehensive Guide"
content = '\n\n'.join(content_lines) if content_lines else text
return title, content
@staticmethod
def count_words(text: str) -> int:
"""Count words accurately"""
if not text:
return 0
words = re.findall(r'\b\w+\b', text)
return len(words)
Why post-processing matters:
6. API Routes: Putting It All Together
Finally, the API endpoints that tie everything together:
# app/api/routes.py
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
router = APIRouter()
@router.post("/generate", response_model=BlogResponse, status_code=201)
def generate_blog(
request: BlogGenerateRequest,
db: Session = Depends(get_db)
):
"""Generate a new blog post using AI"""
try:
# Generate content
result = hf_service.generate_blog(
topic=request.topic,
tone=request.tone,
length=request.length,
keywords=request.keywords
)
# Save to database
blog_post = BlogPost(
topic=request.topic,
tone=request.tone,
length=request.length,
keywords=request.keywords,
title=result["title"],
content=result["content"],
word_count=result["word_count"],
seo_score=result["seo_score"]
)
db.add(blog_post)
db.commit()
db.refresh(blog_post)
return blog_post
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=str(e))
@router.get("/blogs", response_model=BlogListResponse)
def list_blogs(
skip: int = 0,
limit: int = 20,
db: Session = Depends(get_db)
):
"""Get list of all generated blogs"""
total = db.query(BlogPost).count()
blogs = (
db.query(BlogPost)
.order_by(BlogPost.created_at.desc())
.offset(skip)
.limit(limit)
.all()
)
return {"total": total, "blogs": blogs}
Key features:

Frontend Deep Dive
React Component Architecture
I structured the frontend with three main components:
src/
├── components/
│ ├── BlogForm.jsx # Input form
│ ├── BlogDisplay.jsx # Content display
│ └── BlogHistory.jsx # Blog list
├── services/
│ └── api.js # API client
├── utils/
│ └── helpers.js # Utility functions
└── App.jsx
1. BlogForm Component
The input form with validation and user-friendly controls:
// src/components/BlogForm.jsx
import React, { useState } from 'react';
import { Sparkles, Loader2 } from 'lucide-react';
const BlogForm = ({ onGenerate, isGenerating }) => {
const [formData, setFormData] = useState({
topic: '',
tone: 'professional',
length: 'medium',
keywords: '',
});
const [errors, setErrors] = useState({});
const toneOptions = [
{ value: 'professional', label: 'Professional', emoji: '💼' },
{ value: 'casual', label: 'Casual', emoji: '😊' },
{ value: 'technical', label: 'Technical', emoji: '🔧' },
{ value: 'educational', label: 'Educational', emoji: '📚' },
];
const validate = () => {
const newErrors = {};
if (!formData.topic.trim()) {
newErrors.topic = 'Topic is required';
} else if (formData.topic.length < 5) {
newErrors.topic = 'Topic must be at least 5 characters';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = (e) => {
e.preventDefault();
if (validate()) {
onGenerate(formData);
}
};
return (
Generate Blog Post
{/* Topic Input */}
Blog Topic *
<input
type="text"
value={formData.topic}
onChange={(e) => setFormData({
...formData,
topic: e.target.value
})}
placeholder="e.g., The Future of AI"
className="input-field"
disabled={isGenerating}
/>
{errors.topic && (
{errors.topic}
)}
{/* Tone Selection */}
Writing Tone
{toneOptions.map((option) => (
<button
key={option.value}
type="button"
onClick={() => setFormData({
...formData,
tone: option.value
})}
className={`p-3 rounded-lg border-2 ${
formData.tone === option.value
? 'border-primary-500 bg-primary-50'
: 'border-gray-200'
}`}
>
{option.emoji}
{option.label}
))}
{/* Submit Button */}
{isGenerating ? (
<>
Generating Content...
) : (
<>
Generate Blog Post
)}
);
};
export default BlogForm;
UI Features:
2. API Integration
Clean API client using Axios:
// src/services/api.js
import axios from 'axios';
const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000';
const api = axios.create({
baseURL: `${API_BASE_URL}/api/v1`,
headers: {
'Content-Type': 'application/json',
},
timeout: 120000, // 2 minutes for AI generation
});
export const blogAPI = {
generateBlog: async (data) => {
const response = await api.post('/generate', data);
return response.data;
},
getAllBlogs: async (skip = 0, limit = 20) => {
const response = await api.get(`/blogs?skip=${skip}&limit=${limit}`);
return response.data;
},
getBlog: async (id) => {
const response = await api.get(`/blogs/${id}`);
return response.data;
},
deleteBlog: async (id) => {
const response = await api.delete(`/blogs/${id}`);
return response.data;
},
};
Why this structure:
3. Utility Functions
Helper functions for common tasks:
// src/utils/helpers.js
// Copy to clipboard
export const copyToClipboard = async (text) => {
try {
await navigator.clipboard.writeText(text);
return true;
} catch (err) {
console.error('Failed to copy:', err);
return false;
}
};
// Download as file
export const downloadAsFile = (content, filename, type = 'text/markdown') => {
const blob = new Blob([content], { type });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
};
// Get SEO score color
export const getSEOScoreColor = (score) => {
if (score >= 80) return 'text-green-600 bg-green-100';
if (score >= 60) return 'text-yellow-600 bg-yellow-100';
return 'text-red-600 bg-red-100';
};
// Format date
export const formatDate = (dateString) => {
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
};

Challenges & Solutions
Challenge 1: AI Response Consistency
Problem: The AI sometimes generated content in inconsistent formats, making it hard to extract titles and structure.
Solution: I built a robust post-processing pipeline:
# Fallback chain for title extraction
if line.startswith('# '):
title = line.replace('# ', '')
elif first_line and len(first_line) < 100:
title = first_line
else:
title = f"{topic}: A Comprehensive Guide"
Challenge 2: Long API Response Times
Problem: AI generation takes 20-60 seconds, which feels like forever without feedback.
Solution: Multiple UX improvements:
{isGenerating ? (
<>
Generating Content...
) : (
<>
Generate Blog Post
)}
Challenge 3: Model 503 Errors
Problem: Hugging Face models go to sleep after inactivity, returning 503 errors on first request.
Solution: Implemented retry logic with exponential backoff:
if response.status_code == 503:
print("Model loading... waiting 30 seconds")
time.sleep(30)
continue # Retry
This handles the warm-up period gracefully.
Challenge 4: SEO Score Fairness
Problem: Creating a fair, comprehensive scoring algorithm that doesn't over-penalize or over-reward.
Solution: Multi-factor analysis with balanced weights:
Each factor is independently scored and summed to 100.
Challenge 5: Database Schema Design
Problem: How to store flexible keyword lists and maintain efficient queries?
Solution:
Current Status & Demo
Here's what we have working right now:
Backend:
Frontend:
Features:

Try it yourself:
# Clone the repo
git clone https://github.com/yourusername/ai-blog-generator
# Backend
cd backend
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt
uvicorn app.main:app --reload
# Frontend
cd frontend
npm install
npm run dev

What I Learned
Technical Lessons
Soft Skills
🚀 What's Next (Part 2)
In the next post, I'll cover:
Key Takeaways
If you're building an AI-powered application, here are my top recommendations:
### Resources & Links
Let's Connect
Building this project was an incredible learning experience. I'd love to hear your thoughts:
Drop a comment below or connect with me:
📢 ### Stay Tuned for Part 2!
Next up: Deployment to Production
I'll walk through:
Make sure to follow me to get notified when Part 2 drops!
Thanks for reading! If you found this helpful, please give it a ❤️ and share with others who might benefit from it.
More...
Why I Built This
I'll be honest with you - I've spent countless hours staring at blank screens, struggling to write blog posts. As a developer who loves building things but finds writing challenging, I often thought: "What if I could generate high-quality blog drafts in seconds?"
That question led me down a rabbit hole that resulted in building a complete AI-powered blog generator from scratch. This isn't just another AI wrapper - it's a full-stack application with custom prompt engineering, SEO optimization, and a beautiful user interface.
What we're building:
- AI-powered blog generation (600-2500 words)
- Multiple writing tones (Professional, Casual, Technical, Educational)
- Automatic SEO scoring and keyword optimization
- PostgreSQL database for content management
- Beautiful, responsive React UI with Tailwind CSS
This is Part 1 of a two-part series. Today, we'll dive deep into the development process. Part 2 will cover deployment to production.
GitHub Repository
Tech Stack: Why These Choices?
Let me walk you through my technical decisions and the reasoning behind them.
Backend: FastAPI
I chose FastAPI over Flask or Django for several compelling reasons:
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
# Type-safe, auto-documented, and blazing fast
class BlogRequest(BaseModel):
topic: str
tone: str = "professional"
length: str = "medium"
keywords: str | None = None
@app.post("/generate")
async def generate_blog(request: BlogRequest):
# FastAPI handles validation automatically
return await ai_service.generate(request)
Why FastAPI?
- Performance: Built on Starlette, it's one of the fastest Python frameworks
- Auto Docs: Interactive API documentation (Swagger UI) out of the box
- Type Safety: Pydantic validation catches errors before they happen
- Modern: Native async/await support, perfect for AI API calls
- Developer Experience: Clear error messages and intuitive API design
Frontend: React + Vite + Tailwind
React for the component-based architecture and ecosystem.
Vite instead of Create React App because:
- Instant server start
- Lightning-fast hot module replacement
- Optimized production build
Tailwind CSS for rapid, maintainable styling:
// Clean, utility-first styling
Generate Blog Post
AI: Hugging Face Transformers
I chose Hugging Face over OpenAI for several reasons:
- Cost: More affordable for experimentation
- Open Models: Access to open-source models like Llama
- Control: Full control over model selection and parameters
- Transparency: Clear understanding of what's happening under the hood
Architecture: How It All Fits Together
Here's the high-level architecture:
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ │ │ │ │ │
│ React Frontend │◄───────►│ FastAPI Backend │◄───────►│ PostgreSQL │
│ (Port 5173) │ HTTP │ (Port 8000) │ ORM │ Database │
│ │ │ │ │ │
└─────────────────┘ └──────────────────┘ └─────────────────┘
│
│ HTTPS
▼
┌──────────────────┐
│ Hugging Face │
│ Inference API │
│ (Llama 3.3 70B) │
└──────────────────┘
Data Flow:
- User fills form → Frontend validation
- POST request to /api/v1/generate → Backend receives data
- Backend builds optimized prompt → Calls Hugging Face API
- AI generates content → Backend processes and cleans output
- Calculate SEO score → Save to PostgreSQL
- Return formatted blog → Frontend displays result
Backend Deep Dive
Project Structure
I organized the backend following a clean architecture pattern:
backend/
├── app/
│ ├── main.py # FastAPI app initialization
│ ├── config.py # Environment configuration
│ ├── database.py # Database connection
│ ├── models/
│ │ └── blog.py # SQLAlchemy models
│ ├── schemas/
│ │ └── blog.py # Pydantic schemas
│ ├── api/
│ │ └── routes.py # API endpoints
│ ├── services/
│ │ ├── ai_service.py # Hugging Face integration
│ │ └── seo_service.py # SEO scoring logic
│ └── utils/
│ └── post_processor.py # Content processing
└── requirements.txt

1. Database Models with SQLAlchemy
First, I defined the database schema for storing blog posts:
# app/models/blog.py
from sqlalchemy import Column, Integer, String, Text, DateTime, Float
from sqlalchemy.sql import func
from app.database import Base
class BlogPost(Base):
__tablename__ = "blog_posts"
id = Column(Integer, primary_key=True, index=True)
topic = Column(String(500), nullable=False, index=True)
tone = Column(String(50), nullable=False)
length = Column(String(20), nullable=False)
keywords = Column(Text, nullable=True)
title = Column(String(500), nullable=True)
content = Column(Text, nullable=False)
seo_score = Column(Float, nullable=True)
word_count = Column(Integer, nullable=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
Why this structure?
- Indexed fields (topic, id) for faster queries
- Flexible keywords as Text for comma-separated values
- Timestamps for tracking creation
- SEO score as Float for decimal precision
2. Pydantic Schemas for Validation
Type-safe request/response models:
# app/schemas/blog.py
from pydantic import BaseModel, Field
from typing import Optional
class BlogGenerateRequest(BaseModel):
topic: str = Field(..., min_length=5, max_length=500)
tone: str = Field(default="professional")
length: str = Field(default="medium")
keywords: Optional[str] = Field(None)
class Config:
json_schema_extra = {
"example": {
"topic": "The Future of AI",
"tone": "professional",
"length": "medium",
"keywords": "AI, machine learning, automation"
}
}
class BlogResponse(BaseModel):
id: int
topic: str
title: str
content: str
seo_score: float
word_count: int
created_at: datetime
class Config:
from_attributes = True # Enable ORM mode
The beauty of Pydantic:
- Automatic validation on incoming requests
- Clear error messages if validation fails
- Auto-generated API documentation
- Type hints everywhere
3. AI Service: The Heart of the Application
This is where the magic happens. Here's my Hugging Face integration:
# app/services/ai_service.py
import requests
import time
from typing import Dict
class HuggingFaceService:
def __init__(self):
self.api_url = "https://router.huggingface.co/v1/chat/completions"
self.headers = {
"Authorization": f"Bearer {settings.HUGGINGFACE_API_KEY}",
"Content-Type": "application/json"
}
def generate_blog(self, topic: str, tone: str, length: str,
keywords: str = None) -> Dict:
"""Main entry point for blog generation"""
# 1. Build optimized prompt
prompt = self._build_prompt(topic, tone, length, keywords)
# 2. Call Hugging Face API
raw_content = self._call_api(prompt)
# 3. Post-process content
cleaned = self.post_processor.clean_content(raw_content)
title, content = self.post_processor.extract_title_and_content(
cleaned, topic
)
# 4. Calculate metrics
word_count = self.post_processor.count_words(content)
seo_score = self.seo_service.calculate_score(
content, title, keywords
)
return {
"title": title,
"content": content,
"word_count": word_count,
"seo_score": seo_score
}
Prompt Engineering: The Secret Sauce
Building effective prompts was crucial. Here's my approach:
def _build_prompt(self, topic: str, tone: str, length: str,
keywords: str = None) -> str:
"""Construct optimized prompt for blog generation"""
tone_map = {
"professional": "authoritative, polished, business-appropriate",
"casual": "friendly, conversational, relatable",
"technical": "detailed, precise, technically accurate",
"educational": "clear, informative, easy to understand"
}
length_map = {
"short": "600-800 words",
"medium": "1000-1500 words",
"long": "1800-2500 words"
}
return f"""You are an expert blog writer. Write a comprehensive blog post.
**Topic:** {topic}
**Tone:** {tone_map.get(tone)}
**Target Length:** {length_map.get(length)}
{f"**Keywords:** {keywords}" if keywords else ""}
**Requirements:**
1. Create an engaging, SEO-friendly title
2. Write a compelling introduction
3. Use clear subheadings (## for H2)
4. Include practical examples
5. End with a strong conclusion
**Format:**
# [Title]
[Introduction]
## [Section 1]
[Content]
## [Section 2]
[Content]
## Conclusion
[Summary]
Write the blog post now:"""
Why this prompt works:
- Clear structure and expectations
- Specific tone guidance
- Explicit formatting requirements
- Target word count for consistency
- Keyword integration instructions

API Call with Retry Logic
Hugging Face models can take time to load (503 errors). Here's my retry logic:
def _call_api(self, prompt: str, max_retries: int = 3) -> str:
"""Call HF API with retry logic"""
payload = {
"model": "meta-llama/Llama-3.3-70B-Instruct",
"messages": [
{
"role": "system",
"content": "You are an expert blog writer."
},
{
"role": "user",
"content": prompt
}
],
"max_tokens": 2500,
"temperature": 0.7,
"top_p": 0.9
}
for attempt in range(max_retries):
try:
response = requests.post(
self.api_url,
headers=self.headers,
json=payload,
timeout=120 # 2 minute timeout
)
# Model loading, wait and retry
if response.status_code == 503:
print(f"Model loading... waiting 30 seconds")
time.sleep(30)
continue
response.raise_for_status()
result = response.json()
return result["choices"][0]["message"]["content"]
except requests.exceptions.Timeout:
if attempt < max_retries - 1:
time.sleep(10)
continue
raise Exception("Request timed out")
raise Exception("Failed after multiple retries")
Key points:
- 503 handling for model warm-up
- Exponential backoff between retries
- Proper timeout handling
- Clear error messages
4. SEO Scoring Algorithm
I built a custom SEO scoring system that evaluates multiple factors:
# app/services/seo_service.py
import re
class SEOService:
@staticmethod
def calculate_score(content: str, title: str,
keywords: str = None) -> float:
"""Calculate SEO score (0-100)"""
score = 0.0
# 1. Word Count Analysis (25 points)
word_count = len(re.findall(r'\b\w+\b', content))
if 800 <= word_count <= 2500:
score += 25
elif 600 <= word_count < 800:
score += 18
elif word_count >= 500:
score += 12
# 2. Title Optimization (15 points)
if title:
title_length = len(title)
if 40 <= title_length <= 70:
score += 15
elif 30 <= title_length < 40:
score += 10
elif title_length > 0:
score += 5
# 3. Heading Structure (20 points)
h2_count = len(re.findall(r'##\s+', content))
h3_count = len(re.findall(r'###\s+', content))
total_headings = h2_count + h3_count
if 3 <= total_headings <= 8:
score += 20
elif 2 <= total_headings <= 10:
score += 15
elif total_headings > 0:
score += 8
# 4. Keyword Optimization (25 points)
if keywords:
keyword_list = [k.strip().lower()
for k in keywords.split(',')]
content_lower = content.lower()
title_lower = title.lower() if title else ""
# Keywords in content
keywords_in_content = sum(
1 for kw in keyword_list if kw in content_lower
)
if keywords_in_content >= len(keyword_list):
score += 15
elif keywords_in_content > 0:
score += 8
# Keywords in title
keywords_in_title = sum(
1 for kw in keyword_list if kw in title_lower
)
if keywords_in_title > 0:
score += 10
else:
score += 12 # Partial credit
# 5. Content Structure (15 points)
has_intro = bool(re.search(
r'(introduction|overview)',
content.lower()[:500]
))
has_conclusion = bool(re.search(
r'(conclusion|summary|final)',
content.lower()[-500:]
))
if has_intro:
score += 7
if has_conclusion:
score += 8
return min(round(score, 2), 100.0)
Scoring breakdown:
- 25 points: Optimal word count (800-2500)
- 15 points: Title length optimization
- 20 points: Proper heading structure
- 25 points: Keyword usage and placement
- 15 points: Content structure (intro/conclusion)
This gives us a fair, multi-dimensional quality score.
5. Post-Processing Pipeline
Raw AI output needs cleaning. Here's my post-processor:
# app/utils/post_processor.py
import re
from typing import Tuple
class PostProcessor:
@staticmethod
def clean_content(text: str) -> str:
"""Clean and format generated text"""
if not text:
return ""
# Remove excessive whitespace
text = re.sub(r'\n{3,}', '\n\n', text)
text = re.sub(r' {2,}', ' ', text)
return text.strip()
@staticmethod
def extract_title_and_content(text: str,
fallback_topic: str) -> Tuple[str, str]:
"""Extract title from content"""
if not text:
return f"{fallback_topic}: A Comprehensive Guide", ""
lines = text.split('\n')
title = None
content_lines = []
for line in lines:
line = line.strip()
# Look for title (lines starting with #)
if line.startswith('# ') and not title:
title = line.replace('# ', '').strip()
elif line:
content_lines.append(line)
# Fallback title if none found
if not title:
title = f"{fallback_topic}: A Comprehensive Guide"
content = '\n\n'.join(content_lines) if content_lines else text
return title, content
@staticmethod
def count_words(text: str) -> int:
"""Count words accurately"""
if not text:
return 0
words = re.findall(r'\b\w+\b', text)
return len(words)
Why post-processing matters:
- AI output isn't always perfectly formatted
- Need to extract structured data (title, content)
- Clean up artifacts and inconsistencies
- Ensure consistent formatting
6. API Routes: Putting It All Together
Finally, the API endpoints that tie everything together:
# app/api/routes.py
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
router = APIRouter()
@router.post("/generate", response_model=BlogResponse, status_code=201)
def generate_blog(
request: BlogGenerateRequest,
db: Session = Depends(get_db)
):
"""Generate a new blog post using AI"""
try:
# Generate content
result = hf_service.generate_blog(
topic=request.topic,
tone=request.tone,
length=request.length,
keywords=request.keywords
)
# Save to database
blog_post = BlogPost(
topic=request.topic,
tone=request.tone,
length=request.length,
keywords=request.keywords,
title=result["title"],
content=result["content"],
word_count=result["word_count"],
seo_score=result["seo_score"]
)
db.add(blog_post)
db.commit()
db.refresh(blog_post)
return blog_post
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=str(e))
@router.get("/blogs", response_model=BlogListResponse)
def list_blogs(
skip: int = 0,
limit: int = 20,
db: Session = Depends(get_db)
):
"""Get list of all generated blogs"""
total = db.query(BlogPost).count()
blogs = (
db.query(BlogPost)
.order_by(BlogPost.created_at.desc())
.offset(skip)
.limit(limit)
.all()
)
return {"total": total, "blogs": blogs}
Key features:
- Automatic request validation via Pydantic
- Database transactions with rollback on error
- Pagination support for blog listing
- Type-safe responses

Frontend Deep Dive
React Component Architecture
I structured the frontend with three main components:
src/
├── components/
│ ├── BlogForm.jsx # Input form
│ ├── BlogDisplay.jsx # Content display
│ └── BlogHistory.jsx # Blog list
├── services/
│ └── api.js # API client
├── utils/
│ └── helpers.js # Utility functions
└── App.jsx
1. BlogForm Component
The input form with validation and user-friendly controls:
// src/components/BlogForm.jsx
import React, { useState } from 'react';
import { Sparkles, Loader2 } from 'lucide-react';
const BlogForm = ({ onGenerate, isGenerating }) => {
const [formData, setFormData] = useState({
topic: '',
tone: 'professional',
length: 'medium',
keywords: '',
});
const [errors, setErrors] = useState({});
const toneOptions = [
{ value: 'professional', label: 'Professional', emoji: '💼' },
{ value: 'casual', label: 'Casual', emoji: '😊' },
{ value: 'technical', label: 'Technical', emoji: '🔧' },
{ value: 'educational', label: 'Educational', emoji: '📚' },
];
const validate = () => {
const newErrors = {};
if (!formData.topic.trim()) {
newErrors.topic = 'Topic is required';
} else if (formData.topic.length < 5) {
newErrors.topic = 'Topic must be at least 5 characters';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = (e) => {
e.preventDefault();
if (validate()) {
onGenerate(formData);
}
};
return (
Generate Blog Post
{/* Topic Input */}
Blog Topic *
<input
type="text"
value={formData.topic}
onChange={(e) => setFormData({
...formData,
topic: e.target.value
})}
placeholder="e.g., The Future of AI"
className="input-field"
disabled={isGenerating}
/>
{errors.topic && (
{errors.topic}
)}
{/* Tone Selection */}
Writing Tone
{toneOptions.map((option) => (
<button
key={option.value}
type="button"
onClick={() => setFormData({
...formData,
tone: option.value
})}
className={`p-3 rounded-lg border-2 ${
formData.tone === option.value
? 'border-primary-500 bg-primary-50'
: 'border-gray-200'
}`}
>
{option.emoji}
{option.label}
))}
{/* Submit Button */}
{isGenerating ? (
<>
Generating Content...
) : (
<>
Generate Blog Post
)}
);
};
export default BlogForm;
UI Features:
- Real-time validation
- Visual feedback on selection
- Loading states during generation
- Disabled state management
- Clean, modern design with Tailwind
2. API Integration
Clean API client using Axios:
// src/services/api.js
import axios from 'axios';
const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000';
const api = axios.create({
baseURL: `${API_BASE_URL}/api/v1`,
headers: {
'Content-Type': 'application/json',
},
timeout: 120000, // 2 minutes for AI generation
});
export const blogAPI = {
generateBlog: async (data) => {
const response = await api.post('/generate', data);
return response.data;
},
getAllBlogs: async (skip = 0, limit = 20) => {
const response = await api.get(`/blogs?skip=${skip}&limit=${limit}`);
return response.data;
},
getBlog: async (id) => {
const response = await api.get(`/blogs/${id}`);
return response.data;
},
deleteBlog: async (id) => {
const response = await api.delete(`/blogs/${id}`);
return response.data;
},
};
Why this structure:
- Centralized API configuration
- Easy to add interceptors later
- Clean function signatures
- Environment-based URL configuration
3. Utility Functions
Helper functions for common tasks:
// src/utils/helpers.js
// Copy to clipboard
export const copyToClipboard = async (text) => {
try {
await navigator.clipboard.writeText(text);
return true;
} catch (err) {
console.error('Failed to copy:', err);
return false;
}
};
// Download as file
export const downloadAsFile = (content, filename, type = 'text/markdown') => {
const blob = new Blob([content], { type });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
};
// Get SEO score color
export const getSEOScoreColor = (score) => {
if (score >= 80) return 'text-green-600 bg-green-100';
if (score >= 60) return 'text-yellow-600 bg-yellow-100';
return 'text-red-600 bg-red-100';
};
// Format date
export const formatDate = (dateString) => {
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
};

Challenges & Solutions
Challenge 1: AI Response Consistency
Problem: The AI sometimes generated content in inconsistent formats, making it hard to extract titles and structure.
Solution: I built a robust post-processing pipeline:
- Multiple fallback strategies for title extraction
- Regex-based cleaning for artifacts
- Format normalization across different outputs
- Validation before saving to database
# Fallback chain for title extraction
if line.startswith('# '):
title = line.replace('# ', '')
elif first_line and len(first_line) < 100:
title = first_line
else:
title = f"{topic}: A Comprehensive Guide"
Challenge 2: Long API Response Times
Problem: AI generation takes 20-60 seconds, which feels like forever without feedback.
Solution: Multiple UX improvements:
- Clear loading indicators with spinner
- Button state changes during generation
- Success/error notifications
- Smooth transitions when content appears
{isGenerating ? (
<>
Generating Content...
) : (
<>
Generate Blog Post
)}
Challenge 3: Model 503 Errors
Problem: Hugging Face models go to sleep after inactivity, returning 503 errors on first request.
Solution: Implemented retry logic with exponential backoff:
if response.status_code == 503:
print("Model loading... waiting 30 seconds")
time.sleep(30)
continue # Retry
This handles the warm-up period gracefully.
Challenge 4: SEO Score Fairness
Problem: Creating a fair, comprehensive scoring algorithm that doesn't over-penalize or over-reward.
Solution: Multi-factor analysis with balanced weights:
- 25 points for word count (most important)
- 25 points for keyword usage
- 20 points for structure
- 15 points for title optimization
- 15 points for content completeness
Each factor is independently scored and summed to 100.
Challenge 5: Database Schema Design
Problem: How to store flexible keyword lists and maintain efficient queries?
Solution:
- Keywords as TEXT field with comma separation
- Indexed fields (topic, id) for fast lookups
- Separate title and content fields for granular access
- Timestamps for sorting and filtering
Current Status & Demo
Here's what we have working right now:
Backend:
- FastAPI server running smoothly
- Database models and migrations
- AI integration with Hugging Face
- SEO scoring algorithm
- Complete CRUD API
Frontend:
- Beautiful, responsive UI
- Blog generation form
- Real-time content display
- Blog history management
- Copy and download features
Features:
- Multiple writing tones
- Flexible content length
- Keyword optimization
- SEO scoring
- Content persistence

Try it yourself:
# Clone the repo
git clone https://github.com/yourusername/ai-blog-generator
# Backend
cd backend
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt
uvicorn app.main:app --reload
# Frontend
cd frontend
npm install
npm run dev

What I Learned
Technical Lessons
- Prompt Engineering Matters: The quality of AI output is 80% about the prompt. I spent hours refining the structure.
- Post-Processing is Essential: Never trust raw AI output - always clean, validate, and structure it.
- UX During Long Operations: Loading states, progress indicators, and clear feedback are crucial for good UX.
- Type Safety Saves Time: Pydantic validation caught countless bugs before they reached production.
- Database Indexing: Proper indexing on topic and timestamps made queries 10x faster.
Soft Skills
- Breaking Down Complexity: Dividing the project into services made it manageable.
- Iterative Development: Started with basic generation, then added SEO, then optimized prompts.
- User-First Design: Always thinking "how would I want this to work?" improved the UX significantly.
🚀 What's Next (Part 2)
In the next post, I'll cover:
- Deploying Backend to Railway
- Setting up PostgreSQL
- Environment variables
- Handling production errors
- Deploying Frontend to Vercel
- Build optimization
- Environment configuration
- Custom domain setup
- Production Optimizations
- Caching strategies
- Rate limiting
- Monitoring and logging
- Future Enhancements
- Image generation for blog posts
- Multi-language support
- Content scheduling
- Analytics dashboard
Key Takeaways
If you're building an AI-powered application, here are my top recommendations:
- Start Simple: Get basic generation working before adding complexity
- Invest in Prompt Engineering: It's the most important factor for output quality
- Build Robust Error Handling: AI APIs fail - plan for it
- Focus on UX: Long wait times need great feedback
- Test with Real Users: Get feedback early and iterate
### Resources & Links
- GitHub Repository
- Live Demo: Coming in Part 2!
- FastAPI Docs
- Hugging Face
- Tailwind CSS
Let's Connect
Building this project was an incredible learning experience. I'd love to hear your thoughts:
- What features would you add?
- What challenges have you faced with AI integration?
- Any questions about the implementation?
Drop a comment below or connect with me:
📢 ### Stay Tuned for Part 2!
Next up: Deployment to Production
I'll walk through:
- Railway deployment for backend
- Vercel deployment for frontend
- Domain setup and SSL
- Production monitoring
- Performance optimization
Make sure to follow me to get notified when Part 2 drops!
Thanks for reading! If you found this helpful, please give it a ❤️ and share with others who might benefit from it.
More...