Help us improve
Share bugs, ideas, or general feedback.
From content-seo
Site-level SEO intelligence. Routes to one of five workflows — status (site-wide GSC performance), opportunities (pages ranking 4–20), diagnose (deep-dive a page with GSC data + fixes), brief (keyword research before writing), or coverage (audit GSC indexing errors from a CSV export).
npx claudepluginhub pcamarajr/content-stack --plugin content-seoHow this skill is triggered — by the user, by Claude, or both
Slash command
/content-seo:seoThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
SEO intelligence skill for content sites. Routes to the appropriate workflow based on the first argument.
Full SEO audit combining Google Search Console, PageSpeed Insights, URL Inspection API, technical crawl, keyword research, metadata/schema markup analysis, and Core Web Vitals monitoring. Produces an actionable 30-day plan.
Optimizes SEO using Google Search Console data: analyzes metrics like clicks/impressions/CTR/position, finds striking-distance keywords, fixes low-CTR pages, detects keyword cannibalization, identifies declining pages, builds data-driven strategies.
Performs comprehensive SEO audits and analysis for any website, covering technical SEO, schema markup, content quality, image optimization, sitemap analysis, and GEO for AI Overviews. Includes local SEO, backlink analysis, and drift monitoring.
Share bugs, ideas, or general feedback.
SEO intelligence skill for content sites. Routes to the appropriate workflow based on the first argument.
Arguments: $ARGUMENTS
Parse $ARGUMENTS:
| Argument | Route |
|---|---|
status | Status workflow |
opportunities | Opportunities workflow |
diagnose <file> | Diagnose workflow |
brief <topic> | Brief workflow |
coverage [csv] | Coverage workflow |
init [round] | Delegate to init skill: read skills/init/SKILL.md and follow instructions |
| empty or unknown | Show usage (below) |
Usage (shown on empty/unknown argument):
/seo <subcommand> [args]
status — Site-wide GSC performance: top pages, movers, CTR outliers
opportunities — Pages ranking 4–20 with the most untapped traffic
diagnose <file> — Deep-dive a page: GSC data + technical audit + content fixes
brief <topic> — Keyword research brief before writing (requires DataForSEO)
coverage [csv] — Audit GSC indexing errors: paste URLs from GSC or provide a CSV, find root causes, save report
init [round] — Setup wizard (project, credentials, strategy)
Hard stop if missing: "content-seo is not configured. Run /seo init first."
Extract:
content_ops_config — hard stop if absent: "content_ops_config is not set. Run /seo init project to link content-ops."gsc_propertygsc_credentials_path (may be empty — gsc-reporter falls back to env var)seo_rulesRead the file at content_ops_config. Extract:
site_urldefault_languagecontent_types (keys and paths)content_pillars_pathresearch_cache_ttl_days (default: 30)Spawn the gsc-reporter agent via Task:
Use the gsc-reporter agent.
gsc_property: [gsc_property]
credentials_path: [gsc_credentials_path]
query_type: site-wide
date_range: last_28_days
dimensions: page
include_comparison: true
From the returned GSC_DATA and COMPARISON_DATA rows:
## SEO Status — [site_url]
Period: [start_date] to [end_date] vs prior 28 days
### Top pages by clicks
| Page | Clicks | Impressions | CTR | Position |
|---|---|---|---|---|
[top 10 rows sorted by clicks descending]
### Position movers
| Page | Position (current) | Position (prior) | Change |
|---|---|---|---|
[pages with >2 position change, sorted by absolute change descending]
### High impressions, low CTR (title/description problem)
| Page | Impressions | CTR | Site avg CTR |
|---|---|---|---|
[pages with >500 impressions and CTR < half site average]
→ Run /seo opportunities to see pages ranked 4–20 with untapped traffic.
→ Run /seo diagnose <file> to deep-dive any page.
Spawn the gsc-reporter agent via Task:
Use the gsc-reporter agent.
gsc_property: [gsc_property]
credentials_path: [gsc_credentials_path]
query_type: site-wide
date_range: last_28_days
dimensions: page
include_comparison: false
From the returned rows:
position between 4.0 and 20.0 (inclusive).score = impressions × (1 - ctr) — traffic being left on the table.For each page, estimate the traffic gain if it moved to position 3:
est_gain = impressions × (0.10 - ctr) (floor at 0).For each page URL in the top 15:
site_url + content_types paths.https://mysite.com/en/articles/bitcoin-basics → src/content/articles/en/bitcoin-basics.md## SEO Opportunities — [site_url]
Pages ranking 4–20 with the most untapped traffic (last 28 days)
| # | Page | Position | Impressions | CTR | Est. gain to #3 | File |
|---|---|---|---|---|---|---|
[top 15 rows]
→ Run /seo diagnose <file> to deep-dive any of these pages.
Arguments: file path extracted from $ARGUMENTS after diagnose .
title, description, tags, seo_keyword (if present).site_url:
content_types.<type>.path prefix and language segment from the file pathsite_urlsrc/content/articles/en/bitcoin-basics.md → https://mysite.com/en/articles/bitcoin-basicsseo_keyword is present in frontmatter → use it. Skip keyword proposal.Spawn the gsc-reporter agent via Task:
Use the gsc-reporter agent.
gsc_property: [gsc_property]
credentials_path: [gsc_credentials_path]
query_type: page-specific
url_filter: [derived live URL]
date_range: last_28_days
dimensions: both
include_comparison: false
Determine the content type from the file path (match against content_types paths). Get seo_rules for that type from config.
Read target audience from the content-ops content strategy file (content_strategy path).
Spawn the page-analyzer agent via Task:
Use the page-analyzer agent.
Source file: [file path]
Live URL: [derived URL]
Site URL: [site_url from content-ops]
Locale: [default_language]
Target audience: [from content strategy]
SEO rules: [seo_rules for this content type]
gsc_data:
position: [avg position from gsc-reporter]
clicks: [total clicks]
impressions: [total impressions]
ctr: [avg CTR]
top_queries: [list of top 10 queries by impressions from gsc-reporter, with impressions count each]
## SEO Diagnosis: [title]
URL: [url]
GSC (last 28 days): position [N.N] · [N] clicks · [N] impressions · [N%] CTR
Top queries: [top 5 from gsc-reporter, comma-separated]
### Fixed in source file
[FIXED section from page-analyzer]
### Content issues detected
[NEEDS_OPTIMIZATION section from page-analyzer]
### Keyword mismatches (pages rank for these but content doesn't cover them)
[KEYWORD_MISMATCHES section from page-analyzer, or "none" if section absent]
### Manual action required
[MANUAL_ACTION section from page-analyzer]
If NEEDS_OPTIMIZATION or KEYWORD_MISMATCHES sections are non-empty:
Ask the user:
Content issues were found. Trigger content-ops agents to fix them?
style-enforcer — structure, tone, length issues
draft-writer — thin sections and keyword gaps (targeted rewrite, not full article)
Fix now? (yes / no — I'll handle it manually)
Use AskUserQuestion with "Yes, fix now" / "No, I'll handle it manually".
If yes:
Spawn style-enforcer agent via Task for structure/tone/length issues (if any in NEEDS_OPTIMIZATION):
Use the style-enforcer agent to review [file path].
Content type: [content type]
Config:
word_range: [content_types.<type>.word_range from content-ops config]
guidelines: [content_types.<type>.guidelines from content-ops config]
reference_content: [reference_content from content-ops config]
Apply must-fix issues only. Return changes made.
If keyword mismatches or thin sections exist, spawn draft-writer agent via Task with a targeted brief:
Use the draft-writer agent.
Mode: targeted rewrite (NOT a full article — patch specific issues only)
File to update: [file path]
Content type: [content type]
Author: [from content-ops config]
Issues to address:
[List each KEYWORD_MISMATCH: "Add '[query]' to intro or a relevant heading/section"]
[List each thin section from NEEDS_OPTIMIZATION: "Expand section '[heading]' — currently under 50 words"]
Constraints:
- Do not restructure the article
- Do not add more than 200 words total
- Preserve all frontmatter fields
- Add the target keyword "[seo_keyword or proposed keyword]" in the first 100 words if not already present
After all fixes (whether content-ops agents ran or not), if seo_keyword is not already in frontmatter:
Add seo_keyword: "[target keyword]" to the article frontmatter.
git add [source file path]
git commit -m "seo: diagnose and fix \"[title]\""
Arguments: topic extracted from $ARGUMENTS after brief .
[ -n "$DATAFORSEO_LOGIN" ] && [ -n "$DATAFORSEO_PASSWORD" ] && echo "ok" || echo "missing"
Hard stop if missing:
/seo brief requires DataForSEO credentials.
DATAFORSEO_LOGIN — [set | not set]
DATAFORSEO_PASSWORD — [set | not set]
Set them as environment variables and re-run.
Get your API password at: https://app.dataforseo.com/api-access
If the argument is a file path rather than a plain topic:
seo_keyword is set in frontmatter → ask: "Keyword [keyword] is already targeted. Re-research?" with options "Yes, re-research" / "No, skip". Skip if no.Derive cache slug from topic: lowercase, spaces and special chars → hyphens.
Check .content-seo/keyword-cache/[slug].json:
[ -f ".content-seo/keyword-cache/[slug].json" ] && cat ".content-seo/keyword-cache/[slug].json" || echo "cache_miss"
If cache hit: check timestamp field against research_cache_ttl_days (from content-ops config, default 30).
Using the topic and content-ops pillars (read pillar files from content_pillars_path if set):
Propose a primary keyword and show it to the user:
Proposed primary keyword: "[keyword]"
Based on: topic "[topic]" + pillars context
Confirm this keyword, or enter your own:
Use AskUserQuestion. If called from write-content in autonomous/backlog mode → auto-confirm the proposed keyword without asking.
Spawn the keyword-researcher agent via Task:
Use the keyword-researcher agent.
Keyword: [confirmed keyword]
Locale: [default_language from content-ops]
Audience: [from content strategy]
Pillars context: [pillar names, comma-separated]
After the agent returns, write the cache:
Create directory if needed: mkdir -p .content-seo/keyword-cache/
Write .content-seo/keyword-cache/[slug].json:
{
"topic": "[topic]",
"keyword": "[primary keyword]",
"timestamp": "[ISO 8601 now]",
"ttl_days": [research_cache_ttl_days],
"volume": [monthly search volume],
"competition": "[competition level]",
"related_keywords": [
{ "keyword": "...", "volume": N },
...
],
"content_gaps": ["...", "...", "..."]
}
## SEO Brief: [topic]
Primary keyword: [keyword] — [volume]/mo · [competition]
Related keywords:
[keyword 1] — [volume]/mo
[keyword 2] — [volume]/mo
[keyword 3] — [volume]/mo
[keyword 4] — [volume]/mo
[keyword 5] — [volume]/mo
Content gaps to cover:
1. [gap 1]
2. [gap 2]
3. [gap 3]
[If cache was used: "Cached result from [date] — re-run with fresh data after [expiry date]"]
If called standalone: Ask "Save this brief to a file for reference?" with options "Yes, save" / "No thanks". If yes, write to .content-seo/briefs/[slug].md.
If called from write-content: Return the brief object to the orchestrator. Do not commit, do not save file.
Arguments: optional CSV path extracted from $ARGUMENTS after coverage .
Extract what follows coverage from $ARGUMENTS.
mode: csv, csv_path: [value]. Proceed to Step C2.If no valid CSV was provided, use AskUserQuestion:
How would you like to provide the GSC coverage data?
Note: GSC's Export button only downloads up to 1,000 representative examples and
doesn't produce a clean url/reason table. The reliable approach is to open each
error type in GSC → copy the URLs → paste them here.
A — Paste URLs directly: paste the URL list for each error type in any format;
just describe which error type each group belongs to.
B — Provide a CSV file path: build a CSV with columns `url` and `reason`
and enter the path (e.g. ~/Downloads/gsc-errors.csv).
Options: "Paste URLs now (A)" / "Provide CSV path (B)"
If user chooses A (paste):
Use AskUserQuestion: "Paste your URL list. Include the error type for each group — any format works:"
Set mode: free_form, free_form_input: [pasted text]. Proceed to Step C2.
If user chooses B (CSV path):
Use AskUserQuestion: "Enter the CSV file path:"
Validate file exists ([ -f "[path]" ]). If not found, ask once more. If still not found, hard-stop with: "File not found: [path]".
Set mode: csv, csv_path: [path]. Proceed to Step C2.
Spawn the coverage-auditor agent via Task:
Use the coverage-auditor agent.
input_mode: [csv or free_form]
csv_path: [csv_path — only if mode is csv]
free_form_input: [free_form_input — only if mode is free_form]
site_url: [site_url from content-ops config]
content_types: [content_types from content-ops config]
Parse the COVERAGE_FINDINGS returned by the agent and write:
Compute today's date:
python3 -c "from datetime import date; print(date.today().isoformat())"
Create directory if needed:
mkdir -p .content-seo/coverage
Write .content-seo/coverage/[date].md with this structure:
---
date: [date]
csv_source: [csv_path]
total_analyzed: N
---
## Coverage Audit — [site_url]
Source: [csv_path] · [N] URLs analyzed · [date]
### Summary
| Category | Count |
|---|---|
[one row per BUCKET + UNRESOLVED]
---
### [Reason] (N URLs)
**Likely root cause:** [codebase_finding]
**Recommended action:** [recommended_action]
| URL | Source file | Finding | HTTP |
|---|---|---|---|
[one row per URL in bucket]
---
[repeat per bucket]
### Could not determine root cause (N URLs)
| URL | GSC Reason | HTTP Status | Notes |
|---|---|---|---|
[UNRESOLVED rows]
---
### Next steps
[checklist item per non-empty bucket with recommended_action]
- [ ] Re-run `/seo coverage` after fixes to verify resolution
✅ Coverage audit complete
[N] URLs analyzed · [N] errors · [N] excluded · [N] unresolved
Report saved: .content-seo/coverage/[date].md
Top issues:
[list top 3 buckets by count with recommended_action]
→ Share the report with a coding agent to apply fixes.