Scenario
Pull keyword suggestions from Ahrefs’ Keywords Explorer, cluster them by parent topic, then generate SEO-optimized content at scale — titles, meta descriptions, and outlines for each cluster — using Mavera with a consistent brand voice. This job uses the OpenAI SDK pattern for some Mavera calls to demonstrate compatibility.Architecture
Code
import os, requests, time
from collections import defaultdict
from openai import OpenAI
AH = os.environ["AHREFS_API_TOKEN"]
MV = os.environ["MAVERA_API_KEY"]
MB = "https://app.mavera.io/api/v1"
AH_H = {"Authorization": f"Bearer {AH}", "Accept": "application/json"}
MH = {"Authorization": f"Bearer {MV}", "Content-Type": "application/json"}
SEEDS, COUNTRY = ["content marketing", "audience research"], "us"
mavera = OpenAI(base_url=MB, api_key=MV)
resp = requests.get("https://api.ahrefs.com/v3/keywords-explorer/keyword-ideas", headers=AH_H,
params={"keywords": ",".join(SEEDS), "country": COUNTRY, "limit": 200,
"order_by": "volume:desc",
"select": "keyword,volume,keyword_difficulty,cpc,parent_topic"})
resp.raise_for_status()
ideas = resp.json().get("keywords", [])
clusters = defaultdict(list)
for kw in ideas:
clusters[kw.get("parent_topic", kw["keyword"].split()[0])].append(kw)
top_clusters = sorted(clusters.items(), key=lambda c: sum(k["volume"] for k in c[1]),
reverse=True)[:8]
print(f"Keywords: {len(ideas)} | Clusters: {len(clusters)} | Top {len(top_clusters)}\n")
bv = requests.post(f"{MB}/brand-voices", headers=MH, json={
"name": "SEO Content Voice",
"description": "Clear, authoritative, data-backed. For practitioners, not beginners.",
"samples": ["73% of teams skip validation. Here's the framework that fixes that.",
"1,200 top pages analyzed. Pattern: data-first, opinion-second."],
}).json()
for topic, kws in top_clusters:
kws_sorted = sorted(kws, key=lambda k: k["volume"], reverse=True)
total_vol = sum(k["volume"] for k in kws)
avg_kd = sum(k.get("keyword_difficulty", 0) for k in kws) // max(len(kws), 1)
kw_list = "\n".join(f"- {k['keyword']} (vol: {k['volume']}, KD: {k.get('keyword_difficulty', 0)}, "
f"CPC: ${k.get('cpc', 0):.2f})" for k in kws_sorted[:12])
gen = requests.post(f"{MB}/generations", headers=MH, json={
"prompt": f"SEO plan for '{topic}'\nVol: {total_vol} | KD: {avg_kd} | KWs: {len(kws)}\n\n"
f"KEYWORDS:\n{kw_list}\n\nGenerate: 1) Title (60ch) 2) Meta (155ch) "
f"3) Primary + 4 secondary KWs 4) H2/H3 outline 5) Word count "
f"6) SERP features 7) Internal links",
"brand_voice_id": bv["id"],
}).json()
print(f"=== {topic} ({len(kws)} kws, {total_vol:,} vol, KD {avg_kd}) ===")
print(gen.get("output", gen.get("content", ""))[:600], "\n")
time.sleep(1)
chat = mavera.responses.create(model="mave", input=[{"role": "user", "content":
f"Summarize: {len(top_clusters)} clusters, "
f"{sum(len(kws) for _, kws in top_clusters)} keywords, "
f"{sum(sum(k['volume'] for k in kws) for _, kws in top_clusters):,} volume. "
f"2-week execution schedule by volume and difficulty."}])
print("=== Execution Schedule ===\n" + chat.output[0].content[0].text)
const AH = process.env.AHREFS_API_TOKEN, MV = process.env.MAVERA_API_KEY;
const MB = "https://app.mavera.io/api/v1";
const AH_H = { Authorization: `Bearer ${AH}`, Accept: "application/json" };
const MH = { Authorization: `Bearer ${MV}`, "Content-Type": "application/json" };
const SEEDS = ["content marketing", "audience research"];
const params = new URLSearchParams({ keywords: SEEDS.join(","), country: "us",
limit: "200", order_by: "volume:desc",
select: "keyword,volume,keyword_difficulty,cpc,parent_topic" });
const ideas = (await fetch(
`https://api.ahrefs.com/v3/keywords-explorer/keyword-ideas?${params}`, { headers: AH_H }
).then((r) => r.json())).keywords || [];
const clusters = {};
for (const kw of ideas) (clusters[kw.parent_topic || kw.keyword.split(" ")[0]] ??= []).push(kw);
const topClusters = Object.entries(clusters)
.map(([t, kws]) => ({ t, kws, vol: kws.reduce((s, k) => s + k.volume, 0) }))
.sort((a, b) => b.vol - a.vol).slice(0, 8);
console.log(`Keywords: ${ideas.length} | Clusters: ${Object.keys(clusters).length}\n`);
const bv = await fetch(`${MB}/brand-voices`, { method: "POST", headers: MH,
body: JSON.stringify({ name: "SEO Content Voice",
description: "Clear, authoritative, data-backed. For practitioners.",
samples: ["73% skip validation. Here's the fix.",
"1,200 top pages analyzed. Pattern: data-first, opinion-second."] }),
}).then((r) => r.json());
for (const { t, kws, vol } of topClusters) {
const sorted = kws.sort((a, b) => b.volume - a.volume);
const avgKd = Math.round(kws.reduce((s, k) => s + (k.keyword_difficulty || 0), 0) / kws.length);
const kwList = sorted.slice(0, 12).map((k) =>
`- ${k.keyword} (vol: ${k.volume}, KD: ${k.keyword_difficulty || 0})`).join("\n");
const gen = await fetch(`${MB}/generations`, { method: "POST", headers: MH,
body: JSON.stringify({
prompt: `SEO plan for '${t}'\nVol: ${vol} | KD: ${avgKd}\nKEYWORDS:\n${kwList}\n\n`
+ `1) Title 2) Meta 3) KWs 4) Outline 5) Word count 6) SERP features 7) Links`,
brand_voice_id: bv.id }),
}).then((r) => r.json());
console.log(`=== ${t} (${kws.length} kws, ${vol.toLocaleString()} vol) ===`);
console.log((gen.output || gen.content || "").slice(0, 600), "\n");
await new Promise((r) => setTimeout(r, 1000));
}
const OpenAI = (await import("openai")).default;
const mavera = new OpenAI({ baseURL: MB, apiKey: MV });
const chat = await mavera.responses.create({ model: "mave",
input: [{ role: "user", content: `Summarize: ${topClusters.length} clusters, `
+ `${topClusters.reduce((s, c) => s + c.kws.length, 0)} keywords. `
+ `2-week execution schedule by volume and difficulty.` }] });
console.log("=== Schedule ===\n" + chat.output[0].content[0].text);
Example Output
{
"total_keywords": 200,
"clusters_processed": 8,
"sample_cluster": {
"topic": "content marketing strategy",
"keywords": 18, "total_volume": 24600, "avg_difficulty": 42,
"title": "Content Marketing Strategy: A Data-Driven Framework for 2026",
"primary_keyword": "content marketing strategy",
"word_count": 3200,
"outline": ["Why Most Strategies Fail", "The Data-First Framework", "Audience Validation", "Topic Clustering", "Production Calendar", "Measurement"]
},
"schedule": {
"week_1": ["content marketing strategy (24.6k vol)", "audience research tools (12.1k vol)"],
"week_2": ["content testing (8.4k vol)", "brand voice AI (6.2k vol)"]
}
}
Error Handling
No keyword ideas returned
No keyword ideas returned
The seed keywords may be too specific or the country database too small. Try broader seeds or a larger market (us, uk, de). Ahrefs requires a minimum of 50 units per request even for empty results.
Parent topic is null for some keywords
Parent topic is null for some keywords
Not all keywords have a parent_topic assigned in Ahrefs’ database. The code falls back to the first word of the keyword — for production, implement proper NLP-based clustering or use Ahrefs’ built-in clustering when available.