Keyword in → SERP research → Gap analysis → GEO-optimised article → WordPress draft + Slack alert
All 7 steps run automatically. Add API keys in Config tab before running.
// STEP 1: Input Validation // n8n: Use a "Code" node as first node after Webhook trigger const validateInput = (params) => { const { keyword, wordCount, lang, audience, contentType, voice } = params; if (!keyword || keyword.trim().length < 3) throw new Error("Keyword must be at least 3 characters"); return { keyword: keyword.trim().toLowerCase(), wordCount: parseInt(wordCount) || 1500, lang: lang || "en", audience: audience || "business professionals", contentType: contentType || "blog", voice: voice || "professional", timestamp: new Date().toISOString(), runId: Math.random().toString(36).slice(2, 10) }; };
// STEP 2: SERP Research via Perplexity // n8n: HTTP Request node // URL: https://api.perplexity.ai/chat/completions // Method: POST // Auth: Header Auth → Authorization: Bearer YOUR_KEY const PERPLEXITY_API_KEY = "ADD_YOUR_PERPLEXITY_KEY_HERE"; // ← REPLACE THIS const serpResearch = async (keyword, lang) => { const res = await fetch("https://api.perplexity.ai/chat/completions", { method: "POST", headers: { "Authorization": `Bearer ${PERPLEXITY_API_KEY}`, "Content-Type": "application/json" }, body: JSON.stringify({ model: "llama-3.1-sonar-large-128k-online", messages: [ { role: "system", content: "You are an expert SEO research analyst. Return ONLY valid JSON." }, { role: "user", content: `Research top 10 ranking pages for keyword: "${keyword}" Language: ${lang}. Return JSON: { "topPages": [{ "url":"", "title":"", "keyPoints":[], "wordCount":0 }], "commonHeadings": [], "averageWordCount": 0, "topEntities": [], "relatedQuestions": [], "searchIntent": "informational|commercial|transactional" }` } ], temperature: 0.1, return_related_questions: true }) }); const data = await res.json(); return JSON.parse(data.choices[0].message.content); };
// STEP 3: Content Gap Analysis via OpenAI // n8n: HTTP Request node // URL: https://api.openai.com/v1/chat/completions // Auth: Header Auth → Authorization: Bearer YOUR_KEY const OPENAI_API_KEY = "ADD_YOUR_OPENAI_KEY_HERE"; // ← REPLACE THIS const gapAnalysis = async (serpData, keyword, audience) => { const res = await fetch("https://api.openai.com/v1/chat/completions", { method: "POST", headers: { "Authorization": `Bearer ${OPENAI_API_KEY}`, "Content-Type": "application/json" }, body: JSON.stringify({ model: "gpt-4o-mini", response_format: { type: "json_object" }, messages: [ { role: "system", content: "Expert content strategist. Return ONLY valid JSON." }, { role: "user", content: `Keyword: "${keyword}" | Audience: ${audience} SERP Data: ${JSON.stringify(serpData)} Find content gaps. Return: { "gaps": ["missing angle 1", "missing angle 2"], "uniqueAngles": ["competitive differentiator 1"], "missingEntities": ["entity not covered by competitors"], "geoOpportunities": ["direct answer opportunity for AI search"], "recommendedWordCount": 1600 }` } ], temperature: 0.3 }) }); const d = await res.json(); return JSON.parse(d.choices[0].message.content); };
// STEP 4: Structured Article Outline // Same OpenAI key used here const generateOutline = async (keyword, gapData, params) => { const res = await fetch("https://api.openai.com/v1/chat/completions", { method: "POST", headers: { "Authorization": `Bearer ${OPENAI_API_KEY}`, "Content-Type": "application/json" }, body: JSON.stringify({ model: "gpt-4o-mini", response_format: { type: "json_object" }, messages: [ { role: "system", content: "Expert SEO content architect. Return ONLY valid JSON." }, { role: "user", content: `Create ${params.wordCount}-word ${params.contentType} outline for: "${keyword}" Audience: ${params.audience} | Voice: ${params.voice} Gaps to fill: ${JSON.stringify(gapData.gaps)} GEO opportunities: ${JSON.stringify(gapData.geoOpportunities)} Return: { "title": "SEO-optimised H1 title", "metaDescription": "155-char meta description", "slug": "url-friendly-slug", "sections": [{ "h2": "heading", "wordCount": 300, "keyPoints": [], "geoTarget": true }], "faqQuestions": ["Q1","Q2","Q3"] }` } ], temperature: 0.4 }) }); const d = await res.json(); return JSON.parse(d.choices[0].message.content); };
// STEP 5: Full Article Draft // Uses GPT-4o (not mini) for higher quality output // This is the most expensive step: ~$0.015 per article const draftArticle = async (outline, keyword, params) => { const langRule = params.lang === "de" ? "Write in formal German. Use Sie-form. No anglicisms." : params.lang === "ar" ? "Write in Modern Standard Arabic (MSA). RTL layout." : "Write in clear, direct professional English."; const res = await fetch("https://api.openai.com/v1/chat/completions", { method: "POST", headers: { "Authorization": `Bearer ${OPENAI_API_KEY}`, "Content-Type": "application/json" }, body: JSON.stringify({ model: "gpt-4o", max_tokens: 4000, messages: [ { role: "system", content: `World-class B2B content writer. Rules: - ${langRule} - Voice: ${params.voice} - Target exactly ${params.wordCount} words - GEO: write direct concise answers AI engines will cite - Use markdown: ## H2, ### H3, **bold** for key terms - Include real statistics with years (e.g. "According to McKinsey 2024...") - FAQ section at end with schema-ready Q and A format` }, { role: "user", content: `Write full article for keyword: "${keyword}" Title: ${outline.title} Audience: ${params.audience} Sections: ${outline.sections.map((s,i) => `${i+1}. ${s.h2} (~${s.wordCount} words): ${s.keyPoints?.join(", ")}`).join("\n")} FAQ questions: ${outline.faqQuestions?.join(" | ")}` } ], temperature: 0.7 }) }); const d = await res.json(); return d.choices[0].message.content; };
// STEP 6: Push to WordPress as Draft // OPTIONAL - only runs if WP credentials are configured // n8n: HTTP Request node // URL: https://YOUR_SITE/wp-json/wp/v2/posts // Auth: Basic Auth → username + app password const WP_URL = "ADD_YOUR_WORDPRESS_URL_HERE"; // ← e.g. https://saddamadil.in const WP_USERNAME = "ADD_YOUR_WORDPRESS_USERNAME_HERE"; // ← e.g. admin const WP_APP_PASS = "ADD_YOUR_WP_APP_PASSWORD_HERE"; // ← xxxx xxxx xxxx xxxx const pushToWordPress = async (outline, articleMarkdown) => { if (!WP_URL || WP_URL.includes("ADD_YOUR")) { return { skipped: true, reason: "WordPress credentials not configured" }; } const credentials = btoa(`${WP_USERNAME}:${WP_APP_PASS}`); const res = await fetch(`${WP_URL}/wp-json/wp/v2/posts`, { method: "POST", headers: { "Authorization": `Basic ${credentials}`, "Content-Type": "application/json" }, body: JSON.stringify({ title: outline.title, content: articleMarkdown, slug: outline.slug, excerpt: outline.metaDescription, status: "draft", // Always draft - human reviews before publishing meta: { _yoast_wpseo_metadesc: outline.metaDescription } }) }); const post = await res.json(); return { postId: post.id, editUrl: `${WP_URL}/wp-admin/post.php?post=${post.id}&action=edit` }; };
// STEP 7: Slack Notification // OPTIONAL - skipped if webhook not configured // n8n: HTTP Request node → POST to your webhook URL const SLACK_WEBHOOK = "ADD_YOUR_SLACK_WEBHOOK_URL_HERE"; // ← REPLACE const notifySlack = async (outline, wpResult, params, runId) => { if (!SLACK_WEBHOOK || SLACK_WEBHOOK.includes("ADD_YOUR")) return { skipped: true }; const editLink = wpResult?.editUrl || "(WordPress not configured)"; await fetch(SLACK_WEBHOOK, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ blocks: [ { type: "header", text: { type: "plain_text", text: "SEO Article Draft Ready" } }, { type: "section", fields: [ { type: "mrkdwn", text: `*Title:*\n${outline.title}` }, { type: "mrkdwn", text: `*Keyword:*\n${params.keyword}` }, { type: "mrkdwn", text: `*Language:*\n${params.lang.toUpperCase()}` }, { type: "mrkdwn", text: `*Run ID:*\n${runId}` } ]}, { type: "section", text: { type: "mrkdwn", text: `*Edit in WordPress:*\n${editLink}` } }, { type: "section", text: { type: "mrkdwn", text: "Review, edit, and publish when ready." } } ] }) }); return { sent: true }; };
// MASTER RUNNER: Chains all 7 steps together // In n8n: Each step above is a separate node connected in sequence // For standalone Node.js: copy all functions above + this runner into one file const runSEOAgent = async (rawParams) => { let params, serpData, gapData, outline, article, wpResult; try { // Step 1: Validate params = validateInput(rawParams); console.log(`[01] Input validated: "${params.keyword}"`); // Step 2: SERP Research serpData = await serpResearch(params.keyword, params.lang); console.log(`[02] Found ${serpData.topPages?.length} top pages`); // Step 3: Gap Analysis gapData = await gapAnalysis(serpData, params.keyword, params.audience); console.log(`[03] Found ${gapData.gaps?.length} content gaps`); // Step 4: Outline outline = await generateOutline(params.keyword, gapData, params); console.log(`[04] Outline: "${outline.title}" | ${outline.sections?.length} sections`); // Step 5: Draft Article article = await draftArticle(outline, params.keyword, params); console.log(`[05] Draft: ${article.split(" ").length} words`); // Step 6: WordPress (optional) wpResult = await pushToWordPress(outline, article); console.log(`[06] WordPress: ${wpResult.skipped ? "skipped" : `post #${wpResult.postId}`}`); // Step 7: Notify (optional) await notifySlack(outline, wpResult, params, params.runId); console.log("[07] Done."); return { success: true, outline, article, wpResult }; } catch (err) { console.error("Agent error:", err.message); return { success: false, error: err.message }; } }; // Run it: runSEOAgent({ keyword: "epoxy resins for automotive composites", lang: "en", wordCount: 1500, contentType: "whitepaper", audience: "industrial procurement managers in Germany", voice: "technical" }).then(console.log);