Install any skill in seconds. Free to start, no credit card required.
Get Started Free →GSC-driven content calendar. Pulls real Search Console data, finds the highest click-potential opportunities (striking-distance queries at positions 5-20, unanswered query intent, related-keyword expansions), and produces a dated, prioritized content calendar — ready to hand to /content-writer. Use when the user asks "plan my content", "what should I write next", "content calendar", "content plan", "content roadmap", "editorial calendar", "what topics will rank", "find quick-win SEO topics", "cl
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-10 | ✗→✓ | ▲ Improved | 97% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 64% | 0% |
| case-13 | ✗→✓ | ▲ Improved | 94% | 0% |
| case-14 | ✗→✓ | ▲ Improved | 77% | 0% |
| case-17 | ✗→✓ | ▲ Improved | 118% | 0% |
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 universecontent-planner (this skill) — start from GSC, prioritize your opportunities, schedule themcontent-writer — take one planned topic and write the full postRead 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.
Pull a wide net once, filter in memory. Fewer round-trips, better correlation.
For the chosen GSC property, fetch last 90 days of:
that lets you reason about intent (page is the answer surface, query is the demand).
attract a lot of impressions but underperform CTR.
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.
For every (query, page) row, classify into one of these buckets. Discard rows that don't fit any bucket — noise.
exact intent can move it into the top 5
you don't have third-party volume)
in {data_dir}/business-context.json if present
that position (use the standard CTR-by-position curve from references/planning-methodology.md)
meta-tags-optimizer forthe title/description rewrite.
long-tails, "vs"/"alternative" variants, question forms). Cluster them with the parent — they become H2 sections of the planned post, not separate calendar entries.
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.
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; informationalposts cluster lower than transactional)
currentCtr = actual GSC CTR for this query, or 0 if the site doesn't rankSort 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.
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 | 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": "<ISO 8601 UTC>", "site": "<GSC property URL>", "lookbackDays": 90, "horizonWeeks": 12, "topics": [ /* one per scheduled post */ ], "warnings": [ /* cannibalization, missing business context, etc. */ ] }
Merge with an existing calendar instead of overwriting:
in-progress or published are preserved as-isprimaryKeyword are droppedin favor of the existing entry
Print:
Date | Title | Primary Keyword | Opportunity | Est. Clicks Gained | P
{data_dir}/content-calendar.jsonnotfair-content-calendar [--port 8323] [--calendar {data_dir}/content-calendar.json]> Open the calendar in your browser: > > bash > ~/.claude/plugins/cache/nowork-studio/notfair/<version>/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.
/seo-analysis for the canonical-page fix first/meta-tags-optimizer for the title/description rewrite/content-writer with the pre-built writerPromptRefuse to ship the calendar if any of these are true:
speculation; tell the user so and stop
the JSON
primaryKeyword → cannibalizationby your own plan; collapse or de-prioritize one
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.
Other measured skills in the registry, with their headline benchmark lift.