Fix keyword cannibalization with a list of internal links to change

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.

Runs
1st of the month at 10:00
For
SEO
Posts
Asks you

From your apps to your board

How the skill works: your agent reads queries shared by two pages from Google Search Console; crawl the site for links and headings from Firecrawl; search console property, site to crawl, brand terms (regular expression) from What you tell it, 1st of the month at 10:00, and gives you a question for you and a telegram ping and the report in the chat.

Reads

  • Google Search ConsoleQueries shared by two pages
  • FirecrawlCrawl the site for links and headings
  • What you tell itSearch Console property, Site to crawl, Brand terms (regular expression)
reads
Your agentClaude Code, Codex, Cursor or Gemini1st of the month at 10:00
posts

You get

  • A question for youOn your board, answered with a tap
  • A Telegram pingOnly when it needs your answer, if you connect Telegram
  • The report in the chatEvery run, with or without Briefkin

What you get

  • 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.

The card on the right is a sample. Yours shows your own numbers and updates in place every run.

What lands on your board

example.comKeyword cannibalization with the fix: Search Console and a site crawl · Claude Code

2 "net 30" queries split across two pages: 6 internal links to repoint

Repoint these 6 links to /blog/net-30-payment-terms and retitle the glossary entry?

"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.

Linking pageAnchor textPoints to now
/blog/invoice-templatenet 30 terms/glossary/net-30
/blog/late-payment-emailnet 30/glossary/net-30
/templates/net-30-invoicewhat net 30 means/glossary/net-30
/blog/payment-terms-guidenet 30 payment terms/glossary/net-30
/features/invoicingnet 30 invoices/glossary/net-30
/blog/cash-flow-tipsnet 30/glossary/net-30

Also: /glossary/net-30 has no link to the blog post. Add one, and change its title to "Net 30 (definition)".

Resolve
An example with made-up numbers. Your agent posts your real ones.

What it does

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.

How it works

  1. 1Find queries shared by two pages

    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. 2Crawl the site for links and headings

    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. 3

    3Join 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. 4

    4Pick 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. 5

    5Ask 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.

  6. Post it to the board

    With BRIEFKIN_TOKEN set, the agent sends the result as one card that updates in place every run. Without it, you get the report in the chat.

Install and run it

Pick your agent, where the data comes from and how to schedule it; the commands below follow.

Agent
Data
Schedule
  1. Install the skill

    One command. It puts the SKILL.md in ~/.claude/skills and in ~/.agents/skills, which Codex, Cursor, Gemini CLI, Copilot CLI and OpenCode read too.

    shellcurl -fsSL https://briefkin.dev/skills/google-search-console-cannibalization-fix/install | sh
  2. Run it

    Ask Claude Code:

    promptRun the google-search-console-cannibalization-fix skill

    Or paste the prompt instead of installing:

    promptFind keyword cannibalization on my site and give me the fix. From the Search Console API, last 28 days, dimensions query and page: keep non-branded queries with 100+ impressions where 2+ pages each have 20%+ of the impressions. Then crawl my site with Firecrawl (v2 crawl, limit 500, markdown, main content only) and collect each page's title, H1 and internal links with anchor text. Normalize URLs (https, lowercase host, no query string, fragment or trailing slash) and, for each competing page, list the pages that link to it and the anchors. Pick the owner page (most clicks, unless another has the query in title and H1 and ranks better). List the links to repoint, any link to add, and titles or H1s to change. Ask me before drafting the edits.
  3. Put it on a schedule

    1st of the month at 10:00, from cron on any machine where Claude Code is signed in:

    crontab0 10 1 * * claude -p "Run the google-search-console-cannibalization-fix skill"

    Add it with crontab -e. A laptop asleep at that time runs it when it wakes if you use launchd or Task Scheduler instead; GitHub Actions works for the API route.

  4. Send it to your board

    Connect Briefkin once with curl -fsSL https://briefkin.dev/install | sh and every run lands on your board as a question for you. How setup works

