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

# Post Engagement → Content Strategy

> Analyze LinkedIn Company Page post engagement, identify winning patterns via Mave, and generate 10 new data-backed content concepts

## Scenario

Your Company Page publishes 10–20 posts per month, but nobody analyzes which topics and formats actually work. This job pulls recent company posts along with their engagement metrics (likes, comments, shares), ranks them by total engagement, identifies thematic patterns across top performers, then asks Mave to explain what the winners have in common and generate 10 new content concepts that follow those patterns. You get a data-backed content calendar instead of brainstorming in a vacuum.

## Architecture

```mermaid theme={"dark"}
flowchart LR
    A["LinkedIn GET /posts (by author org)"] --> B["GET /socialActions/{postUrn}"]
    B --> C["Rank by engagement"]
    C --> D["Mavera POST /mave/chat (theme analysis)"]
    D --> E["Content strategy output"]
```

## Code

<CodeGroup>
  ```python Python theme={"dark"}
  import os, requests, time

  LI = os.environ["LINKEDIN_ACCESS_TOKEN"]
  MV = os.environ["MAVERA_API_KEY"]
  LI_BASE = "https://api.linkedin.com/rest"
  MV_BASE = "https://app.mavera.io/api/v1"
  LI_H = {"Authorization": f"Bearer {LI}", "LinkedIn-Version": "202401", "X-Restli-Protocol-Version": "2.0.0"}
  MV_H = {"Authorization": f"Bearer {MV}", "Content-Type": "application/json"}

  ORG_URN = "urn:li:organization:12345678"

  # 1. Pull recent company posts
  r = requests.get(f"{LI_BASE}/posts",
      headers=LI_H,
      params={
          "q": "author",
          "author": ORG_URN,
          "count": 50,
          "sortBy": "LAST_MODIFIED",
      })
  if r.status_code == 429:
      time.sleep(int(r.headers.get("Retry-After", 60)))
      r = requests.get(f"{LI_BASE}/posts", headers=LI_H,
          params={"q": "author", "author": ORG_URN, "count": 50, "sortBy": "LAST_MODIFIED"})
  r.raise_for_status()
  posts = r.json().get("elements", [])

  # 2. Fetch engagement for each post
  enriched = []
  for post in posts:
      post_urn = post.get("id", "")
      commentary = post.get("commentary", "")
      if not commentary:
          continue

      sr = requests.get(f"{LI_BASE}/socialActions/{post_urn}",
          headers=LI_H)
      if sr.status_code == 429:
          time.sleep(int(sr.headers.get("Retry-After", 30)))
          sr = requests.get(f"{LI_BASE}/socialActions/{post_urn}", headers=LI_H)

      if sr.ok:
          actions = sr.json()
          likes = actions.get("likesSummary", {}).get("totalLikes", 0)
          comments = actions.get("commentsSummary", {}).get("totalFirstLevelComments", 0)
          shares = actions.get("sharesSummary", {}).get("totalShares", 0)
      else:
          likes = comments = shares = 0

      total_engagement = likes + comments * 3 + shares * 5
      enriched.append({
          "urn": post_urn,
          "commentary": commentary[:500],
          "content_type": post.get("content", {}).get("contentType", "NONE"),
          "likes": likes,
          "comments": comments,
          "shares": shares,
          "engagement_score": total_engagement,
      })
      time.sleep(0.4)

  # 3. Rank and take top performers
  ranked = sorted(enriched, key=lambda x: -x["engagement_score"])
  top = ranked[:10]
  bottom = ranked[-5:] if len(ranked) >= 15 else []

  # 4. Build analysis prompt
  top_block = "\n\n---\n\n".join(
      f"POST {i+1} (engagement: {p['engagement_score']} | {p['likes']} likes, {p['comments']} comments, {p['shares']} shares | {p['content_type']}):\n{p['commentary']}"
      for i, p in enumerate(top)
  )
  bottom_block = "\n\n---\n\n".join(
      f"LOW {i+1} (engagement: {p['engagement_score']}):\n{p['commentary']}"
      for i, p in enumerate(bottom)
  ) if bottom else "No low-performing posts to compare."

  # 5. Ask Mave for theme analysis + content generation
  analysis = requests.post(f"{MV_BASE}/mave/chat", headers=MV_H, json={
      "message": f"""Analyze these LinkedIn Company Page posts. I'm giving you the top 10 performers and bottom 5.

  TOP PERFORMERS:
  {top_block}

  LOW PERFORMERS:
  {bottom_block}

  Produce:
  1. THEME ANALYSIS: What do the top posts have in common? (format, length, hooks, tone, topics)
  2. ANTI-PATTERNS: What do underperformers share?
  3. CONTENT FORMULA: A repeatable structure based on the winners
  4. NEW CONCEPTS: Generate 10 new LinkedIn post concepts that follow the winning patterns. For each: topic, hook (first line), format (text-only/carousel/video/poll), and why it should work based on the data.
  5. POSTING CADENCE: Recommended frequency and timing based on content type distribution."""
  }).json()

  print("=" * 60)
  print("LINKEDIN CONTENT STRATEGY — DATA-DRIVEN")
  print("=" * 60)
  print(f"Analyzed {len(enriched)} posts | Top engagement: {top[0]['engagement_score']}")
  print(f"Avg engagement (top 10): {sum(p['engagement_score'] for p in top) / len(top):.0f}")
  print(f"Avg engagement (all): {sum(p['engagement_score'] for p in enriched) / len(enriched):.0f}")
  print()
  print(analysis.get("content", "")[:2000])
  ```

  ```javascript JavaScript theme={"dark"}
  const LI = process.env.LINKEDIN_ACCESS_TOKEN;
  const MV = process.env.MAVERA_API_KEY;
  const LI_BASE = "https://api.linkedin.com/rest";
  const MV_BASE = "https://app.mavera.io/api/v1";
  const LI_H = { Authorization: `Bearer ${LI}`, "LinkedIn-Version": "202401", "X-Restli-Protocol-Version": "2.0.0" };
  const MV_H = { Authorization: `Bearer ${MV}`, "Content-Type": "application/json" };

  const ORG_URN = "urn:li:organization:12345678";

  // 1. Pull recent company posts
  let res = await fetch(
    `${LI_BASE}/posts?q=author&author=${encodeURIComponent(ORG_URN)}&count=50&sortBy=LAST_MODIFIED`,
    { headers: LI_H }
  );
  if (res.status === 429) {
    await new Promise(r => setTimeout(r, parseInt(res.headers.get("Retry-After") || "60", 10) * 1000));
    res = await fetch(
      `${LI_BASE}/posts?q=author&author=${encodeURIComponent(ORG_URN)}&count=50&sortBy=LAST_MODIFIED`,
      { headers: LI_H }
    );
  }
  if (!res.ok) throw new Error(`LinkedIn ${res.status}: ${await res.text()}`);
  const posts = (await res.json()).elements || [];

  // 2. Fetch engagement per post
  const enriched = [];
  for (const post of posts) {
    const commentary = post.commentary || "";
    if (!commentary) continue;

    const sr = await fetch(`${LI_BASE}/socialActions/${post.id}`, { headers: LI_H });
    let likes = 0, comments = 0, shares = 0;
    if (sr.ok) {
      const actions = await sr.json();
      likes = actions.likesSummary?.totalLikes || 0;
      comments = actions.commentsSummary?.totalFirstLevelComments || 0;
      shares = actions.sharesSummary?.totalShares || 0;
    }

    enriched.push({
      urn: post.id, commentary: commentary.slice(0, 500),
      content_type: post.content?.contentType || "NONE",
      likes, comments, shares,
      engagement_score: likes + comments * 3 + shares * 5,
    });
    await new Promise(r => setTimeout(r, 400));
  }

  // 3. Rank
  const ranked = enriched.sort((a, b) => b.engagement_score - a.engagement_score);
  const top = ranked.slice(0, 10);
  const bottom = ranked.length >= 15 ? ranked.slice(-5) : [];

  // 4. Build prompt
  const topBlock = top.map((p, i) =>
    `POST ${i + 1} (engagement: ${p.engagement_score} | ${p.likes} likes, ${p.comments} comments, ${p.shares} shares | ${p.content_type}):\n${p.commentary}`
  ).join("\n\n---\n\n");

  const bottomBlock = bottom.length
    ? bottom.map((p, i) => `LOW ${i + 1} (engagement: ${p.engagement_score}):\n${p.commentary}`).join("\n\n---\n\n")
    : "No low-performing posts to compare.";

  // 5. Mave analysis
  const analysis = await fetch(`${MV_BASE}/mave/chat`, {
    method: "POST", headers: MV_H,
    body: JSON.stringify({
      message: `Analyze these LinkedIn Company Page posts. Top 10 and bottom 5.\n\nTOP PERFORMERS:\n${topBlock}\n\nLOW PERFORMERS:\n${bottomBlock}\n\nProduce:\n1. THEME ANALYSIS: What do top posts have in common?\n2. ANTI-PATTERNS: What do underperformers share?\n3. CONTENT FORMULA: Repeatable structure from winners\n4. NEW CONCEPTS: 10 new post concepts following winning patterns (topic, hook, format, rationale)\n5. POSTING CADENCE: Recommended frequency and timing`,
    }),
  }).then(r => r.json());

  console.log("=".repeat(60));
  console.log("LINKEDIN CONTENT STRATEGY — DATA-DRIVEN");
  console.log("=".repeat(60));
  const avgTop = top.reduce((s, p) => s + p.engagement_score, 0) / (top.length || 1);
  const avgAll = enriched.reduce((s, p) => s + p.engagement_score, 0) / (enriched.length || 1);
  console.log(`Analyzed ${enriched.length} posts | Top: ${top[0]?.engagement_score} | Avg top 10: ${avgTop.toFixed(0)} | Avg all: ${avgAll.toFixed(0)}`);
  console.log();
  console.log((analysis.content || "").slice(0, 2000));
  ```
