---
name: google-search-console-cannibalization-fix
description: "Monthly: queries where your pages split clicks, the page that should own each, and the exact internal links and anchors to repoint, from a crawl. Use when the user asks for this, or when it runs on a schedule."
---

# Keyword cannibalization with the fix: Search Console and a site crawl

Monthly: queries where your pages split clicks, the page that should own each, and the exact internal links and anchors to repoint, from a crawl.

Knowing that two pages compete for a query is half the job. The other half is deciding which page wins and changing what tells Google so: the titles, the H1s and, above all, the internal links whose anchor text says the query but points at the wrong page. Search Console has the first half; only a crawl of your own site has the second.

Each month the agent finds the split queries in Search Console, crawls the site with Firecrawl to collect every internal link and its anchor, and joins the two on the URL. The card names the owner page and the links to edit, page by page, so the fix is an afternoon of edits rather than a research project.

## Two ways to get the data

Use the API or MCP route when you have an MCP server, a CLI or an API token for the app. Otherwise, if you can drive a browser the user is signed in to (Claude in Chrome, a Playwright or Chrome DevTools MCP server), use the browser route. Say which one you used.

## Route A: API or MCP server

### What you need

- Read access to the Search Console property, through a Search Console MCP server or a service account.
- A Firecrawl API key in `FIRECRAWL_API_KEY` or the Firecrawl MCP server; a 500-page crawl spends about 500 credits.
- Your brand terms as a regular expression, so branded queries stay out.

If something is missing, say what and stop; do not invent numbers.

### Steps

1. **Find queries shared by two pages** (Google Search Console): Call `searchanalytics.query` for the last 28 days of final data with `dimensions: ["query", "page"]`, `rowLimit: 25000`, paging with `startRow`. Keep non-branded queries with 100 or more impressions where two or more pages each hold at least 20% of them; keep each page's clicks and position.
2. **Crawl the site for links and headings** (Firecrawl): Call `POST /v2/crawl` with the site URL, `limit: 500`, `sitemap: "include"`, `ignoreQueryParameters: true` and `scrapeOptions: { formats: ["markdown"], onlyMainContent: true }`, then poll `GET /v2/crawl/{id}`. From each page keep `metadata.title`, the first `# ` heading as the H1, and every Markdown link `[anchor](href)` that points at your own host.
3. **Join links to the competing pages**: Resolve each `href` against its page, then normalize it and every Search Console URL the same way: `https`, lowercase host, no query string, fragment or trailing slash. Build, for each competing page, the list of pages linking to it with the anchor text. A competing page with no inbound links in the crawl is flagged as an orphan or beyond the crawl limit.
4. **Pick the owner and the fixes**: The owner is the page with the most clicks for the query, unless another one has the query's words in both title and H1 and ranks better. Then list: links to the other page whose anchor contains the query's main words (repoint to the owner), a missing link from the other page to the owner (add one), and a title or H1 on the other page that targets the same words (reword it).
5. **Ask before drafting**: Post an action card for the split with the most clicks at stake: the queries, both pages with position and clicks, the owner and the links to change, with how many other split queries were found. Draft the edits only for the answer given.

## Route B: the browser

### What you need

- Signed in to Google with access to the Search Console property, including the Links report.

If something is missing, say what and stop; do not invent numbers.

### Steps

Start at https://search.google.com/search-console/performance/search-analytics. The user is signed in already; never type passwords or codes, and ask if a sign-in page appears.

1. **Find split queries**: Pick the property, set Date to `Last 28 days`, and on the Queries tab click each of your top non-branded queries, then the Pages tab. Keep queries with 100 or more impressions where two or more pages each hold at least 20% of them.
2. **Find the pages linking to each competitor**: Open Links in the left menu, then Internal links > More. Find each competing page and click it to see the pages that link to it.
3. **Read titles, H1s and anchors**: Open each competing page and read its title and H1. Open each linking page, find the link to the competing page, and note its anchor text.
4. **Pick the owner and list fixes**: The owner is the page with the most clicks, unless the other has the query in title and H1 and ranks better. List links to repoint, a missing link to the owner, and titles or H1s to reword.

