Claude Cursor Skill

content-planner

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 /

LLM Mart · 0 points · 21 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download nowork-studio-notfair-plugin-seo_content-planner-adf7601.zip · 8 KB
Part of nowork-studio/notfair-plugin — 88 skills

Install

skills CLI npx skills add https://github.com/nowork-studio/notfair-plugin/tree/main/seo/content-planner
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install nowork-studio-notfair-plugin@llmmart
Git git clone https://github.com/nowork-studio/notfair-plugin.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole nowork-studio/notfair-plugin collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

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:

{
  "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.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-progressready_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:

{
  "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:

  • 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:

~/.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.

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

Files (notfair-plugin)
  • evals
    • evals.json 2.9 KB
      {
        "skill_name": "content-planner",
        "evals": [
          {
            "id": 1,
            "prompt": "Plan my next 90 days of content based on Search Console data. My site is example.com — a small B2B accounting SaaS for freelancers.",
            "expected_output": "A GSC-driven content calendar with 8–12 prioritized topics, each tagged by opportunity bucket (striking-distance, gap, ctr-underperformer), scheduled across 12 weeks, with click-potential scores. The skill should write content-calendar.json to {data_dir} and tell the user how to launch the notfair-content-calendar viewer.",
            "files": [],
            "expectations": [
              "Skill checks for GSC connection before doing anything else",
              "Pulls 90-day query × page report, not just query-level",
              "Classifies opportunities into the 5 buckets (striking-distance, gap, ctr-underperformer, related-expansion, cannibalization)",
              "Computes clickPotential per topic using the documented formula",
              "Every scheduled title uses a hook pattern (number, audience, contrarian, or outcome-promise) — no bare keyword titles",
              "Writes content-calendar.json to {data_dir}/content-calendar.json with the documented schema",
              "Tells the user the exact command to run the notfair-content-calendar viewer",
              "Caps at 12 topics for a 90-day horizon unless user asks for more"
            ]
          },
          {
            "id": 2,
            "prompt": "I keep getting impressions for 'invoice software for freelancers' but I'm at position 14. What should I do?",
            "expected_output": "Recognizes this as a striking-distance opportunity (position 5–20 with impressions), classifies it as bucket A, suggests scheduling a focused post with the related-keyword expansions clustered as H2s, and offers to hand off to /content-writer with the pre-built writerPrompt.",
            "files": [],
            "expectations": [
              "Classifies as Bucket A: striking-distance",
              "Identifies 2–4 related-keyword expansions (Bucket D) as secondary keywords",
              "Computes clickPotential and surfaces the estimated additional clicks if the post reaches position 3",
              "Generates a hook-driven title, not 'Invoice Software For Freelancers'",
              "Hands off to /content-writer with a complete writerPrompt"
            ]
          },
          {
            "id": 3,
            "prompt": "My GSC shows 3 different pages on my site all ranking for 'time tracking tips' between positions 12 and 18. Should I write a new post?",
            "expected_output": "Flags cannibalization (Bucket E). Refuses to schedule new content for that query. Routes user to /seo-analysis to pick a canonical page and consolidate.",
            "files": [],
            "expectations": [
              "Identifies cannibalization explicitly",
              "Does NOT schedule a new post for this query",
              "Writes a warning entry into warnings[] in content-calendar.json",
              "Routes user to /seo-analysis as the next step"
            ]
          }
        ]
      }
      
  • references
    • planning-methodology.md 5.5 KB
      # Content Planning Methodology
      
      Reference for `/content-planner`. The skill applies these rules; this file is
      the load-bearing logic, kept out of the SKILL.md so the prompt stays focused on
      workflow.
      
      ---
      
      ## CTR-by-position curve (informational intent)
      
      Use as the baseline for "expected CTR at position P". Numbers reflect ~2024–2026
      median informational CTR; transactional and brand queries skew higher.
      
      | Position | Expected CTR |
      |---|---|
      | 1  | 0.275 |
      | 2  | 0.160 |
      | 3  | 0.110 |
      | 4  | 0.080 |
      | 5  | 0.062 |
      | 6  | 0.050 |
      | 7  | 0.040 |
      | 8  | 0.033 |
      | 9  | 0.028 |
      | 10 | 0.024 |
      | 11–20 | 0.010 |
      | 21+   | < 0.005 |
      
      A query whose actual CTR is < 50% of the expected CTR at its average position
      is a **CTR underperformer** — title/meta rewrite candidate, not new content.
      
      ---
      
      ## Opportunity buckets — full rubric
      
      ### Bucket A: Striking-distance queries
      Condition: average position in **[5.0, 20.0]**, impressions/90d ≥ 100, intent
      is informational (the query has "how", "what", "why", "best", "vs", "guide",
      or is a noun phrase Google answers with content).
      
      Action: schedule a new post or substantial refresh that targets the exact
      query intent.
      
      ### Bucket B: Unanswered intent (gaps)
      Condition: query appears in GSC at position > 20 with impressions ≥ 50/90d,
      and no page on the site ranks for it in the top 20 (the query×page report
      shows only one weak page or none).
      
      Action: schedule a new post. Verify it fits the site's topical area against
      `business-context.json` if available — don't plan a gardening post for a SaaS
      analytics site just because GSC saw 200 impressions for "compost ratios".
      
      ### Bucket C: CTR underperformers (refresh)
      Condition: position in **[1.0, 10.0]**, impressions/90d ≥ 500, actual CTR <
      0.5 × expected CTR at that position.
      
      Action: emit a **refresh** entry — type = "refresh", `refreshTarget` set to
      the URL. The writerPrompt should point at `/meta-tags-optimizer` first, then
      `/content-writer` for body refresh if needed.
      
      ### Bucket D: Related-keyword expansions
      Not its own calendar entry. For each Bucket A/B parent, identify 2–4 related
      queries from the same `query × page` cluster — synonyms, long-tails, question
      forms, "[parent] vs X", "[parent] for [audience]". They become **secondary
      keywords** on the parent's calendar entry → become H2s in the eventual post.
      
      ### Bucket E: Cannibalization warning
      Condition: same query appears at position < 20 for **two or more pages on the
      same site**, each with ≥ 20 impressions.
      
      Action: write a warning into `warnings[]`, do NOT schedule new content for
      this query. Route the user to `/seo-analysis` for the canonical-page fix.
      
      ---
      
      ## Click potential formula
      
      For each candidate topic that survives bucket classification:
      
      ```
      projectedImpressions   = impressions90d × seasonalityFactor
      targetCtrAtPosition3   = 0.110   # from CTR curve, informational baseline
      currentExpectedClicks  = impressions90d × currentCtr
      projectedClicks        = projectedImpressions × targetCtrAtPosition3
      clickPotential         = projectedClicks − currentExpectedClicks
      ```
      
      `seasonalityFactor` defaults to 1.0. If the query is clearly seasonal (e.g.
      "black friday", "tax filing", "summer camp"), adjust the next 90 days:
      
      | Pattern | factor (next 90d) |
      |---|---|
      | Peak season ahead | 2.0 |
      | Mid-season | 1.0 |
      | Off-season trailing | 0.4 |
      
      Tie-breakers when click potential is within 10%:
      1. Lower content-production cost (refresh > new post)
      2. Higher intent-to-revenue alignment (commercial > pure informational)
      3. Closer to topical authority the site already has
      
      ---
      
      ## Scheduling rules
      
      - **One post per week** by default. The agent should not pack 12 posts into 4
        weeks just because the data supports it — production capacity is the real
        bottleneck, and SEO compounds over months.
      - **P0 first** — the top 4 by click potential are P0, the next 4 are P1, the
        rest P2. Schedule P0s in weeks 1–4.
      - **Refresh entries can run in parallel** with new posts (they're cheaper to
        ship) — schedule them as same-week siblings, not consuming a weekly slot.
      - **Skip weeks** where the user has external deadlines if they tell you about
        them — don't fight the user's actual calendar.
      
      ---
      
      ## Click-potential output (what the JSON has to carry)
      
      For every topic in `content-calendar.json`:
      
      ```json
      "gsc": {
        "currentPosition": 9.4,
        "impressions90d": 2480,
        "currentCtr": 0.018,
        "clickPotential": 228
      }
      ```
      
      `clickPotential` is the **estimated additional monthly clicks** if the topic
      hits position 3. It's an upper-bound estimate, not a guarantee — communicate
      that clearly in the rationale field.
      
      ---
      
      ## Title-hook reuse
      
      Every planned title goes through the four `/content-writer` hook patterns:
      number + specificity, audience-named guide, contrarian myth-break, outcome
      promise + proof. Reject bare-keyword titles ("Facebook SEO Optimization") at
      the planning stage — fixing it post-write costs more.
      
      ---
      
      ## Failure modes to avoid
      
      - **Padding the calendar** with low-clickPotential topics to hit a count.
        Better to ship 6 strong topics than 12 mediocre ones.
      - **Ignoring intent mismatch** between bucket A queries and the site's actual
        business. If GSC sees a query but the site can't credibly answer it,
        skip it.
      - **Treating impressions as volume** for brand-new queries. GSC impressions
        reflect what *you* served — a query with 12 impressions could be a 12k/mo
        query you barely show for. When third-party volume isn't available, note
        the uncertainty in the rationale.
      - **Scheduling refreshes for pages that are already strong** (position < 3,
        CTR within 80% of expected). Don't break what works.
      
  • SKILL.md 9.4 KB
    ---
    name: content-planner
    argument-hint: "<site URL or 'plan from GSC'>"
    description: >
      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", "click-potential analysis", or "schedule
      my SEO content".
    ---
    
    # 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.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:
    - 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/<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.
    
    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.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related