topic-discovery
Layer 0 of the keyword research pipeline. Builds a topic-graph snapshot for the brand's category before any seed work, approximated from Semrush phrase_related + phrase_questions on the category seeds
Files1
View on GitHubSKILL.mdpaperclip-source/blog-engine/.claude/skills/topic-discovery/SKILL.md
GitHubTopic Discovery Skill (Layer 0)
DATA LAYER RULING (2026-06-12). Every
mcp__semrush__<kebab-name>tool named below is FICTIONAL (never existed on the live server), and DataForSEO is retired. Before making any data call, read../keyword-research-pipeline/references/semrush-data-layer.md— it maps this layer's old calls to the real Semrush MCP pattern (get_report_schema->execute_report). The logic below (filters, thresholds, output schema) remains binding; only the data calls changed.
Take brand-config.md and produce two artefacts that downstream layers consume as enrichment:
content-pipeline/0-keywords/topic-graph.json— the Semrush Topic Research idea-cluster tree rooted at the brand's category. Layer 1a (/seed-modifier-prompt) reads the top 5 cluster names so the seed agent grounds its output in Semrush's actual topic graph rather than brand-config alone. Layer 5 (/keyword-prioritization) reads cluster-level metrics to compute thecluster_authority_gapboost.content-pipeline/0-keywords/trends.md— a short markdown summary of category-level trending queries, momentum direction, and competitor traffic shifts pulled from Semrush .Trends. Layer 4 (/keyword-redteam) reads this to ground its "is this keyword trending up or down today?" critique.
This layer is enrichment, not a gate. If Semrush returns nothing useful, downstream layers fall through to brand-config-only behavior. Layer 0 must never block the pipeline.
Input
/topic-discovery [--regen]
Reads:
brand-config.md— brand category positioning, brand domaincontent-pipeline/0-keywords/topic-graph.json— previous output (for change detection)
--regen forces a fresh run even if the brand-config hash hasn't changed.
Process
Compute brand-config hash. SHA-256 of
brand-config.mdcontent. Compare againsttopic-graph.json'sbrand_snapshotfield if it exists. If unchanged AND--regennot passed, exit cleanly with a one-line message saying "topic graph is current, skipping" — Layer 0 stays idempotent (mirrors the pattern inseed-modifier-prompt/SKILL.mdstep 1).Read brand-config. Extract:
- Brand category root — the noun-phrase that defines the brand's category for SEO purposes. Derived from
brand-config.md's "Category positioning" line. If the line is verbose, condense to 1–3 words (e.g. for Pleasur.AI: "ai adult companions" → root seedai companion). When in doubt, prefer the broader of two candidates — Topic Research returns richer trees from broader roots. - Brand domain — used as the .Trends root URL. Strip protocol and trailing slash.
- Brand category root — the noun-phrase that defines the brand's category for SEO purposes. Derived from
Call
mcp__semrush__topic-researchwith the brand category as the root keyword. Request the full idea-cluster tree: each cluster includes a name, headlines, questions, related searches, aggregated volume, and aggregated difficulty (KD%). Cache the raw response undercontent-pipeline/0-keywords/cache/topic-research-raw.jsonso re-runs without--regendon't refetch when only writing the structured tree.Call
mcp__semrush__trends-overviewwith the brand domain as the root URL. Pull:- Trending queries within the category (volume momentum up vs. down)
- Competitor traffic shifts (domains gaining or losing share)
- Audience overlap (which competitor audiences also visit which sites)
Cache the raw response under
content-pipeline/0-keywords/cache/trends-raw.json.
Assemble
topic-graph.jsonas a structured tree:{ "root_seed": "ai companion", "brand_domain": "pleasur.ai", "clusters": [ { "id": "ksb-cluster-id-from-semrush", "name": "ai girlfriend", "volume": 246000, "kd_percent": 64, "headlines": ["...", "..."], "questions": ["what is an ai girlfriend?", "..."], "related_searches": ["...", "..."] } ], "generated_at": "ISO8601 UTC", "brand_snapshot": "sha256-hex-of-brand-config-md", "_meta": { "source": "Semrush Topic Research + .Trends", "tool_inventory_ref": ".claude/skills/keyword-research-pipeline/references/semrush-mcp-tool-inventory.md" } }Sort clusters by
volumedescending. Cap headlines / questions / related_searches at 10 entries each per cluster — Layer 1a only consumes the top 5 clusters anyway, but downstream tooling stays bounded.Assemble
trends.mdas readable markdown for the redteam agent:# Category trends — {brand_domain} ({YYYY-MM-DD}) ## Trending up (volume momentum +) - keyword (vol_now → vol_prev, change %) ## Trending down (volume momentum −) - keyword (vol_now → vol_prev, change %) ## Competitor traffic shifts - competitor.com — gaining/losing X% organic share over the last 90 days ## Audience overlap - competitor.com ↔ othercompetitor.com — Y% audience overlap _Source: Semrush .Trends, generated {ISO timestamp}_Keep it tight — under 60 lines. The redteam agent reads this as ambient context, not a primary input.
Write both files atomically (write to
.tmpthen move). Print a one-line summary: cluster count, top 3 cluster names with volumes, trending-up count, trending-down count, top 3 competitors by traffic shift.
Output
content-pipeline/0-keywords/topic-graph.json— structured idea-cluster treecontent-pipeline/0-keywords/trends.md— markdown trends summarycontent-pipeline/0-keywords/cache/topic-research-raw.json— raw MCP response (gitignored)content-pipeline/0-keywords/cache/trends-raw.json— raw MCP response (gitignored)
Idempotency
Same contract as seed-modifier-prompt: re-runs without --regen exit cleanly when the brand-config SHA-256 hasn't changed. Two MCP calls per actual run; zero on the no-op path. Safe for the orchestrator to call on every keyword-research-pipeline invocation without worrying about cost.
Failure handling
Layer 0 never blocks the pipeline. It's enrichment, not a gate. Specific failure modes:
mcp__semrush__topic-researchreturns empty (root seed too narrow / not indexed): write a stubtopic-graph.jsonwithclusters: [],_meta.degraded: true, and the root_seed used. Append a one-line entry tocontent-pipeline/0-keywords/cache/topic-discovery-failed.logwith the timestamp and reason. Exit code 0. Layer 1a reads the emptyclustersarray and falls through to brand-config-only seed generation (current behavior preserved).mcp__semrush__topic-researcherrors (auth, network, server): write the same stub graph with_meta.degraded: trueand_meta.error: "<reason>". Log tocache/topic-discovery-failed.log. Exit 0.mcp__semrush__trends-overviewreturns empty / errors: write atrends.mdcontaining only the heading and a single line_Source: Semrush .Trends — no data returned for {brand_domain} ({YYYY-MM-DD})_. Log tocache/topic-discovery-failed.log. Exit 0. The redteam agent reads the absence as "no trend signal available" and reasons without it.- Both calls error with the same auth code (401): Semrush MCP isn't connected or consent expired. Surface a clear one-line error to stderr ("Semrush MCP unauthenticated — re-run consent or check
SEMRUSH_API_KEY_BLOG_AGENT") and exit 0 anyway. The orchestrator continues. - Rate-limit (429): persist whatever responses we did get; if neither call succeeded, write the stub artefacts and log the quota event. Exit 0 (do not propagate exit 75 — Layer 0 is non-blocking). The orchestrator will pick up the cached stubs on the next run.
The discipline here is the user's call-out: "the pipeline never blocks on Layer 0 because it's enrichment, not a gate."
Quality checklist
-
topic-graph.jsonexists and parses as JSON -
brand_snapshotmatches currentbrand-config.mdSHA-256 -
clustersarray is sorted by volume descending (or empty +_meta.degraded: true) - Each non-degraded cluster has at least: id, name, volume, headlines, questions
-
trends.mdexists and has a section heading even if empty - Failure paths log to
cache/topic-discovery-failed.lograther than crashing - Two MCP calls maximum per actual run; zero on no-op
- Cache files are under
content-pipeline/0-keywords/cache/(gitignored)
Why this exists
Ryan Law's pre-Semrush method had no equivalent of Topic Research or .Trends — Ahrefs surfaced individual keywords but not the cluster topology of a category, and not the time-axis momentum signal that tells you whether a topic is rising or fading. Both are first-class signals in Semrush.
- The cluster topology lets Layer 1a generate seeds that match how the brand's category is actually organized in Google's view (rather than a brand-config-trained generative guess).
- The momentum signal lets Layer 4's redteam reject keywords that look attractive but are on a 12-month decline — an opportunity-cost critique the legacy pipeline couldn't make.
Adding Layer 0 doesn't disturb the existing chain — Layers 1–5 still run if Layer 0 produces nothing. It's pure enrichment, gated on a cheap idempotency check.
When the brand-config has no clear category
(Edge case: a personal blog or generic agency site without a sharp category positioning.) The root seed will be too broad and Topic Research returns a shallow cluster tree. The skill still runs — it writes whatever it gets, and downstream layers behave as if Layer 0 returned an empty tree. No special-casing required; the failure-handling path covers it.
Invocation from the master orchestrator
/keyword-research-pipeline calls this skill first, before Layer 1a. Because of the brand-config hash check, the master can call it on every run without worrying about waste. On a typical run with stable brand-config, Layer 0 is a < 1-second no-op.
References
.claude/skills/research/references/semrush-mcp-cheatsheet.md— Topic Research playbook (sections "Topic Research playbook" and ".Trends quick-reference").claude/skills/keyword-research-pipeline/references/semrush-mcp-tool-inventory.md— source of truth for the actualmcp__semrush__*tool names exposed by the connected MCP. Verifymcp__semrush__topic-researchandmcp__semrush__trends-overviewresolve to live tools before assuming the placeholders are correct..claude/skills/keyword-research-pipeline/references/semrush-metric-translation.md— KSB cluster id is the "parent topic" replacement; reference for downstream layers that readtopic-graph.json.