Writing blog posts takes hours. Research, outline, drafting, editing — it adds up. In this article, you will build an AI-powered blog writer that automates the process. Give it a topic, and it researches, outlines, and writes a complete article.
This is Article 23 in the Claude AI — From Zero to Power User series. You should know Tool Use and Structured Output before this article.
Architecture
The blog writer follows a five-step pipeline:
Topic → Research (Web Search) → Outline → Write Sections → Review & Edit
Each step uses Claude in a different way:
- Research — Claude uses web search to find current information
- Outline — Claude creates a structured outline with headings
- Write — Claude writes each section one at a time
- Review — Claude reviews and improves the draft
- SEO — Claude generates meta description and keywords
Step 1: Research with Web Search
Use Claude’s built-in web search tool to gather current information:
Python
import anthropic
import json
client = anthropic.Anthropic()
def research_topic(topic: str) -> str:
"""Research a topic using Claude's web search."""
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=4096,
tools=[{"type": "web_search_20250305"}],
messages=[
{
"role": "user",
"content": f"""Research this topic for a blog article: "{topic}"
Find:
1. Current facts and statistics (2025-2026)
2. Best practices from reputable sources
3. Real-world examples
4. Common mistakes to avoid
Summarize your findings in a structured format.""",
}
],
)
# Extract the final text response
for block in response.content:
if hasattr(block, "text"):
return block.text
return ""
research = research_topic("Python async programming best practices")
print(research[:500])
TypeScript
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
async function researchTopic(topic: string): Promise<string> {
const response = await client.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 4096,
tools: [{ type: "web_search_20250305" as any }],
messages: [
{
role: "user",
content: `Research this topic for a blog article: "${topic}"
Find:
1. Current facts and statistics (2025-2026)
2. Best practices from reputable sources
3. Real-world examples
4. Common mistakes to avoid
Summarize your findings in a structured format.`,
},
],
});
for (const block of response.content) {
if ("text" in block) return block.text;
}
return "";
}
Step 2: Generate an Outline
Use structured output to get a consistent outline format:
Python
def generate_outline(topic: str, research: str) -> dict:
"""Generate a blog post outline from research."""
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=2048,
temperature=0,
system="""You create blog post outlines. Return valid JSON only.
<output_format>
{
"title": "Blog post title (60-70 characters)",
"description": "Meta description (under 160 characters)",
"sections": [
{
"heading": "Section heading",
"key_points": ["point 1", "point 2", "point 3"],
"estimated_words": 300
}
],
"target_words": 2000,
"keywords": ["keyword1", "keyword2", "keyword3"]
}
</output_format>""",
messages=[
{
"role": "user",
"content": f"""Create a blog post outline for: "{topic}"
<research>
{research}
</research>
Requirements:
- 5-8 sections
- Total target: 2000 words
- Include an introduction and conclusion
- Each section should cover one main idea
- Include code examples where relevant""",
}
],
)
text = response.content[0].text
if "```json" in text:
text = text.split("```json")[1].split("```")[0]
elif "```" in text:
text = text.split("```")[1].split("```")[0]
return json.loads(text.strip())
outline = generate_outline("Python async programming best practices", research)
print(f"Title: {outline['title']}")
print(f"Sections: {len(outline['sections'])}")
for s in outline["sections"]:
print(f" - {s['heading']} (~{s['estimated_words']} words)")
Step 3: Write Section by Section
Writing the article one section at a time produces better results than asking for the entire article at once:
Python
def write_section(
topic: str,
section: dict,
previous_sections: str,
research: str,
style: str = "simple English, short sentences, practical examples",
) -> str:
"""Write one section of the blog post."""
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=2048,
system=f"""You are a technical blog writer.
<style>
- Write in {style}
- Use short paragraphs (2-3 sentences)
- Include code examples with language tags
- No fluff or filler — every sentence adds value
- Write for developers who are intermediate level
</style>""",
messages=[
{
"role": "user",
"content": f"""Write the following section for a blog post about "{topic}".
<section>
Heading: {section['heading']}
Key points to cover: {json.dumps(section['key_points'])}
Target length: {section['estimated_words']} words
</section>
<research>
{research[:3000]}
</research>
<previous_sections>
{previous_sections[-2000:] if previous_sections else "This is the first section."}
</previous_sections>
Write this section. Start with the heading (## {section['heading']}). Do not repeat content from previous sections.""",
}
],
)
return response.content[0].text
def write_article(topic: str, outline: dict, research: str) -> str:
"""Write the full article section by section."""
sections = []
for i, section in enumerate(outline["sections"]):
print(f"Writing section {i+1}/{len(outline['sections'])}: {section['heading']}")
previous = "\n\n".join(sections)
text = write_section(topic, section, previous, research)
sections.append(text)
return "\n\n".join(sections)
article = write_article(
"Python async programming best practices",
outline,
research,
)
print(f"Article written: {len(article.split())} words")
TypeScript
async function writeSection(
topic: string,
section: { heading: string; key_points: string[]; estimated_words: number },
previousSections: string,
research: string
): Promise<string> {
const response = await client.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 2048,
system: `You are a technical blog writer.
<style>
- Write in simple English, short sentences
- Use short paragraphs (2-3 sentences)
- Include code examples with language tags
- No fluff — every sentence adds value
</style>`,
messages: [
{
role: "user",
content: `Write the following section for a blog post about "${topic}".
<section>
Heading: ${section.heading}
Key points: ${JSON.stringify(section.key_points)}
Target length: ${section.estimated_words} words
</section>
<research>
${research.slice(0, 3000)}
</research>
<previous_sections>
${previousSections.slice(-2000) || "This is the first section."}
</previous_sections>
Write this section. Start with ## ${section.heading}. Do not repeat previous content.`,
},
],
});
return response.content[0].type === "text" ? response.content[0].text : "";
}
async function writeArticle(
topic: string,
outline: any,
research: string
): Promise<string> {
const sections: string[] = [];
for (let i = 0; i < outline.sections.length; i++) {
const section = outline.sections[i];
console.log(
`Writing section ${i + 1}/${outline.sections.length}: ${section.heading}`
);
const previous = sections.join("\n\n");
const text = await writeSection(topic, section, previous, research);
sections.push(text);
}
return sections.join("\n\n");
}
Step 4: Review and Edit
After writing, use Claude to review and improve the article:
def review_article(article: str, topic: str) -> dict:
"""Review the article and suggest improvements."""
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=2048,
temperature=0,
system="""You are a technical editor. Review blog articles.
Return JSON:
{
"score": 8,
"strengths": ["...", "..."],
"issues": [
{"type": "accuracy|clarity|structure|seo", "description": "...", "location": "section name"}
],
"improved_title": "...",
"improved_description": "..."
}""",
messages=[
{
"role": "user",
"content": f"""Review this blog article about "{topic}":
<article>
{article}
</article>
Check for:
1. Technical accuracy
2. Clarity (simple English, no jargon)
3. Structure (logical flow, good headings)
4. SEO (keywords, meta description)
5. Code examples (correct syntax, useful)""",
}
],
)
text = response.content[0].text
if "```json" in text:
text = text.split("```json")[1].split("```")[0]
return json.loads(text.strip())
review = review_article(article, "Python async programming best practices")
print(f"Score: {review['score']}/10")
for issue in review["issues"]:
print(f" [{issue['type']}] {issue['description']}")
Step 5: SEO Optimization
Generate SEO metadata for the article:
def generate_seo(article: str, topic: str) -> dict:
"""Generate SEO metadata for the article."""
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
temperature=0,
system="Return valid JSON only.",
messages=[
{
"role": "user",
"content": f"""Generate SEO metadata for this article about "{topic}":
<article>
{article[:5000]}
</article>
Return:
{{
"title": "SEO-optimized title (50-60 chars)",
"description": "Meta description (150-160 chars)",
"keywords": ["5-10 target keywords"],
"slug": "url-friendly-slug"
}}""",
}
],
)
text = response.content[0].text
if "```json" in text:
text = text.split("```json")[1].split("```")[0]
return json.loads(text.strip())
Complete Pipeline
Put it all together in one function:
def write_blog_post(topic: str) -> dict:
"""Complete blog writing pipeline."""
print(f"\n{'='*50}")
print(f"Writing blog post: {topic}")
print(f"{'='*50}\n")
# Step 1: Research
print("Step 1: Researching...")
research = research_topic(topic)
# Step 2: Outline
print("Step 2: Creating outline...")
outline = generate_outline(topic, research)
print(f" Title: {outline['title']}")
print(f" Sections: {len(outline['sections'])}")
# Step 3: Write
print("Step 3: Writing article...")
article = write_article(topic, outline, research)
word_count = len(article.split())
print(f" Words: {word_count}")
# Step 4: Review
print("Step 4: Reviewing...")
review = review_article(article, topic)
print(f" Score: {review['score']}/10")
# Step 5: SEO
print("Step 5: Generating SEO metadata...")
seo = generate_seo(article, topic)
# Combine into final output
result = {
"title": seo["title"],
"description": seo["description"],
"keywords": seo["keywords"],
"slug": seo["slug"],
"article": article,
"word_count": word_count,
"review_score": review["score"],
"outline": outline,
}
print(f"\nDone! {word_count} words, score: {review['score']}/10")
return result
# Generate a blog post
post = write_blog_post("Python async programming best practices")
# Save to file
with open(f"{post['slug']}.md", "w") as f:
f.write(f"---\ntitle: \"{post['title']}\"\n")
f.write(f"description: \"{post['description']}\"\n")
f.write(f"keywords: {json.dumps(post['keywords'])}\n---\n\n")
f.write(post["article"])
Human-in-the-Loop
Never publish AI-generated content without review. Add approval steps:
def write_with_approval(topic: str) -> dict:
"""Blog writing with human approval at each step."""
research = research_topic(topic)
print(f"\nResearch summary:\n{research[:500]}...")
input("\nPress Enter to continue or Ctrl+C to stop...")
outline = generate_outline(topic, research)
print(f"\nOutline:")
for s in outline["sections"]:
print(f" - {s['heading']}")
input("\nPress Enter to continue or Ctrl+C to stop...")
article = write_article(topic, outline, research)
print(f"\nArticle preview:\n{article[:1000]}...")
input("\nPress Enter to approve or Ctrl+C to stop...")
return {"article": article, "outline": outline}
Style Customization
Adjust the writing style with the system prompt:
styles = {
"technical": "Technical and precise. Include code examples. Target experienced developers.",
"beginner": "Simple English, short sentences. Explain every term. Target beginners.",
"casual": "Conversational tone, use examples from everyday life. Keep it fun.",
"seo": "SEO-optimized. Use keywords naturally. Include FAQ section.",
}
# Use a style
article = write_article(topic, outline, research)
Cost Per Article
| Step | Model | Tokens | Cost |
|---|---|---|---|
| Research | Sonnet 4.6 + Web Search | ~3,000 in + 2,000 out | ~$0.04 |
| Outline | Sonnet 4.6 | ~2,000 in + 1,000 out | ~$0.02 |
| Write (6 sections) | Sonnet 4.6 | ~12,000 in + 6,000 out | ~$0.13 |
| Review | Sonnet 4.6 | ~4,000 in + 1,000 out | ~$0.03 |
| SEO | Sonnet 4.6 | ~2,000 in + 500 out | ~$0.01 |
| Total | ~$0.23 |
Writing a 2,000-word researched article costs approximately $0.20-0.50 depending on the topic complexity.
Summary
| Step | What It Does |
|---|---|
| Research | Web search for current facts and examples |
| Outline | Structured JSON outline with sections and key points |
| Write | One section at a time for quality |
| Review | Score and suggest improvements |
| SEO | Generate title, description, keywords |
What’s Next?
In the next article, we will integrate Claude into CI/CD pipelines for automated code review on every pull request.
Next: Claude in CI/CD — Automated Code Review