> ## 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 Gap → Content Generation Pipeline

> Find competitor keyword gaps with SEMrush and generate content briefs for each cluster with Mavera

### Scenario

Find keywords your competitors rank for but you don't. Pull `domain_domains` comparing your domain against a competitor, parse semicolon-delimited results, filter for high-volume / low-difficulty opportunities, cluster by topic, then send each cluster to Mavera for a content brief.

### Architecture

```mermaid theme={"dark"}
flowchart LR
    A["SEMrush domain_domains (your domain vs competitor)"] --> B["Parse semicolon-delimited"]
    B --> C["Filter high vol / low KD"]
    C --> D["Cluster by root term"]
    D --> E["POST /api/v1/mave/chat per cluster"]
```

### Code

<CodeGroup>
  ```python Python theme={"dark"}
  import os, requests, csv, io, time
  from collections import defaultdict

  SR, MV = os.environ["SEMRUSH_API_KEY"], os.environ["MAVERA_API_KEY"]
  MB = "https://app.mavera.io/api/v1"
  MH = {"Authorization": f"Bearer {MV}", "Content-Type": "application/json"}

  resp = requests.get("https://api.semrush.com/", params={
      "type": "domain_domains", "key": SR,
      "domains": "-|OR|yourdomain.com|*|OR|competitor.com",
      "database": "us", "display_limit": 200,
      "display_filter": "+|Kd|Lt|60|+|Nq|Gt|100",
      "export_columns": "Ph,Nq,Kd,Co,Nr",
  })
  reader = csv.reader(io.StringIO(resp.text), delimiter=";")
  next(reader)
  gaps = [{"keyword": r[0], "volume": int(r[1]), "difficulty": int(r[2])}
          for r in reader if len(r) >= 5]
  gaps.sort(key=lambda g: g["volume"], reverse=True)

  clusters = defaultdict(list)
  for g in gaps:
      clusters[g["keyword"].split()[0]].append(g)
  top = sorted(clusters.items(), key=lambda c: sum(g["volume"] for g in c[1]), reverse=True)[:5]

  for root, kws in top:
      kw_block = "\n".join(f"- {g['keyword']} (vol: {g['volume']}, KD: {g['difficulty']})"
                           for g in sorted(kws, key=lambda x: -x["volume"])[:10])
      vol = sum(g["volume"] for g in kws)
      mave = requests.post(f"{MB}/mave/chat", headers=MH, json={
          "message": f"Content brief for cluster '{root}' ({len(kws)} kws, {vol} vol).\n\n"
                     f"KEYWORDS:\n{kw_block}\n\nGenerate: 1) SEO title 2) Meta desc "
                     f"3) Primary + secondary kws 4) H2/H3 outline 5) Word count 6) CTA",
      }).json()
      print(f"=== {root} ({len(kws)} kws, {vol} vol) ===")
      print(mave.get("content", "")[:600], "\n")
      time.sleep(0.5)
  ```

  ```javascript JavaScript theme={"dark"}
  const SR = process.env.SEMRUSH_API_KEY, MV = process.env.MAVERA_API_KEY;
  const MB = "https://app.mavera.io/api/v1";
  const MH = { Authorization: `Bearer ${MV}`, "Content-Type": "application/json" };

  const params = new URLSearchParams({
    type: "domain_domains", key: SR,
    domains: "-|OR|yourdomain.com|*|OR|competitor.com",
    database: "us", display_limit: "200",
    display_filter: "+|Kd|Lt|60|+|Nq|Gt|100", export_columns: "Ph,Nq,Kd,Co,Nr",
  });
  const text = await fetch(`https://api.semrush.com/?${params}`).then((r) => r.text());
  const gaps = text.trim().split("\n").slice(1).map((line) => {
    const c = line.split(";");
    return c.length >= 5 ? { keyword: c[0], volume: parseInt(c[1]), difficulty: parseInt(c[2]) } : null;
  }).filter(Boolean).sort((a, b) => b.volume - a.volume);

  const clusters = {};
  for (const g of gaps) (clusters[g.keyword.split(" ")[0]] ??= []).push(g);
  const top = Object.entries(clusters)
    .map(([r, kws]) => ({ r, kws, vol: kws.reduce((s, g) => s + g.volume, 0) }))
    .sort((a, b) => b.vol - a.vol).slice(0, 5);

  for (const { r, kws, vol } of top) {
    const block = kws.sort((a, b) => b.volume - a.volume).slice(0, 10)
      .map((g) => `- ${g.keyword} (vol: ${g.volume}, KD: ${g.difficulty})`).join("\n");
    const mave = await fetch(`${MB}/mave/chat`, { method: "POST", headers: MH,
      body: JSON.stringify({
        message: `Brief for '${r}' (${kws.length} kws, ${vol} vol).\nKEYWORDS:\n${block}\nGenerate: 1) Title 2) Meta 3) Keywords 4) Outline 5) Word count 6) CTA`,
      }),
    }).then((r) => r.json());
    console.log(`=== ${r} (${kws.length} kws, ${vol} vol) ===`);
    console.log((mave.content || "").slice(0, 600), "\n");
    await new Promise((r) => setTimeout(r, 500));
  }
  ```
</CodeGroup>

### Example Output

```json theme={"dark"}
{
  "keyword_gaps_found": 147,
  "clusters_processed": 5,
  "sample_brief": {
    "cluster": "content", "keywords": 23, "total_volume": 18400,
    "title": "Content Marketing Strategy: The 2026 Playbook for B2B Teams",
    "primary_keyword": "content marketing strategy", "word_count": 2500
  }
}
```

### Error Handling

<AccordionGroup>
  <Accordion title="ERROR 50 :: WRONG KEY">Your SEMrush API key is invalid or expired. Verify the key in your profile settings and check that your subscription is active.</Accordion>
  <Accordion title="ERROR 120 :: LIMIT REACHED">You've exhausted your monthly API units. Check your balance at SEMrush → Subscription Info. Cache responses locally to avoid repeated calls.</Accordion>
  <Accordion title="Empty response / no gaps">If the response has only a header row, no keyword gaps exist for these domains or the filters are too strict. Lower volume threshold or raise difficulty ceiling.</Accordion>
  <Accordion title="Semicolons in keyword text">Rarely, a keyword contains a semicolon. Python's `csv.reader` handles quoted fields; in JavaScript, verify column count before parsing.</Accordion>
</AccordionGroup>
