Fixing Keyword Cannibalization With Claude Code

From 140 to 561 Cannibalized Queries a Day

Between late June and mid August, the number of queries where two or more amicited.com URLs ranked on the same day climbed from around 140 a day to a peak of 561. At its worst, cannibalized queries made up 11.6% of everything the site ranked for.

So I pointed Claude Code at the site’s Hugo repo, connected it to the AmICited SEO MCP server, and asked it to find the keyword cannibalization, decide what to do about each case, and apply the fix across all 16 languages the site ships in.

This is the method end to end: the prompts I used, what the agent actually called and did, and the three things it surfaced that I never asked about. None of it is deployed yet, so consider this a walkthrough of the process, not a results post. The 28-day re-check comes later.

Dashboard chart showing cannibalized queries per day climbing from about 140 to a peak of 561 between late June and September, with a worst contests table below it

Keyword cannibalization is when two or more pages on the same site compete for the same search query, so Google keeps swapping which one it shows and neither one ranks as well as a single strong page would. It’s internal competition between your own URLs, and it’s one of the SEO jobs where Claude Code earns its keep: the data lives in Search Console, the fix lives in the repo, and an agent can hold both at once. (Don’t confuse it with AI content cannibalization, where an AI-generated answer takes the click your page would have earned instead.) If you want the full breakdown, we’ve written up how to identify and fix keyword cannibalization issues separately.

The Setup

  • Repo: my Hugo site’s repository, 500+ English blog posts, each translated into 15 other languages.
  • Data: the AmICited MCP server, which exposes Google Search Console-based SEO reports, including cannibalization, as tools Claude Code can call directly.
  • Agent: Claude Code (Opus 5.5) running in auto mode, on a fresh branch, seo/cannibalization-oct-2026. Nothing gets committed without me reading the diff first.

Connecting the MCP server is a one-time OAuth step: run /mcp, pick the server, approve the workspace in the browser, and Claude Code can call it from then on.

OAuth authorization screen for connecting Claude Code to the AmICited MCP server, asking to approve read and manage access for a specific workspace
Claude Code terminal showing the /mcp command with the response: Authentication successful, Connected to amicited

One thing worth knowing up front: my AmICited workspace has several domains in it. Every prompt I wrote named amicited.com explicitly, and the first one asked Claude Code to tell me which domain ID it picked. Skip that step and you’re trusting the agent to guess correctly.

Logo

Ready to Monitor Your AI Visibility?

Track how AI chatbots mention your brand across ChatGPT, Perplexity, and other platforms.

Step 1: Find Keyword Cannibalization, Cut the Noise

Use the AmICited MCP server to pull the keyword cannibalization report for the amicited.com domain only (the workspace has several domains, pick the one for amicited.com and tell me which domain/project ID you used). Use Google Search Console data, last 90 days. First list the MCP tools you call. Exclude noise: queries with search operators (site:, inurl:), pure brand/navigational queries (“amicited”, “am i cited”), and queries under 50 impressions. Show the top 15 real cannibalization clusters as a table […] Do not edit any files.

The noise filter exists because of what the raw report actually looks like. The “Worst contests” list on my dashboard was led by site:www.amicited.com (417 URLs), site:amicited.com (277 URLs), and the bare brand query amicited (193 URLs). That isn’t cannibalization. That’s me, and probably my own team, searching the site directly. Any tool that counts that as a cannibalization cluster is counting the wrong thing.

Claude Code terminal running the cannibalization report prompt, showing it found amicited.com among several domains and pulled the top 200 of 10,152 cannibalized queries

What it did, in order:

  1. list_domains, to find amicited.com among the domains in the workspace (and report back the domain ID it used).
  2. search_tools, because the cannibalization tools weren’t in its default tool list, so it searched for them by description.
  3. describe_tool, twice, to read the input schemas for the two cannibalization-query tools before calling them.
  4. The cluster-list tool, once: the top 200 queries by severity, out of over 10,000 cannibalized queries in the 90-day window.
  5. The per-query detail tool, 15 times, once per cluster, pulling the competing URLs and their daily positions. One response was too large to display inline, so it saved the output to disk and summarized it with jq.

Then it filtered. About 30 of the top 200 were site: queries. On its own initiative, it added two more filters and told me it had done so: long, prompt-shaped queries with thousands of impressions and zero clicks (they read like AI tracking-tool prompts copied verbatim, not real people searching), and a handful of code-like queries that looked like bot traffic.

Table of the top 15 cannibalization clusters with a how-to-read-this section noting that most of them are not really split traffic

The most useful line in the output wasn’t the table itself. It was this:

Most of these aren’t really split traffic. In nine of the top fifteen clusters, one URL gets over 97% of the impressions and the rest get a handful.

Nine of the top fifteen “cannibalization” clusters were one page doing essentially all the work, with a second URL that had shown up for a few days and nothing more. A report sorted purely by severity won’t tell you that. Reading the per-URL split does.

