AINode.jsAutomation

AI Blog Post Generator

TT
TopicTrick Team
AI Blog Post Generator

AI Blog Post Generator

Generate full SEO-optimised blog posts from a single topic or URL. This tool uses a two-stage approach — outline first, then expand each section — to produce coherent, 1,500–2,500 word articles with headings, meta description, and keyword targeting.

This is Tool 27 of the Build 50 AI Automation Tools course.


What You'll Build

  • POST /generate — topic or URL in, full markdown article out
  • Two-stage generation: structured outline → section-by-section expansion
  • SEO fields: meta description, keyword density, internal link suggestions
  • POST /generate/batch — bulk article generation from a list of topics

Setup

bash
mkdir ai-blog-gen && cd ai-blog-gen
npm init -y
npm install express openai axios cheerio p-limit dotenv
bash
# .env
OPENAI_API_KEY=sk-your-key-here
PORT=3000

Blog Generation Service

js
// src/services/blogService.js
import OpenAI from 'openai';
import axios from 'axios';
import * as cheerio from 'cheerio';

const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

async function fetchUrlContent(url) {
  const { data } = await axios.get(url, { timeout: 10_000 });
  const $ = cheerio.load(data);
  $('script, style, nav, footer, header').remove();
  return $('body').text().replace(/\s+/g, ' ').trim().slice(0, 5_000);
}

async function generateOutline(topic, audience, tone, wordCount) {
  const response = await openai.chat.completions.create({
    model: 'gpt-4o',
    messages: [
      {
        role: 'system',
        content: `You are an expert SEO content strategist. Create a detailed blog post outline.

Return ONLY a JSON object:
{
  "title": "SEO-optimised H1 title containing the primary keyword",
  "primaryKeyword": "main keyword to target",
  "metaDescription": "compelling meta description 150-160 chars with keyword",
  "targetWordCount": ${wordCount},
  "audience": "${audience}",
  "tone": "${tone}",
  "sections": [
    {
      "heading": "H2 heading",
      "keyPoints": ["3-5 key points to cover"],
      "estimatedWords": 200
    }
  ],
  "relatedKeywords": ["5 semantically related keywords"],
  "internalLinkSuggestions": ["topics to link to from this article"],
  "faq": [
    { "question": "string", "answer": "one-sentence answer" }
  ]
}`,
      },
      {
        role: 'user',
        content: `Topic: ${topic}\nAudience: ${audience}\nTone: ${tone}\nTarget word count: ${wordCount}`,
      },
    ],
    temperature: 0.4,
    response_format: { type: 'json_object' },
  });

  return JSON.parse(response.choices[0].message.content);
}

async function expandSection(heading, keyPoints, primaryKeyword, tone, wordTarget) {
  const response = await openai.chat.completions.create({
    model: 'gpt-4o',
    messages: [
      {
        role: 'system',
        content: `You are an expert blog writer. Write a complete blog section.
- Tone: ${tone}
- Primary keyword to include naturally: "${primaryKeyword}"
- Word count target: ~${wordTarget} words
- Use short paragraphs (2-4 sentences)
- No filler phrases like "In today's world" or "It's important to note"
- Return ONLY the markdown content, no JSON wrapper`,
      },
      {
        role: 'user',
        content: `Write the section for heading: "${heading}"\n\nKey points to cover:\n${keyPoints.map(p => `- ${p}`).join('\n')}`,
      },
    ],
    temperature: 0.5,
  });

  return response.choices[0].message.content.trim();
}

