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

# Position Tracking → Performance Reports

> Generate executive SEO narrative reports from SEMrush position tracking data with Mavera

### Scenario

Report on ranking changes — which keywords improved, which dropped, and what to do. Pull current positions from `domain_organic`, compare against a stored baseline, then send change data to Mave for an executive-ready narrative report.

### Architecture

```mermaid theme={"dark"}
flowchart LR
    A["SEMrush domain_organic (current positions)"] --> B["Compare vs stored baseline"]
    B --> C["Classify movers"]
    C --> D["POST /api/v1/mave/chat"]
    D --> E["Executive narrative"]
```

### Code

<CodeGroup>
  ```python Python theme={"dark"}
  import os, requests, csv, io, json
  from pathlib import Path

  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"}
  DOMAIN, BF = "yourdomain.com", "semrush_baseline.json"

  resp = requests.get("https://api.semrush.com/", params={
      "type": "domain_organic", "key": SR, "domain": DOMAIN,
      "database": "us", "display_limit": 100, "display_sort": "tr_desc",
      "export_columns": "Ph,Po,Pp,Nq,Cp,Ur,Tr,Tc,Co,Kd",
  })
  reader = csv.reader(io.StringIO(resp.text), delimiter=";")
  next(reader)
  cur = {}
  for r in reader:
      if len(r) < 10: continue
      cur[r[0]] = {"kw": r[0], "pos": int(r[1] or 100), "prev": int(r[2] or 100),
                   "vol": int(r[3] or 0), "url": r[5], "tr": float(r[6] or 0)}

  bl = json.loads(Path(BF).read_text()) if Path(BF).exists() else {}
  up, down, new, lost = [], [], [], []
  for kw, d in cur.items():
      if kw in bl:
          d["chg"] = bl[kw]["pos"] - d["pos"]
          (up if d["chg"] > 0 else down if d["chg"] < 0 else []).append(d)
      else: d["chg"] = 0; new.append(d)
  for kw in bl:
      if kw not in cur: lost.append({"kw": kw, "pos": bl[kw]["pos"], "vol": bl[kw].get("vol", 0)})

  up.sort(key=lambda x: x["chg"], reverse=True)
  down.sort(key=lambda x: x["chg"])
  Path(BF).write_text(json.dumps({k: {"pos": v["pos"], "vol": v["vol"]} for k, v in cur.items()}, indent=2))

  def fmt(m):
      return "\n".join(f"- \"{x['kw']}\" (vol: {x['vol']}): {x.get('prev','?')} → {x['pos']} "
                       f"({'+' if x.get('chg',0)>0 else ''}{x.get('chg',0)})" for x in m[:10])

  total_tr = sum(d["tr"] for d in cur.values())
  t10 = sum(1 for d in cur.values() if d["pos"] <= 10)
  report = (f"RANKING: {DOMAIN} | KWs: {len(cur)} | Traffic: {total_tr:.0f} | Top10: {t10}\n"
            f"Up: {len(up)} | Down: {len(down)} | New: {len(new)} | Lost: {len(lost)}\n\n"
            f"IMPROVED:\n{fmt(up)}\n\nDECLINED:\n{fmt(down)}")

  mave = requests.post(f"{MB}/mave/chat", headers=MH, json={
      "message": f"Executive SEO report.\n\n{report}\n\n"
                 f"Include: 1) Summary (3 sentences) 2) Wins 3) Risks "
                 f"4) Priority actions for next 2 weeks. Plain language for VP Marketing.",
  }).json()
  print(mave.get("content", ""))
  ```

  ```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 fs = require("fs");
  const DOMAIN = "yourdomain.com", BF = "semrush_baseline.json";

  const params = new URLSearchParams({
    type: "domain_organic", key: SR, domain: DOMAIN,
    database: "us", display_limit: "100", display_sort: "tr_desc",
    export_columns: "Ph,Po,Pp,Nq,Cp,Ur,Tr,Tc,Co,Kd",
  });
  const text = await fetch(`https://api.semrush.com/?${params}`).then((r) => r.text());
  const cur = {};
  for (const line of text.trim().split("\n").slice(1)) {
    const c = line.split(";");
    if (c.length < 10) continue;
    cur[c[0]] = { kw: c[0], pos: parseInt(c[1]) || 100, prev: parseInt(c[2]) || 100,
      vol: parseInt(c[3]) || 0, url: c[5], tr: parseFloat(c[6]) || 0 };
  }

  let bl = {};
  try { bl = JSON.parse(fs.readFileSync(BF, "utf8")); } catch {}
  const up = [], down = [], nw = [], lost = [];
  for (const [kw, d] of Object.entries(cur)) {
    if (bl[kw]) { d.chg = bl[kw].pos - d.pos; (d.chg > 0 ? up : d.chg < 0 ? down : []).push(d); }
    else { d.chg = 0; nw.push(d); }
  }
  for (const kw of Object.keys(bl)) if (!cur[kw]) lost.push({ kw, pos: bl[kw].pos, vol: bl[kw].vol || 0 });
  up.sort((a, b) => b.chg - a.chg); down.sort((a, b) => a.chg - b.chg);
  const nb = {}; for (const [k, v] of Object.entries(cur)) nb[k] = { pos: v.pos, vol: v.vol };
  fs.writeFileSync(BF, JSON.stringify(nb, null, 2));

  const fmt = (a) => a.slice(0, 10).map((x) =>
    `- "${x.kw}" (vol: ${x.vol}): ${x.prev||"?"} → ${x.pos} (${x.chg>0?"+":""}${x.chg})`).join("\n");
  const tTr = Object.values(cur).reduce((s, d) => s + d.tr, 0);
  const t10 = Object.values(cur).filter((d) => d.pos <= 10).length;
  const report = `RANKING: ${DOMAIN} | KWs: ${Object.keys(cur).length} | Traffic: ${tTr.toFixed(0)} | Top10: ${t10}\nUp: ${up.length} | Down: ${down.length} | New: ${nw.length} | Lost: ${lost.length}\n\nIMPROVED:\n${fmt(up)}\n\nDECLINED:\n${fmt(down)}`;

  const mave = await fetch(`${MB}/mave/chat`, { method: "POST", headers: MH,
    body: JSON.stringify({
      message: `Executive SEO report.\n\n${report}\n\n1) Summary 2) Wins 3) Risks 4) 2-week actions. Plain language for VP.`,
    }),
  }).then((r) => r.json());
  console.log(mave.content || "");
  ```
</CodeGroup>

### Example Output

```text theme={"dark"}
Summary: 23 up, 14 down, 8 new. Traffic: 12,400/mo. 31 in top 10.

Wins: "marketing persona tool" 18→6 (+12, 2400 vol). "content testing
  platform" 24→11 (+13). Close to page 1 — backlinks needed.
Risks: "focus group software" 8→15 (-7). Lost ~800/mo. Check competitors.

Actions: 1) Audit /product/focus-groups 2) Build backlinks for "content
  testing platform" 3) Internal-link to new ranking pages 4) Weekly alerts
```

### Error Handling

<AccordionGroup>
  <Accordion title="First run has no baseline">On first run, all keywords appear as "new." Run twice with a gap (daily/weekly) for meaningful change data.</Accordion>
  <Accordion title="Position 0 for featured snippets">SEMrush may return position 0 for SERP features. The code defaults to 100 for unparseable values — adjust as needed.</Accordion>
  <Accordion title="Database mismatch">Match the `database` parameter to your target market. US rankings won't match UK Search Console data.</Accordion>
  <Accordion title="Stale baseline after migrations">Delete the baseline file and rebuild after domain migrations, URL restructures, or redirect changes.</Accordion>
</AccordionGroup>
