Search Console Cannibalization Analyzer
Detects keyword cannibalization by pulling query-and-page data from the Google Search Console API and clustering querie…
Updated
Use this skill
$ npx skills add seoskills.sh/gsc-cannibalization-analyzerAbout this skill
Search Console Cannibalization Analyzer is a Analytics and Rank Tracking skill for AI agents, published in the seoskills.sh catalog. Reach for it when your work involves gA4, Search Console, event tracking, and rank monitoring. Install it with one command and it runs inside your own agent, so the work happens in your workflow, not a separate SEO tool.
SKILL.md
Search Console Cannibalization Analyzer
AGENT ROLE: Autonomous Search Console analyst. Pull query→page performance, identify cannibalized queries deterministically, recommend a single canonical target per cluster, and emit the JSON in references/output.schema.json.
OBJECTIVE
For a verified GSC property and date range, find queries where ≥2 URLs receive impressions, quantify the resulting click loss and position instability, and recommend which URL to consolidate to.
INPUTS
site_url(REQUIRED): exactly as verified in GSC. Domain property =sc-domain:example.com; URL-prefix property =https://example.com/.start_date,end_date(OPTIONAL): ISO dates. Default = last 28 complete days (GSC data lags ~2–3 days; never requesttoday).country/search_type(OPTIONAL): default all /web.min_impressions(OPTIONAL, default 50): ignore queries below this to suppress noise.competitor_page_threshold(OPTIONAL, default 2): min competing URLs to flag.
AUTHENTICATION (Search Console API)
- REQUIRE
GOOGLE_APPLICATION_CREDENTIALS(service account) OR a user OAuth token with scopehttps://www.googleapis.com/auth/webmasters.readonly.- IF neither present THEN STOP
error.code="AUTH_MISSING_CREDENTIALS".
- IF neither present THEN STOP
- The identity MUST be a verified owner/user of
site_urlin Search Console (API access is NOT implied by GA/GSC-UI access).- IF API returns
403THEN STOPerror.code="AUTH_NO_SITE_ACCESS": "Add the service-account email as a user on the Search Console property {site_url}."
- IF API returns
- Endpoint:
POST https://searchconsole.googleapis.com/webmasters/v3/sites/{urlEncoded site_url}/searchAnalytics/query.
EXPECTED TOOL CALLS
- Run
scripts/cannibalization.py --site {site_url} --start {d} --end {d} --min-impr {n}. - The script pages the API (
rowLimit=25000,startRowincrements) withdimensions=["query","page"]anddataState="final".
PROCEDURE
STEP 1 — FETCH: query with dimensions=["query","page"]. Page until a response returns < rowLimit rows.
STEP 2 — GROUP by query. Keep only queries with SUM(impressions) >= min_impressions.
STEP 3 — DETECT: within a query, count URLs with impressions > 0. IF distinct_urls >= competitor_page_threshold THEN candidate cluster.
STEP 4 — SCORE each cluster:
primary= URL with the highest clicks; tie-break by best (lowest) average position.contenders= the other URLs.position_spread= max(avg_position) − min(avg_position) across URLs. Higher = more instability.estimated_lost_clicks=sum(contender_impressions) * CTR(primary_avg_position) − sum(contender_clicks), floored at 0. Use the standard position→CTR curve inreferences/ctr_curve.json.severity=highIF (position_spread ≥ 5 AND estimated_lost_clicks ≥ 10) ELSEmediumIF estimated_lost_clicks ≥ 3 ELSElow. STEP 5 — RECOMMEND:consolidate_to = primary; action = "canonicalize/redirect contenders or differentiate intent". Never recommend deleting the primary. STEP 6 — EMIT JSON, clusters sorted byestimated_lost_clicksdesc.
RATE LIMITS & ERROR HANDLING
- GSC API quota: ~1200 queries/min/property and a daily cap. IF
429/RESOURCE_EXHAUSTEDTHEN backoff2^attempt(max 5) then STOPerror.code="RATE_LIMITED". - IF
400 invalid dimension/dateTHEN STOPerror.code="BAD_REQUEST"echoing the message. - Respect
dataState: only trustfinalrows; discardfresh/partial days.
MISSING / INSUFFICIENT DATA
- IF total rows == 0 THEN
status="no_data"(property may be new or date range empty); return empty clusters, no error. - IF a query has URLs each with impressions but zero clicks THEN still evaluate (impression-level cannibalization); mark
clicks_basis="impressions_only". - Never invent CTR: if
primary_avg_position > 20, cap CTR at the 20+ bucket value.
OUTPUT
One JSON object per references/output.schema.json. No prose.
FILES
scripts/cannibalization.py— auth, paging, backoff, clustering, scoring.references/ctr_curve.json— position→CTR lookup used for lost-click estimation.references/output.schema.json— output contract.
Not using the CLI? Copy the SKILL.md and paste it straight into ChatGPT, Claude, or any agent.
Embed a badge
Install into your agent
More in Analytics and Rank Tracking
GA4 Anomaly Detector
Detects statistically significant anomalies in GA4 organic traffic and conversions by pulling daily metrics from the GA…
GA4 Attribution Path Modeler
Reconstructs multi-touch conversion paths from the event-level GA4 BigQuery export and assigns credit across channels w…
Striking Distance Opportunity Finder
Surfaces "striking distance" keywords ranking in Search Console positions 11-20 with real impression demand, ranks them…
BigQuery Log File Analyzer
Analyzes raw server and CDN access logs stored in BigQuery to quantify Googlebot crawl behavior, surface crawl-budget w…