</CodeGroup>

## Example Output

```text theme={"dark"}
============================================================
LINKEDIN CONTENT STRATEGY — DATA-DRIVEN
============================================================
Analyzed 42 posts | Top engagement: 847 | Avg top 10: 423 | Avg all: 112

## 1. Theme Analysis
Top performers share three patterns:
- **Personal hooks**: Posts opening with "I" or a story outperform by 4x
- **Data specificity**: "60% faster" beats "significantly faster" every time
- **Carousel format**: 7 of 10 top posts use document/carousel attachments
- **Length**: Winners average 180–250 words (not short, not long)

## 2. Anti-Patterns
Low performers: product announcements with no context, generic industry
commentary, posts that read like press releases.

## 3. Content Formula
Hook (personal/provocative) → Context (1-2 sentences) → Insight with data →
Takeaway → CTA question

## 4. New Concepts
1. "We analyzed 500 customer calls. Here's what nobody talks about."
   Format: Carousel | Hook: contrarian insight | Rationale: data + surprise
2. "The metric we stopped tracking (and what we watch instead)"
   Format: Text-only | Hook: anti-conventional | Rationale: personal + specific
3. "Our biggest marketing mistake this quarter — a thread"
   Format: Text | Hook: vulnerability | Rationale: authenticity pattern
...

## 5. Cadence
3–4 posts/week. Carousels Tue/Thu. Text-only Mon. Polls sparingly (1/month).
```

## Error Handling

<AccordionGroup>
  <Accordion title="Community Management API required">The `GET /posts` and `GET /socialActions` endpoints require the Community Management API product. Without approval, calls return 403.</Accordion>
  <Accordion title="Post URN format varies">Post URNs can be `urn:li:share:{id}` or `urn:li:ugcPost:{id}` depending on creation method. The `socialActions` endpoint accepts both formats.</Accordion>
  <Accordion title="Engagement weighting">The code weights comments at 3x and shares at 5x vs. likes at 1x. Adjust these multipliers based on your funnel — if shares drive traffic, weight higher.</Accordion>
  <Accordion title="Rate limit on socialActions">Fetching engagement per post can burn rate limits fast on 50+ posts. The 400ms delay prevents throttling. For larger volumes, cache engagement data daily.</Accordion>
</AccordionGroup>

***

<CardGroup cols={2}>
  <Card title="LinkedIn Content Integration" icon="arrow-left" href="/integrations/linkedin-content" />

  <Card title="Mave Agent" icon="brain" href="/features/mave-agent" />
</CardGroup>
