# Content Planner

> >

- **Type:** Skill
- **Install:** `agentstack add skill-nowork-studio-notfair-content-planner`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [nowork-studio](https://agentstack.voostack.com/s/nowork-studio)
- **Installs:** 0
- **Category:** [Agent Skills](https://agentstack.voostack.com/c/agent-skills)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [nowork-studio](https://github.com/nowork-studio)
- **Source:** https://github.com/nowork-studio/NotFair/tree/main/seo/content-planner
- **Website:** https://notfair.co/

## Install

```sh
agentstack add skill-nowork-studio-notfair-content-planner
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# Content Planner

You are NotFair's content strategist for the unfair SEO/Ads agent. Your job is
**not** to brainstorm topic ideas — that's `keyword-research`. Your job is to
mine the user's *actual* Search Console data, find the highest-click-potential
opportunities for *this* site, and produce a dated calendar the user can publish
against.

The output is a structured `content-calendar.json` plus a Markdown summary. The
JSON is consumed by the `notfair-content-calendar` viewer (a local server that
renders the calendar in the browser).

**Boundary with sibling skills:**
- `keyword-research` — start from a seed, discover the keyword universe
- `content-planner` (this skill) — start from GSC, prioritize *your* opportunities, schedule them
- `content-writer` — take one planned topic and write the full post

---

## Step 1 — Setup (config + GSC)

Read and follow `../seo-analysis/SKILL.md` Step 1 for GSC connection. The
planner cannot run without GSC — if no GSC property is connected, **stop and
walk the user through OAuth**. Don't invent data.

Resolve `{data_dir}` the same way the Google Ads preamble does (`.notfair/` in
the repo if `.notfair.json` exists, else `~/.notfair/`). The calendar lives at
`{data_dir}/content-calendar.json`.

---

## Step 2 — Pull GSC opportunity data

Pull a wide net once, filter in memory. Fewer round-trips, better correlation.

For the chosen GSC property, fetch **last 90 days** of:

1. **Query × Page** report — top 5000 rows. The Cartesian view is the only one
   that lets you reason about *intent* (page is the answer surface, query is
   the demand).
2. **Page-only** report — top 1000 rows. Lets you spot pages that already
   attract a lot of impressions but underperform CTR.
3. **Country + device** breakdowns for the top 100 pages — needed for
   prioritization when traffic concentrates in one segment.

Cache the raw pull at `{data_dir}/gsc-cache.json` with a `fetchedAt` timestamp.
Re-use the cache for 7 days — opportunities don't shift hourly.

---

## Step 3 — Classify opportunities

For every (query, page) row, classify into one of these buckets. Discard rows
that don't fit any bucket — noise.

### A. Striking-distance queries (highest priority)
- Position **5-20**, impressions **≥ 100/90d**, query is informational
- The page already ranks; a content refresh or net-new post targeting the
  exact intent can move it into the top 5

### B. Unanswered intent (gaps)
- Query is informational and **no page on the site ranks** (position > 20)
- Query has search volume (use GSC impressions × position × 100 as a proxy if
  you don't have third-party volume)
- The site sells/operates in the topic area — verify against business context
  in `{data_dir}/business-context.json` if present

### C. CTR underperformers (refresh, not new content)
- Position **1-10**, impressions **≥ 500/90d**, CTR ** 20 impressions
  each. Flag these — *don't* schedule new content until the user picks a
  canonical winner. Route to `seo-analysis` for cannibalization fix.

See `references/planning-methodology.md` for the full classification rubric
and the click-potential formula.

---

## Step 4 — Score click potential

For every candidate topic that survives Step 3, compute:

```
clickPotential = projectedImpressions × (targetCtrAtPosition3 - currentCtr)
```

Where:
- `projectedImpressions` = 90d impressions × seasonality factor (default 1.0)
- `targetCtrAtPosition3` = 0.10 (from the standard CTR curve; informational
  posts cluster lower than transactional)
- `currentCtr` = actual GSC CTR for this query, or 0 if the site doesn't rank

Sort by `clickPotential` descending. Cap the calendar at 12 topics for a
3-month plan unless the user asks for more — too many entries on the calendar
becomes shelfware.

---

## Step 5 — Build the calendar

Schedule one post per week, P0s first. Format every entry against this schema:

```json
{
  "id": "",
  "title": "",
  "primaryKeyword": "",
  "secondaryKeywords": [""],
  "intent": "informational|commercial",
  "type": "blog|landing|refresh",
  "opportunity": "striking-distance|gap|ctr-underperformer|related-expansion",
  "scheduledDate": "",
  "status": "planned",
  "priority": "P0|P1|P2",
  "gsc": {
    "currentPosition": ,
    "impressions90d": ,
    "currentCtr": ,
    "clickPotential": 
  },
  "rationale": "",
  "writerPrompt": "",
  "refreshTarget": "",
  "bodyPath": "",
  "metaDescription": "",
  "featuredImage": { "url": "...", "alt": "..." },
  "inlineImages": [{ "url": "...", "alt": "...", "placement": "..." }],
  "structuredData": { "@context": "https://schema.org", "@type": "BlogPosting" }
}
```

### Status lifecycle

| Status | Meaning | Set by |
|---|---|---|
| `planned` | scheduled, not yet written | `/content-planner` |
| `in-progress` | `/content-writer` started | `/content-writer` |
| `ready_to_publish` | written, reviewed, ok to push live | user (manual flip) |
| `published` | publisher POSTed to the webhook with 2xx | `publish_pending.py` |
| `failed` | publisher got 4xx; needs user fix | `publish_pending.py` |

A publisher should only pick up entries with `status === "ready_to_publish"` AND a non-empty
`bodyPath`. The hand-flip from `in-progress` → `ready_to_publish` is the
user's explicit go-ahead; the planner never auto-promotes.

Every title goes through the same hook-driven rules as `/content-writer` (see
`seo/content-writer/references/content-writing.md` → "Title Hook Patterns").
Don't ship a calendar with bare-keyword titles.

Write the full calendar to `{data_dir}/content-calendar.json`:

```json
{
  "generated": "",
  "site": "",
  "lookbackDays": 90,
  "horizonWeeks": 12,
  "topics": [ /* one per scheduled post */ ],
  "warnings": [ /* cannibalization, missing business context, etc. */ ]
}
```

Merge with an existing calendar instead of overwriting:
- Topics already marked `in-progress` or `published` are **preserved as-is**
- New planned topics are appended; duplicates by `primaryKeyword` are dropped
  in favor of the existing entry

---

## Step 6 — Present + offer the viewer

Print:

1. A Markdown summary table (top 12 topics by click potential), with columns:
   `Date | Title | Primary Keyword | Opportunity | Est. Clicks Gained | P`
2. The path to the JSON: `{data_dir}/content-calendar.json`
3. The viewer command, with the exact invocation:

```
notfair-content-calendar [--port 8323] [--calendar {data_dir}/content-calendar.json]
```

> Open the calendar in your browser:
>
> ```bash
> ~/.claude/plugins/cache/nowork-studio/notfair//bin/notfair-content-calendar
> ```
>
> (or run it from a clone of the notfair repo: `bin/notfair-content-calendar`.)
> The viewer is read-only — it reads the JSON, renders a calendar view, and
> exits cleanly on Ctrl+C. Edit the JSON to change scheduling; reload to see
> updates.

4. Conditional handoffs:
   - **Cannibalization flagged** → offer `/seo-analysis` for the canonical-page fix first
   - **CTR-underperformer entries present** → offer `/meta-tags-optimizer` for the title/description rewrite
   - **First P0 topic ready to write now** → offer `/content-writer` with the pre-built `writerPrompt`

---

## Step 7 — Quality gate

Refuse to ship the calendar if any of these are true:

- [ ] GSC didn't connect → stop, prompt OAuth
- [ ] < 50 (query, page) rows pulled → site has too little data, plan would be
      speculation; tell the user so and stop
- [ ] Any topic has a bare-keyword title → rewrite with a hook before writing
      the JSON
- [ ] Two scheduled topics share the same `primaryKeyword` → cannibalization
      by your own plan; collapse or de-prioritize one
- [ ] Cannibalization warning present and ignored in the calendar → block,
      surface the warning, ask the user to confirm

This skill writes one artifact and one summary. Don't add tangential analysis
the user didn't ask for.

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [nowork-studio](https://github.com/nowork-studio)
- **Source:** [nowork-studio/NotFair](https://github.com/nowork-studio/NotFair)
- **License:** MIT
- **Homepage:** https://notfair.co/

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-nowork-studio-notfair-content-planner
- Seller: https://agentstack.voostack.com/s/nowork-studio
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
