> ## 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 Clusters → Focus Group Validation

> Build topic clusters from SEMrush related keywords and validate content concepts with Mavera Focus Groups

### Scenario

Start with a seed keyword, build topic clusters from `phrase_related`, create a target reader persona per cluster, then validate content concepts with a focus group before writing.

### Architecture

```mermaid theme={"dark"}
flowchart LR
    A["SEMrush phrase_related (seed keywords)"] --> B["Cluster by root terms / intent"]
    B --> C["POST /api/v1/personas"]
    C --> D["POST /api/v1/focus-groups"]
```

### 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"}
  SEED = "content marketing automation"

  resp = requests.get("https://api.semrush.com/", params={
      "type": "phrase_related", "key": SR, "phrase": SEED,
      "database": "us", "display_limit": 100, "export_columns": "Ph,Nq,Kd,Co",
  })
  reader = csv.reader(io.StringIO(resp.text), delimiter=";")
  next(reader)
  kws = [{"keyword": r[0], "volume": int(r[1] or 0), "difficulty": int(r[2] or 0)}
         for r in reader if len(r) >= 4]

  intent_map = {"how": "info", "best": "commercial", "tool": "transactional", "software": "transactional"}
  stops = {"how", "to", "what", "is", "the", "a", "for", "and", "best", "top"}
  clusters = defaultdict(list)
  for kw in kws:
      words = kw["keyword"].lower().split()
      intent = next((intent_map[w] for w in words if w in intent_map), "info")
      topic = next((w for w in words if w not in stops), words[0])
      clusters[f"{intent}:{topic}"].append(kw)

  top = sorted(clusters.items(), key=lambda c: sum(k["volume"] for k in c[1]), reverse=True)[:4]

  pids = []
  for key, group in top:
      intent, topic = key.split(":", 1)
      p = requests.post(f"{MB}/personas", headers=MH, json={
          "name": f"Reader: {intent} — {topic}",
          "description": f"Searches for {intent} content about '{topic}'. "
                         f"{len(group)} kws, {sum(k['volume'] for k in group)} vol.",
      }).json()
      pids.append(p["id"])
      time.sleep(0.3)

  concepts = [f"{k.split(':')[0]} about '{k.split(':')[1]}': "
              + ", ".join(kw["keyword"] for kw in sorted(v, key=lambda x: -x["volume"])[:5])
              for k, v in top]

  fg = requests.post(f"{MB}/focus-groups", headers=MH, json={
      "name": f"Cluster Validation: {SEED}", "persona_ids": pids,
      "questions": [
          "Which concept would you click first?\n" + "\n".join(f"{i+1}. {c}" for i, c in enumerate(concepts)),
          "What question must a blog post answer for you to read it fully?",
          "Comprehensive guide (3000+ words) or quick checklist? Why?",
      ], "responses_per_persona": 2,
  }).json()

  for _ in range(24):
      time.sleep(5)
      data = requests.get(f"{MB}/focus-groups/{fg['id']}", headers=MH).json()
      if data.get("status") == "completed": break
  for r in data.get("responses", [])[:8]:
      print(f"[{r.get('persona_id','?')}] {r.get('answer','')[:250]}\n")
  ```

  ```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 SEED = "content marketing automation";

  const params = new URLSearchParams({
    type: "phrase_related", key: SR, phrase: SEED,
    database: "us", display_limit: "100", export_columns: "Ph,Nq,Kd,Co",
  });
  const text = await fetch(`https://api.semrush.com/?${params}`).then((r) => r.text());
  const kws = text.trim().split("\n").slice(1).map((line) => {
    const c = line.split(";");
    return c.length >= 4 ? { keyword: c[0], volume: parseInt(c[1]) || 0, difficulty: parseInt(c[2]) || 0 } : null;
  }).filter(Boolean);

  const iMap = { how: "info", best: "commercial", tool: "transactional", software: "transactional" };
  const stops = new Set(["how","to","what","is","the","a","for","and","best","top"]);
  const clusters = {};
  for (const kw of kws) {
    const w = kw.keyword.toLowerCase().split(" ");
    const intent = w.find((x) => iMap[x]) ? iMap[w.find((x) => iMap[x])] : "info";
    const topic = w.find((x) => !stops.has(x)) || w[0];
    (clusters[`${intent}:${topic}`] ??= []).push(kw);
  }
  const top = Object.entries(clusters)
    .map(([k, g]) => ({ k, g, vol: g.reduce((s, x) => s + x.volume, 0) }))
    .sort((a, b) => b.vol - a.vol).slice(0, 4);

  const pids = [];
  for (const { k, g, vol } of top) {
    const [intent, topic] = k.split(":");
    const p = await fetch(`${MB}/personas`, { method: "POST", headers: MH,
      body: JSON.stringify({ name: `Reader: ${intent} — ${topic}`,
        description: `${intent} searcher, '${topic}'. ${g.length} kws, ${vol} vol.` }),
    }).then((r) => r.json());
    pids.push(p.id);
    await new Promise((r) => setTimeout(r, 300));
  }

  const concepts = top.map(({ k, g }) => {
    const [i, t] = k.split(":");
    return `${i} about '${t}': ${g.sort((a, b) => b.volume - a.volume).slice(0, 5).map((x) => x.keyword).join(", ")}`;
  });
  const fg = await fetch(`${MB}/focus-groups`, { method: "POST", headers: MH,
    body: JSON.stringify({
      name: `Cluster: ${SEED}`, persona_ids: pids,
      questions: ["Which first?\n" + concepts.map((c, i) => `${i+1}. ${c}`).join("\n"),
        "What question must a post answer for you to finish it?",
        "Long guide or quick checklist? Why?"],
      responses_per_persona: 2,
    }),
  }).then((r) => r.json());

  let data;
  for (let i = 0; i < 24; i++) {
    await new Promise((r) => setTimeout(r, 5000));
    data = await fetch(`${MB}/focus-groups/${fg.id}`, { headers: MH }).then((r) => r.json());
    if (data.status === "completed") break;
  }
  for (const r of (data.responses || []).slice(0, 8))
    console.log(`[${r.persona_id}] ${(r.answer || "").slice(0, 250)}\n`);
  ```
</CodeGroup>

### Example Output

```text theme={"dark"}
Persona: info:content → per_cl_01 (18 kws, 12400 vol)
Persona: commercial:marketing → per_cl_02 (14 kws, 8900 vol)
Persona: transactional:tool → per_cl_03 (11 kws, 6200 vol)

[per_cl_01] #3 — transactional about tools. I already know what content
  marketing automation is. I want to compare platforms.
[per_cl_03] "How much time will this save me per week?" Concrete numbers.
[per_cl_02] Checklist first, guide linked. Scan in 2 minutes, then dive in.
```

### Error Handling

<AccordionGroup>
  <Accordion title="No related keywords">The seed keyword may be too niche or broad. Try variations. Check that the database parameter matches your target market.</Accordion>
  <Accordion title="Single-keyword clusters">If most clusters have 1-2 keywords, grouping is too granular. Use 2-word pairs instead of single root words.</Accordion>
  <Accordion title="Focus group polling timeout">4 personas × 3 questions × 2 responses = 24 generations. Expect 60-90s. Increase polling iterations for larger configurations.</Accordion>
</AccordionGroup>
