AINode.jsAutomation
AI Blog Post Generator
TT
TopicTrick Team
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 dotenvbash
# .env
OPENAI_API_KEY=sk-your-key-here
PORT=3000Blog 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 →
Post Navigation (Previous/Next)
