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:

  1. Research — Claude uses web search to find current information
  2. Outline — Claude creates a structured outline with headings
  3. Write — Claude writes each section one at a time
  4. Review — Claude reviews and improves the draft
  5. SEO — Claude generates meta description and keywords

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

StepModelTokensCost
ResearchSonnet 4.6 + Web Search~3,000 in + 2,000 out~$0.04
OutlineSonnet 4.6~2,000 in + 1,000 out~$0.02
Write (6 sections)Sonnet 4.6~12,000 in + 6,000 out~$0.13
ReviewSonnet 4.6~4,000 in + 1,000 out~$0.03
SEOSonnet 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

StepWhat It Does
ResearchWeb search for current facts and examples
OutlineStructured JSON outline with sections and key points
WriteOne section at a time for quality
ReviewScore and suggest improvements
SEOGenerate 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