content-planner
**content-planner** This Claude Code skill analyzes a site's Google Search Console data to identify high-value content opportunities and generates a prioritized publishing calendar. It pulls 90 days of query-page performance data, classifies opportunities into buckets such as striking-distance queries and content gaps, then outputs a structured JSON calendar and markdown summary for strategic content execution. Use it when you have GSC access and need to prioritize what to write based on actual search demand and ranking potential rather than speculative keyword research.
git clone --depth 1 https://github.com/nowork-studio/NotFair /tmp/content-planner && cp -r /tmp/content-planner/seo/content-planner ~/.claude/skills/content-plannerSKILL.md
# 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 **< 50% of expected** for
that position (use the standard CTR-by-position curve from
`references/planning-methodology.md`)
- Output a *refresh* task, not a new post. Route to `meta-tags-optimizer` for
the title/description rewrite.
### D. Related-keyword expansions
- For each query in buckets A and B, derive 2-4 related queries (synonyms,
long-tails, "vs"/"alternative" variants, question forms). Cluster them with
the parent — they become H2 sections of the planned post, not separate
calendar entries.
### E. Cannibalization warning (planning blocker)
- Same query, **multiple pages on the same site rank** with > 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": "<slug>",
"title": "<hook-driven title, ≤ 60 chars>",
"primaryKeyword": "<from GSC>",
"secondaryKeywords": ["<related cluster from Step 3D>"],
"intent": "informational|commercial",
"type": "blog|landing|refresh",
"opportunity": "striking-distance|gap|ctr-underperformer|related-expansion",
"scheduledDate": "<YYYY-MM-DD>",
"status": "planned",
"priority": "P0|P1|P2",
"gsc": {
"currentPosition": <number>,
"impressions90d": <number>,
"currentCtr": <number 0-1>,
"clickPotential": <number>
},
"rationale": "<one sentence: why this topic, why now>",
"writerPrompt": "<the exact prompt to paste into /content-writer when it's time to write>",
"refreshTarget": "<URL of existing page, only set when type=refresh>",
"bodyPath": "<relative path to written markdown body, set after /content-writer runs>",
"metaDescription": "<set after /content-writer runs>",
"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>
Google Ads account audit and business context setup. Run this first — it gathers business information, analyzes account health, and saves context that all other ads skills reuse. Trigger on "audit my ads", "ads audit", "set up my ads", "onboard", "account overview", "how's my account", "ads health check", "what should I fix in my ads", or when the user is new to NotFair and hasn't run an audit before. Also trigger proactively when other ads skills detect that business-context.json is missing.
Generate and A/B test Google Ads copy. Use when asked to write ad copy, headlines, descriptions, create ad variants, test ad messaging, improve CTR, or generate RSA (Responsive Search Ad) components. Trigger on "ad copy", "write ads", "headlines", "descriptions", "RSA", "responsive search ad", "ad text", "ad creative", "improve CTR", "ad A/B test", "ad variants", "write me an ad", "ad variation experiment", or when the user wants to improve click-through rate on existing ads.
Score and diagnose Google Ads landing pages. Use when asked to audit a landing page, check landing page quality, diagnose high-CTR but low-conversion-rate ad groups, improve Quality Score's Landing Page Experience component, or compare an ad group's messaging against its landing page. Trigger on "landing page audit", "landing page score", "landing page quality", "why is my conversion rate low", "LPX", "landing page experience", "ad to page match", or when `/google-ads-audit` surfaces a high-CTR / low-CVR ad group.
Manage Google Ads — performance, keywords, bids, budgets, negatives, campaigns, ads, search terms, QS, location targeting, bulk operations, experiments, asset management, portfolio bidding, offline conversions. Use for any mention of Google Ads, CPA, ROAS, ad spend, or campaign settings.
Meta Ads (Facebook + Instagram) account audit and business context setup. Run this first — it gathers business information, analyzes account health, and saves context that all other Meta ads skills reuse. Trigger on "audit my Meta ads", "audit my Facebook ads", "Meta ads audit", "set up my Meta ads", "onboard Meta", "Meta account overview", "how's my Meta account", "Meta health check", "what should I fix in my Facebook ads", or when the user is new to NotFair Meta and hasn't run an audit before. Also trigger proactively when other Meta ads skills detect that meta business-context.json is missing.
Manage Meta Ads (Facebook + Instagram) — performance, ROAS, CPM, frequency, audience overlap, learning phase, creative fatigue, budgets, ad sets, campaigns, ads. Use for any mention of Meta Ads, Facebook Ads, Instagram Ads, ROAS, CPM, ad spend, or campaign settings on Meta.
>