> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mavera.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Keyword Ideas → Generate at Scale

> Pull Ahrefs keyword suggestions, cluster by topic, and generate SEO content at scale with Mavera brand voice

### 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

```mermaid theme={"dark"}
flowchart LR
    A["Ahrefs /v3/keywords-explorer/keyword-ideas"] --> B["Parse JSON"]
    B --> C["Cluster by parent_topic"]
    C --> D["POST /api/v1/brand-voices"]
    D --> E["POST /api/v1/generations per cluster"]
```

### Code

<CodeGroup>
  ```python Python theme={"dark"}
  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)
  ```

  ```javascript JavaScript theme={"dark"}
  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);
  ```
</CodeGroup>

### Example Output

```json theme={"dark"}
{
  "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

<AccordionGroup>
  <Accordion title="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.</Accordion>
  <Accordion title="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.</Accordion>
</AccordionGroup>