Step 2: Diagnose Before You Fix

For the clusters in your table where impressions are genuinely split between pages (not the ones where one URL has 97%+), open the competing pages in content/ and classify each: CONSOLIDATE, DIFFERENTIATE, FIX-TECHNICAL, or LEAVE. Pick the winner by clicks, then position, then internal links pointing to it. Check how this Hugo site handles redirects today. Do not edit anything yet. Output a decision table with a one-line reason per row.

This step made no MCP calls at all. It was about 20 shell commands: reading the competing Markdown files, counting internal links to each page, reading the theme’s templates, and running curl against the live site to see what the redirects and hreflang tags actually returned in production, not just in the repo.

Claude Code terminal confirming that none of the server-side 301 redirect rules are live, meaning every redirect on the site is currently a Hugo alias page with a meta refresh

Of the fifteen clusters, one needed a merge, one needed a retarget, two needed a technical fix, and the rest needed nothing. That ratio is the main reason I’d never let an agent jump straight from “here’s a cannibalization report” to “here are the pages I deleted.”

Three Findings I Didn’t Ask For

1. My 301 redirect rules had quietly stopped working two months earlier. My site has a script that generates real 301 rules for Amplify to serve. Claude Code noticed that the output file didn’t exist, walked back through git history, and found it had been deleted five weeks earlier inside an “update content” commit that touched over 21,000 files, collateral damage from a bulk sync, not a decision anyone made on purpose. It confirmed on the live site that none of the rules were active (a moved URL returned a 404 where a redirect should have been). Since then, every “redirect” on the site had actually been a Hugo alias: a 200-status page with a meta refresh, not a real 301.

2. That wasn’t just theoretical. The old URL of a page I’d moved in July was still showing up on its own in Search Console, 188 impressions and counting, two and a half months after the move. A meta-refresh stub that Google keeps indexing anyway is exactly what a broken alias setup looks like in practice.

3. It caught its own mistake. In step 1, it had flagged one page pair as a likely hreflang problem, since a translated version was outranking the English original. In step 2, it fetched the live HTML, checked that the hreflang tags were absolute and reciprocal the way Google requires, and reported that this corrected what it had said the first time. The finding moved from FIX to LEAVE. I’d much rather get that than a confident wrong answer that sits unchallenged in a report.

Step 3: Apply the Fix in 16 Languages

Approved. Apply the decision table on this branch, do not commit. 1) CONSOLIDATE […] without fabricating facts or adding em-dashes, then delete the loser in every language it exists in, add its old URL to the winner’s aliases in each matching language, and repoint all internal links to the loser across content/. 2) DIFFERENTIATE […] in all languages, and add one contextual link to the winner. 3) FIX-TECHNICAL: check git history to see whether the static/_redirects deletion was deliberate. If it was accidental, regenerate them […] If it was deliberate, do not restore, just tell me.

It fact-checked before merging anything. The page being retired had sections the winner didn’t: a mention of a competing tool, a product-ranking feature, and a roundup of free trials. Before carrying any of that forward, Claude Code fetched the vendors’ live sites to verify it was still accurate.

Claude Code terminal fetching competitor vendor sites to verify claims before merging content, finding one tool had shut down new signups and confirming pricing on another

One of the named tools turned out to have been acquired and closed to new signups weeks earlier, so that became a correction instead of something copied forward as current fact. The retired page’s pricing claim for another tool also conflicted with the number already verified on the winning page, so the verified figure stayed and the stale one didn’t make it into the merge.

Git diff showing Claude Code retargeting a page's title, description, keywords, and intro paragraph as part of the DIFFERENTIATE fix, plus a contextual link added to the winning page

For the FIX-TECHNICAL case, it checked whether the redirect deletion had been deliberate before touching anything. Confirming it was accidental (the bulk commit that removed it touched thousands of unrelated files with no mention of redirects), it regenerated the rules and added real 301s for the pages affected by the merge and the moved URL still showing impressions under its old address. For CONSOLIDATE and DIFFERENTIATE, it repeated the same change across every language the affected pages existed in, not just English, then ran a full production build to confirm nothing broke.

What’s Next

After you publish new content, that’s not the end of the job, it’s just the beginning. Knowing exactly which URLs are quietly competing with each other, which ones need to be merged, and which ones just need a technical fix is what keeps that content from working against itself. Nothing from this pass is live yet; the 28-day re-check, to see whether the cannibalized-query count actually comes back down, is next.

If you want to see this kind of cannibalization report on your own domain, book a call and we’ll walk through it.

Frequently asked questions

Yasha is a talented software developer specializing in Python, Java, and machine learning. Yasha writes technical articles on AI, prompt engineering, and chatbot development.

Yasha Boroumand
Yasha Boroumand
CTO, FlowHunt

Find Your Own Cannibalized Queries

AmICited pulls keyword cannibalization straight out of Google Search Console and exposes it as MCP tools, so Claude Code (or any agent) can query it directly and act on it.