AINode.jsAutomation

AI Blog Post Generator

Build an AI blog post generator with Node.js and GPT-4o that produces full SEO-optimised articles from a topic or URL, complete with outline, headings, body, meta description, and internal link suggestions.

TT
Emily Ross
••6 min read
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 →