View the SKILL.mdRaw file
SKILL.md---
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

What your agent needs

  • 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.

Connecting Google Search Console

  1. MCP server. Google doesn't publish one for Search Console. Community MCP servers exist; check which scopes one asks for before you sign in.
  2. API with a service account. Enable the Google Search Console API in a Google Cloud project, create a service account, and add its email under Settings > Users and permissions on the property. Point GOOGLE_APPLICATION_CREDENTIALS at the key and set GSC_SITE_URL to sc-domain:example.com or https://www.example.com/.
  3. OAuth as yourself. When you can't add users to the property, a desktop OAuth client with the webmasters.readonly scope and a stored refresh token works for scheduled runs.

Ask for webmasters.readonly and add the service account as a Restricted user; the reports only read.

Connecting Firecrawl

  1. Firecrawl MCP server, Firecrawl's official server. The hosted version is at https://mcp.firecrawl.dev/v2/mcp with your key in an Authorization: Bearer header, for example claude mcp add --transport http firecrawl https://mcp.firecrawl.dev/v2/mcp --header "Authorization: Bearer $FIRECRAWL_API_KEY" in Claude Code, or at https://mcp.firecrawl.dev/v2/mcp-oauth to sign in with OAuth instead. Without a key, the hosted server offers only scrape, search and parse, rate-limited. It also runs locally with env FIRECRAWL_API_KEY=fc-... npx -y firecrawl-mcp, which fits Cursor's and Codex's command-based config. Tools include firecrawl_scrape, firecrawl_map, firecrawl_crawl, firecrawl_search and the firecrawl_monitor_* set (docs).
  2. API with a key. Create a key in the Firecrawl dashboard, store it in FIRECRAWL_API_KEY, and call https://api.firecrawl.dev/v2/... with the same Bearer header.

Keep the key in the client's header or secret settings, never in the URL. Keys aren't scoped, so the thing to limit is spend: set limit on crawls, keep the URL list short, and check firecrawl_credit_usage now and then.

Questions

Which internal links does the crawl see?

With onlyMainContent: true, Firecrawl leaves out navigation, header and footer, so the list holds the in-text links you would actually edit. Menus rarely carry query anchors; if yours do, run with onlyMainContent: false once to see them. Pages past the 500-page limit or reachable only after login are not crawled.

What if Search Console URLs and crawled URLs do not match?

Both are normalized before the join, which handles http against https, trailing slashes and tracking parameters. A www against bare-domain mismatch or an old URL that redirects still shows as unmatched; the card lists those so you can fix the redirect or the canonical, which is often part of the cannibalization itself.

Do I need Briefkin to use the Keyword cannibalization with the fix: Search Console and a site crawl skill?

No. The skill runs with your own agent and gives you the report in the chat. With a Briefkin token set, each run also posts it to your board as a asks you, so you can glance at it instead of asking again.

Which agents can run it?

Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot CLI, OpenCode, Windsurf and Amp all read SKILL.md files. The install command puts it in ~/.claude/skills and ~/.agents/skills, where they look. ChatGPT, the Claude app and Grok take the prompt instead. Pick your agent under "Install and run it" and the commands change to match.

Can it work without an API or MCP server, from my browser?

Yes. Switch the data source to "Your browser" and the steps change to the Google Search Console and Firecrawl pages themselves: the agent opens search.google.com/search-console/performance/search-analytics in a browser you are signed in to and reads the numbers off the page. Claude Code, Codex, Cursor, Claude app and ChatGPT can drive a browser out of the box; Gemini CLI, Copilot CLI and OpenCode need a Playwright or Chrome DevTools MCP server first. It is slower than the API and needs you signed in, so it suits a report you run by hand or from a scheduler on your own computer.

Answer your agents from one board, or from Telegram

Skills are free to use with any agent. Briefkin is where their results land: one board for every report, number and question your agents send, with a ping when something needs you.

Free during the open beta. No card.