export async function generateBlogPost(options) {
  const {
    topic,
    url,
    audience = 'developers and technical professionals',
    tone = 'informative and practical',
    wordCount = 1800,
  } = options;

  let enrichedTopic = topic;
  if (url) {
    const urlContent = await fetchUrlContent(url);
    enrichedTopic = `${topic}\n\nReference content from this URL:\n${urlContent}`;
  }

  const outline = await generateOutline(enrichedTopic, audience, tone, wordCount);

  const sections = await Promise.all(
    outline.sections.map(section =>
      expandSection(section.heading, section.keyPoints, outline.primaryKeyword, tone, section.estimatedWords)
        .then(content => ({ heading: section.heading, content }))
    )
  );

  const intro = await openai.chat.completions.create({
    model: 'gpt-4o',
    messages: [
      {
        role: 'system',
        content: `Write a compelling 100-150 word introduction for a blog post. Include the primary keyword "${outline.primaryKeyword}" in the first sentence. Hook the reader immediately. Return only the paragraph text.`,
      },
      { role: 'user', content: `Title: ${outline.title}\nAudience: ${audience}` },
    ],
    temperature: 0.5,
  });

  const faqSection = outline.faq?.length > 0
    ? '\n\n## Frequently Asked Questions\n\n' + outline.faq.map(f =>
        `**${f.question}**\n\n${f.answer}`
      ).join('\n\n')
    : '';

  const body = sections.map(s => `## ${s.heading}\n\n${s.content}`).join('\n\n');

  const fullMarkdown = `# ${outline.title}\n\n${intro.choices[0].message.content}\n\n${body}${faqSection}`;

  return {
    title: outline.title,
    metaDescription: outline.metaDescription,
    primaryKeyword: outline.primaryKeyword,
    relatedKeywords: outline.relatedKeywords,
    internalLinkSuggestions: outline.internalLinkSuggestions,
    wordCount: fullMarkdown.split(/\s+/).length,
    markdown: fullMarkdown,
    outline,
  };
}

Server

js
// src/server.js
import 'dotenv/config';
import express from 'express';
import pLimit from 'p-limit';
import { generateBlogPost } from './services/blogService.js';

const app = express();
app.use(express.json());

app.post('/generate', async (req, res, next) => {
  try {
    const { topic, url, audience, tone, wordCount } = req.body;
    if (!topic && !url) return res.status(400).json({ error: 'topic or url required' });

    const post = await generateBlogPost({ topic, url, audience, tone, wordCount });
    res.json({ success: true, ...post });
  } catch (err) { next(err); }
});

app.post('/generate/batch', async (req, res, next) => {
  try {
    const { topics, audience, tone, wordCount } = req.body;
    if (!Array.isArray(topics) || topics.length === 0) {
      return res.status(400).json({ error: 'topics array required' });
    }

    const limit = pLimit(3); // 3 concurrent generations
    const results = await Promise.all(
      topics.map(topic =>
        limit(() =>
          generateBlogPost({ topic, audience, tone, wordCount })
            .then(post => ({ topic, success: true, ...post }))
            .catch(err => ({ topic, success: false, error: err.message }))
        )
      )
    );

    res.json({ success: true, count: results.length, results });
  } catch (err) { next(err); }
});

app.use((err, _req, res, _next) => res.status(500).json({ error: err.message }));
app.listen(process.env.PORT ?? 3000, () => console.log('Blog generator running'));

Testing

bash
curl -X POST http://localhost:3000/generate \
  -H "Content-Type: application/json" \
  -d '{
    "topic": "How to build a REST API with Node.js and Express",
    "audience": "junior developers learning backend development",
    "tone": "friendly and practical with code examples",
    "wordCount": 2000
  }'

Batch generation:

bash
curl -X POST http://localhost:3000/generate/batch \
  -H "Content-Type: application/json" \
  -d '{
    "topics": [
      "Node.js error handling best practices",
      "How to use Redis for caching in Node.js",
      "JWT authentication in Express.js"
    ],
    "wordCount": 1500
  }'

Build 50 AI Automation Tools — Tool 27 of 50

Blog post generator is live. Continue to Tool 28 to build an AI social media caption generator.


    Summary

    • Two-stage generation (outline → expand) produces coherent long-form articles vs. single-shot generation
    • Section-level parallelism — all sections expand simultaneously, cutting generation time by 60%
    • URL enrichment lets you generate companion content or updated versions of existing articles
    • Batch endpoint with p-limit throttling enables content calendar generation without rate limit errors
    • Always edit AI output — add personal experience, data citations, and examples for E-E-A-T signals

    Continue to Tool 28: AI Social Media Caption Generator →