Note: Search Console's Links report counts internal links but does not show their anchor text and samples large sites, so the browser route opens linking pages one by one. Past a few dozen links the crawl in the API route is the better one.

## The report

- **Split queries**: the query, the competing pages, clicks and position for each.
- **Owner page** for each, with the reason (position, title, H1).
- **Links to repoint**: linking page, anchor text, current target, new target.
- **Title and H1 changes** on the page that should step back.
- The ask: which fix to draft first.

## Post it to Briefkin

When `BRIEFKIN_TOKEN` is set, or `~/.config/briefkin/env` exists (the installer from https://briefkin.dev/docs writes it), also send the result to the user's Briefkin board as one card. Keep the `item_key` the same on every run so the card updates in place, use a new `event_id` per run, and put today's real numbers in place of the example ones. If the request fails, say so once and carry on.

```bash
[ -n "${BRIEFKIN_TOKEN:-}" ] || . ~/.config/briefkin/env
curl -sS -X POST "${BRIEFKIN_URL:-https://briefkin.dev}/v1/events" \
  -H "authorization: Bearer $BRIEFKIN_TOKEN" \
  -H "content-type: application/json" \
  -d @- <<'JSON'
{
  "v": 1,
  "event_id": "google-search-console-cannibalization-fix/<date of this run, e.g. 2026-10-16>",
  "item_key": "google-search-console-cannibalization-fix",
  "project": "example.com",
  "source": "Keyword cannibalization with the fix: Search Console and a site crawl skill",
  "kind": "action",
  "title": "2 \"net 30\" queries split across two pages: 6 internal links to repoint",
  "summary": "**\"net 30 payment terms\"** and **\"net 30 invoice\"** split 540 clicks between /blog/net-30-payment-terms (position 3.9) and /glossary/net-30 (7.1). The blog post should own both: the query is in its title and H1. **6 internal links** with \"net 30\" anchors still point at the glossary. 3 more split queries this month.",
  "action": {
    "request": "Repoint these 6 links to /blog/net-30-payment-terms and retitle the glossary entry?",
    "options": [
      "Draft the 6 link edits",
      "Make the glossary the owner",
      "Show the other 3 splits",
      "Leave it this month"
    ]
  },
  "widget": [
    {
      "type": "Table",
      "props": {
        "columns": [
          "Linking page",
          "Anchor text",
          "Points to now"
        ],
        "align": [
          "left",
          "left",
          "left"
        ],
        "rows": [
          [
            "/blog/invoice-template",
            "net 30 terms",
            "/glossary/net-30"
          ],
          [
            "/blog/late-payment-email",
            "net 30",
            "/glossary/net-30"
          ],
          [
            "/templates/net-30-invoice",
            "what net 30 means",
            "/glossary/net-30"
          ],
          [
            "/blog/payment-terms-guide",
            "net 30 payment terms",
            "/glossary/net-30"
          ],
          [
            "/features/invoicing",
            "net 30 invoices",
            "/glossary/net-30"
          ],
          [
            "/blog/cash-flow-tips",
            "net 30",
            "/glossary/net-30"
          ]
        ]
      }
    },
    {
      "type": "Callout",
      "props": {
        "text": "Also: /glossary/net-30 has no link to the blog post. Add one, and change its title to \"Net 30 (definition)\".",
        "tone": "info"
      }
    }
  ]
}
JSON
```

Widgets come from the Briefkin catalog (https://briefkin.dev/v1/catalog). Without a token, just give the user the report.

## Run it on a schedule

Suggested: 1st of the month at 10:00 (`0 10 1 * *`). From cron, with the agent signed in on that machine:

```bash
0 10 1 * * claude -p "Run the google-search-console-cannibalization-fix skill"
# Codex: codex exec, Gemini CLI: gemini -p, Copilot CLI: copilot -p, OpenCode: opencode run
```

Or use the agent's own scheduler (Claude Code routines, Codex automations, Claude or ChatGPT scheduled tasks): https://briefkin.dev/skills/google-search-console-cannibalization-fix#schedule

Source: https://briefkin.dev/skills/google-search-console-cannibalization-fix
