# Keyword Cannibalization Checker - Google Search Console (`cygnix_labs/keyword-cannibalization-finder`) Actor

Find keyword cannibalization from a Search Console query+page export: competing URLs, click split, severity and the fix (merge + 301 or de-optimize). First 3 issues free, then $0.007 per issue. No API key, no Google login.

- **URL**: https://apify.com/cygnix_labs/keyword-cannibalization-finder.md
- **Developed by:** [Umair Saleem Mohammad Saleem](https://apify.com/cygnix_labs) (community)
- **Categories:** SEO tools, AI, Agents
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$7.00 / 1,000 cannibalization issues

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Keyword Cannibalization Checker - Google Search Console

**Answer first:** this actor is a **keyword cannibalization checker for Google Search Console data**. Give it a GSC export with query and page columns. It finds every query where **two or more of your own pages compete**, shows the click and impression split, rates the problem **HIGH / MEDIUM / LOW** and gives the fix (**merge + 301**, or **de-optimize + internal link**). **First 3 issues free every run, then $0.007 per issue found.** Clean queries are never charged.

| Quick facts | |
|---|---|
| Best for | Keyword cannibalization audits, SEO content consolidation, "which of my pages compete for the same query?" |
| Input | A Search Console CSV with query + page (paste it or give a public CSV URL) |
| Output | One row per cannibalized query: `severity`, `primaryPage`, `competingPages`, `clicksAtRisk`, `positionGap`, `pages[]`, `recommendation` |
| Price | First 3 issues free every run, then $0.007 per issue. No start fee. Queries without cannibalization are free |
| Needs | No Google login, no API key, no scraping |
| Not included | Live SERP checks, URL normalization (URLs are grouped as they appear) |

### Why it matters

When several pages target the same query, Google rotates between them and none ranks well. Fixing cannibalization is one of the fastest SEO wins: consolidating usually lifts the primary page within weeks.

### Input example

```json
{
  "csvText": "query,page,clicks,impressions,position\nbest running shoes,https://example.com/best-running-shoes,120,4000,6.2\nbest running shoes,https://example.com/blog/running-shoes-guide,15,2100,14.8\nbest running shoes,https://example.com/shop/running,3,600,31.0\nhow to clean running shoes,https://example.com/blog/clean-shoes,40,900,3.5\nhow to clean running shoes,https://example.com/blog/shoe-care,22,850,7.9",
  "minImpressions": 10,
  "minPageShare": 10,
  "excludeQueries": ["mybrand"]
}
```

Or pass a public CSV URL in `csvUrl` (a Google Sheets "publish to web" CSV link works) instead of pasting `csvText`. Columns are auto-detected (`query`/`keyword`, `page`/`url`, `clicks`, `impressions`, `position`). Defaults: `minImpressions` 10, `minPageShare` 10%.

### How severity works

| Severity | Rule (from code) |
|---|---|
| HIGH | Secondary page has ≥30% of the query's impressions and position gap ≤10 (or position unknown) |
| MEDIUM | Secondary page has ≥20% impression share |
| LOW | Competing pages pass the share filter but below the MEDIUM threshold |

Severity is judged on the **#2 page** (the strongest competitor). The **primary page** is the one with the most clicks (ties: most impressions).

URL-path heuristics decide the fix. Paths containing `/blog/`, `/news/`, `/guide(s)/`, `/learn/` or `/article(s)/` count as *article*; `/shop/`, `/product(s)/`, `/category/`, `/collections/` or `/store(s)/` count as *commercial*; anything else is *page*. All competing pages the same type → merge + 301. Mixed types → keep both, de-optimize the secondary page and link it to the primary.

### Output (one row per cannibalized query)

`query, severity, pageCount, primaryPage, competingPages, totalClicks, totalImpressions, clicksAtRisk, positionGap, pages[] (clicks, impressions, share, position), recommendation`

Sample:

```json
{
  "query": "best running shoes",
  "severity": "HIGH",
  "pageCount": 2,
  "primaryPage": "https://example.com/best-running-shoes",
  "competingPages": ["https://example.com/blog/running-shoes-guide"],
  "totalClicks": 138,
  "totalImpressions": 6700,
  "clicksAtRisk": 15,
  "positionGap": 8.6,
  "pages": [
    {"page": "https://example.com/best-running-shoes", "clicks": 120, "impressions": 4000, "position": 6.2, "impressionShare": 59.7},
    {"page": "https://example.com/blog/running-shoes-guide", "clicks": 15, "impressions": 2100, "position": 14.8, "impressionShare": 31.3}
  ],
  "recommendation": "Pages serve different intents (page vs article). Keep both but de-optimize the secondary page for \"best running shoes\" (title/H1/anchors) and link it to https://example.com/best-running-shoes."
}
```

A SUMMARY record gives `queriesAnalyzed`, `cannibalizedQueries` and the HIGH count. Queries with fewer than two qualifying pages are not emitted and not charged.

Second issue from the same run (same-type pages → merge advice):

```json
{
  "query": "how to clean running shoes",
  "severity": "HIGH",
  "pageCount": 2,
  "primaryPage": "https://example.com/blog/clean-shoes",
  "competingPages": ["https://example.com/blog/shoe-care"],
  "totalClicks": 62, "totalImpressions": 1750, "clicksAtRisk": 22, "positionGap": 4.4,
  "recommendation": "Merge the weaker page(s) into https://example.com/blog/clean-shoes and 301 redirect them, or re-target them at a different query. Point internal links for \"how to clean running shoes\" to the primary page."
}
```

SUMMARY for the default input: `{"queriesAnalyzed": 4, "cannibalizedQueries": 2, "high": 2}` (the `/shop/running` row is filtered out at 9% share, below the 10% default).

### Pricing (pay per event)

**First 3 issues in every run are free**, then **$0.007 per cannibalization issue found** ($7 / 1,000 issues). Queries without cannibalization are free.

### What a run costs

| Issues found | You pay |
|---|---|
| 3 | $0 (free in every run) |
| 10 | $0.05 |
| 60 | $0.40 |
| 200 | $1.38 |
| 1,000 | $6.98 |

Set a maximum charge per run in Apify run options; when the event spend cap is hit the actor stops cleanly (`eventChargeLimitReached`).

### How it compares (live Apify Store prices, verified 18 Oct 2026)

List prices on the Apify Free plan, read from the Apify Store API on 18 October 2026.

| Actor | What you pay | Input | Notes |
|---|---|---|---|
| **cygnix_labs/keyword-cannibalization-finder** (this) | **$0.007 / issue, first 3 free every run, no start fee** | GSC query + page CSV | Severity, click split and a merge/301 or de-optimize fix per query; clean queries free |
| quanmatrix/keyword-cannibalization-seo-collision-intelligence | $0.0115 / result | SERP / ranked-keyword datasets | Recurring "collision" monitoring |
| sprightly_blackberry/content-portfolio-audit | $1.00 / report + tiny start fee | Site content | Broad content audit; cannibalization is one part |
| alizarin_refrigerator-owner/gsc-data-analyzer | $0.10 start + $0.08 / analysis + $0.01 / opportunity | GSC data | General GSC insights, not query-level cannibalization rows |
| siteverdict/siteverdict-gsc-ga4-analyst | $0.35 / property + $0.005 / URL inspected | Live GSC + GA4 API | Property-level analyst, different job |

**Honest take.** This is the lowest per-issue price we found for query-level cannibalization on the Store, and you pay only when a real issue is found. It works from your own GSC numbers only; if you want cannibalization detected from third-party ranking data without GSC access, quanmatrix is built for that.

### For AI agents / MCP

**When an agent should call this actor:** the user shares Search Console data (query + page) and asks about keyword cannibalization, pages competing for the same keyword, or which pages to merge or redirect.

Connect the Apify MCP server with only this actor enabled (works in Claude, ChatGPT, Cursor, VS Code, Codex and any MCP client):

```
https://mcp.apify.com?tools=cygnix_labs/keyword-cannibalization-finder
```

Or search for it with the default server: `search-actors` keywords such as `keyword cannibalization`, `Search Console`.

Example `call-actor` input:

```json
{
  "csvUrl": "https://docs.google.com/spreadsheets/d/e/.../pub?output=csv",
  "minImpressions": 10,
  "minPageShare": 10,
  "excludeQueries": ["mybrand"]
}
```

**What to read from the result:** each row is one cannibalized query with `severity`, `primaryPage`, `competingPages`, `clicksAtRisk`, `positionGap`, `pages[]` and a plain-English `recommendation`. `SUMMARY` gives `queriesAnalyzed`, `cannibalizedQueries` and the HIGH count. Use `csvText` instead of `csvUrl` to paste the data inline.

**Cost control for agents:** Exports with 3 or fewer issues are free; 60 issues cost about $0.40. Pass `maxTotalChargeUsd` in `callOptions` to hard-cap spend. This actor is pay-per-event, so it also works with Apify agentic payments (no Apify account needed for the agent).

Example prompt: *"Here is my GSC query+page export. Find keyword cannibalization and tell me which pages to merge and 301."*

### FAQ

**My GSC export has no query+page columns.** The Search Console web UI exports queries and pages in separate tabs. Use Looker Studio, the GSC API, the Search Analytics for Sheets add-on, or a BigQuery bulk export to get both in one table.
**Does it need my Google account or API key?** No. Paste the CSV or give a public CSV URL. Nothing is scraped and nothing logs into Google.
**What do minImpressions and minPageShare do?** A page must meet both thresholds for a query to count as competing (defaults 10 impressions and 10% share). Raise them to cut noise on large exports.
**How do I exclude brand queries?** Put brand strings in `excludeQueries` (e.g. `["acme", "acme.com"]`). Any query containing those substrings is skipped.
**Can I cap my spend?** Yes. Set a max charge per run; the actor stops when `eventChargeLimitReached` is returned and you are not billed beyond that cap.
**Are English-only queries required?** No language filter is applied. Queries are lowercased for grouping; non-English GSC exports work the same way.
**Are URLs normalized?** No. Pages are grouped exactly as they appear in your CSV, so `https://x.com/a` and `https://x.com/a/` (or URLs with `?utm=` parameters) count as different pages. That can surface real duplicate-URL problems, but clean or normalize URLs first if you only want distinct content pages.
**Why are totalClicks/totalImpressions larger than the sum of `pages[]`?** Totals include every page that got impressions for the query; `pages[]` lists only the pages that pass `minImpressions` and `minPageShare`.
**Is it AI or rule-based?** Rule-based and deterministic: the same CSV gives the same output. No LLM, no SERP data. It reads only your Search Console numbers.
**Do I pay for clean queries?** No. Only rows that are actual cannibalization issues (and only after the first 3 free per run) trigger the `issue-found` event.

**How do I get a query + page export?** The GSC web UI exports queries and pages separately. Use the Search Console API (dimensions `query` and `page`), the Search Analytics for Sheets add-on, Looker Studio or a BigQuery bulk export.

### Integrations

Run via the Apify API, schedule weekly runs, or send results to Google Sheets, Make, Zapier, n8n, Slack or webhooks.

### Changelog

- **0.1.6** (2026-10-18): README rewritten answer-first for search and AI agents: quick facts, comparison re-verified on 18 Oct 2026, "For AI agents / MCP" section with example input. Price unchanged ($0.007/issue, first 3 free).
- **0.1.5** (2026-10-16): Named Store comps, full URL-type rules, second sample row, FAQ.

### More SEO and AEO tools by Cygnix Labs

- [Website SEO Audit + AEO/GEO Checker (full-site crawl)](https://apify.com/cygnix_labs/site-seo-aeo-audit)
- [AEO & GEO Audit + llms.txt Checker (URL list)](https://apify.com/cygnix_labs/aeo-readiness-checker)
- [Internal Link Audit & Orphan Page Finder](https://apify.com/cygnix_labs/internal-link-audit)
- [Content Decay Audit (Search Console)](https://apify.com/cygnix_labs/content-decay-audit)
- [Keyword Clustering + Content Briefs](https://apify.com/cygnix_labs/keyword-clusterer-brief)
- [Search Intent Classifier](https://apify.com/cygnix_labs/serp-intent-mapper)
- Free local SEO MCP server: [cygnix-seo-mcp](https://github.com/cygnixlabs-cmd/cygnix-seo-mcp) (`npx -y cygnix-seo-mcp`)

Questions or a feature request? Open an issue on this actor's Issues tab. We answer within 24 hours.

# Actor input Schema

## `csvText` (type: `string`):

CSV/TSV with query, page, clicks, impressions, position columns (GSC API, Looker Studio or Search Analytics for Sheets export).

## `csvUrl` (type: `string`):

Public link to the CSV (Google Sheets 'publish to web' CSV link works).

## `minImpressions` (type: `integer`):

Ignore pages with fewer impressions for a query (noise).

## `minPageShare` (type: `integer`):

A page must have at least this share of the query's impressions to count as competing.

## `excludeQueries` (type: `array`):

e.g. your brand name.

## Actor input object example

```json
{
  "csvText": "query,page,clicks,impressions,position\nbest running shoes,https://example.com/best-running-shoes,120,4000,6.2\nbest running shoes,https://example.com/blog/running-shoes-guide,15,2100,14.8\nbest running shoes,https://example.com/shop/running,3,600,31.0\nrunning shoes for flat feet,https://example.com/flat-feet-shoes,80,1500,4.1\nhow to clean running shoes,https://example.com/blog/clean-shoes,40,900,3.5\nhow to clean running shoes,https://example.com/blog/shoe-care,22,850,7.9\ntrail running shoes,https://example.com/trail-shoes,60,1800,5.0",
  "minImpressions": 10,
  "minPageShare": 10,
  "excludeQueries": []
}
```

# Actor output Schema

## `results` (type: `string`):

No description

## `summary` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "csvText": `query,page,clicks,impressions,position
best running shoes,https://example.com/best-running-shoes,120,4000,6.2
best running shoes,https://example.com/blog/running-shoes-guide,15,2100,14.8
best running shoes,https://example.com/shop/running,3,600,31.0
running shoes for flat feet,https://example.com/flat-feet-shoes,80,1500,4.1
how to clean running shoes,https://example.com/blog/clean-shoes,40,900,3.5
how to clean running shoes,https://example.com/blog/shoe-care,22,850,7.9
trail running shoes,https://example.com/trail-shoes,60,1800,5.0`
};

// Run the Actor and wait for it to finish
const run = await client.actor("cygnix_labs/keyword-cannibalization-finder").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "csvText": """query,page,clicks,impressions,position
best running shoes,https://example.com/best-running-shoes,120,4000,6.2
best running shoes,https://example.com/blog/running-shoes-guide,15,2100,14.8
best running shoes,https://example.com/shop/running,3,600,31.0
running shoes for flat feet,https://example.com/flat-feet-shoes,80,1500,4.1
how to clean running shoes,https://example.com/blog/clean-shoes,40,900,3.5
how to clean running shoes,https://example.com/blog/shoe-care,22,850,7.9
trail running shoes,https://example.com/trail-shoes,60,1800,5.0""" }

# Run the Actor and wait for it to finish
run = client.actor("cygnix_labs/keyword-cannibalization-finder").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "csvText": "query,page,clicks,impressions,position\\nbest running shoes,https://example.com/best-running-shoes,120,4000,6.2\\nbest running shoes,https://example.com/blog/running-shoes-guide,15,2100,14.8\\nbest running shoes,https://example.com/shop/running,3,600,31.0\\nrunning shoes for flat feet,https://example.com/flat-feet-shoes,80,1500,4.1\\nhow to clean running shoes,https://example.com/blog/clean-shoes,40,900,3.5\\nhow to clean running shoes,https://example.com/blog/shoe-care,22,850,7.9\\ntrail running shoes,https://example.com/trail-shoes,60,1800,5.0"
}' |
apify call cygnix_labs/keyword-cannibalization-finder --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,cygnix_labs/keyword-cannibalization-finder"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/gGag7AgtUzjoLYIwQ/builds/cePkd9T7wBkzpJREt/openapi.json
