Keyword cannibalization, or content cannibalization, is two of our own pages ranking for the same queries and splitting the impressions between them. I find it in Search Console by pulling query and page together, then measuring, for each page in a pair, what share of its own impressions comes from the queries it shares with the other page. That percentage and the position gap on those shared queries decide whether one page gets redirected into the other. Search Console calculates neither, so this is a check I run on the data itself, and these days a question I ask Claude.
Search Console, GA4 and your CMS (WordPress here, since that’s what we run) are three separate places to check, with three logins, three exports and three URL formats that do not match by default. Joined into one table on the same URL key, they become one dataset you can query, and both checks in this post run on that table.
That table makes two things visible that the separate reports do not: keyword cannibalization, and content updates whose results get judged before Google has recrawled the page.
Both show up on any site where more than one person has edited content over time, as strategy shifts and different people make calls without a shared view of what is already live.
I check for both with the cannibalization module in an internal dashboard built around a process I ran by hand for years before it existed: pulling Search Console, GA4, and WordPress data separately and merging it myself in spreadsheets. The dashboard also covers editorial priorities, opportunity scoring and page-level performance. This post is about the two checks above. Making a call between two competing pages takes three sources, each doing a different job:
- Search Console shows which one Google already prefers, and when it last crawled either.
- WordPress shows when each was last edited, so you know whether a before/after read is even trustworthy yet.
- GA4 shows the engagement rate for each, which is how I decide whether the page I’m keeping needs anything from the one I’m redirecting.
How I find keyword cannibalization in Search Console
The pull is query and page together, with impressions and average position for each row, over the last 30 days. Non-branded queries only, and each query needs at least 30 impressions in that window, so a handful of stray impressions doesn’t create a pair.
For every query where two of our pages both show, I work out two numbers per page: the share of its own impressions that comes from the queries it shares with the other page, and its average position on those shared queries only. A page drawing 80% or more of its impressions from queries the other page outranks it on is a merge and redirect candidate. An 80% concentration over 3 shared queries is the same signal as 80% over 30.
Search Console’s Performance report shows one page’s queries at a time and exports 1,000 rows. It has no view that puts two pages next to each other and it doesn’t calculate the share, which is why I ran this in a spreadsheet for years after exporting, and why the cannibalization module in the dashboard exists.
The pair I found
The two pages share eight queries. Page A draws 38% of its own impressions from that shared set, and Page B draws 100%.
The URLs and queries are relabeled. The pages and numbers are real ones from our own site.
- 38.0% of Page A’s impressions come from these 8 queries
- 100.0% of Page B’s impressions come from these 8 queries
| Query | Page A impr. | Page A pos. | Page B impr. | Page B pos. |
|---|---|---|---|---|
| Query 1 | 668 | 3.0 | 44 | 3.0 |
| Query 2 | 26 | 26.0 | 46 | 6.2 |
| Query 3 | 69 | 7.9 | 1 | 13.0 |
| Query 4 | 35 | 6.7 | 33 | 9.6 |
| Query 5 | 17 | 8.0 | 2 | 9.5 |
| Query 6 | 11 | 7.2 | 7 | 10.7 |
| Query 7 | 8 | 9.0 | 1 | 7.0 |
| Query 8 | 4 | 77.8 | 3 | 88.3 |
Keep Page A. It averages position 4.8 on the shared queries versus 8.1 for Page B, Google already prefers it. Page B should be merged into Page A and redirected, it draws no impressions from outside this overlap.
Unique to Page A: 1,367 impressions outside the shared set. Unique to Page B: none of significant volume.
Page B has no traffic outside the shared queries, and Page A outranks it on every one of them. That’s not visible from either page’s Search Console numbers alone, only when you check what percentage of each page’s traffic comes from the shared set, a percentage Search Console doesn’t calculate for you.
Contrast this with a pair where the overlap is real but partial, say 40% for one page and 20% for the other. Redirecting there is riskier: both pages draw meaningful traffic the other doesn’t touch, so consolidating would throw away real, unique impressions. The shared queries can look the same in both cases, and the percentage is what tells them apart.
Before I redirect anything, I check GA4
Search Console only measures visibility, not what happens after the click. If the losing page has better engagement metrics, I update the winner’s format to match it before redirecting.
So before I acted on the recommendation above, I pulled GA4 engagement data for both pages over the same window:
| Metric (last 3 months) | Page A | Page B |
|---|---|---|
| Sessions | 53 | 10 |
| Engagement rate | 47.2% | 50.0% |
| Avg. engagement time | 31s | 22s |
Nothing here changes how Page A ends up looking. Page B’s engagement rate edges out Page A’s by under 3 points, but that’s on a sample of 10 sessions, too small to treat as a real difference. Its average engagement time is shorter. If Page B had real volume behind that engagement rate, or if its engagement time were ahead too, that’s the pattern that would send me back into Page A’s content to borrow whatever Page B was doing right, before touching the redirect. Here, there’s nothing worth carrying over.
That is the usual outcome. Most of the time GA4 turns up nothing worth changing about the page I’m keeping. I run the check every time anyway, because when it does turn something up, I want to update that page’s format before the redirect goes in, while the losing page is still live to compare against.
Why I don’t trust a before/after check without a crawl date
The usual way to measure a content update is to change the page, note the date, come back in a couple of weeks and compare Search Console numbers before and after. It reaches the wrong conclusion whenever Google has not recrawled the page in between.
Search Console’s Performance data doesn’t record when Google recrawled a page, only when clicks and impressions were logged. You can get a page’s last crawl date from the URL Inspection tool in the GSC UI, but only one URL at a time, and there’s no way to export it, you’d have to check and note down each URL by hand. Confirming it at any scale means the URL Inspection API instead.
So before trusting a before/after comparison, I check whether Google has recrawled the page since I edited it. If not, I stop. The dashboard also flags this automatically: any edit inside the last 14 days is too fresh to read, on top of Search Console’s 2-3 day reporting delay and GA4 dropping the current day as partial.
New content needs the same check, in reverse. The data point is when the new page’s URL first shows impressions in Search Console, not the publish date. Zero impressions for the first couple of weeks may just mean it’s not indexed yet, not that it’s underperforming.
What I check, every time
On every page pair, whether or not anything looks off:
- Any pair where one page draws 80%+ of its own impressions from queries the other page already outranks it on gets flagged for a merge or redirect decision.
- Position gap gets checked on the shared queries specifically, not each page’s overall average position, which is diluted by every unrelated query it also ranks for.
- Before acting on that flag, I pull GA4 engagement rate and average engagement time for both pages. If the “losing” page is ahead on both, with real session volume behind it, I update the winner’s format to match it before redirecting.
- Pairs with meaningful traffic unique to both pages get a closer look instead of a redirect. I check whether the overlapping queries matter to each page on its own terms, and how much of each page’s unique impressions they bring in, before deciding whether the two need differentiating or can stay as they are.
How I ask for it instead of exporting it
Once this is one connected dataset, I ask directly instead of exporting three files and comparing them in a spreadsheet. The first prompt is the keyword cannibalization check itself, the same calculation described above, run on connected data:
Which of our page pairs have one page drawing 80%+ of its own impressions from non-branded queries it shares with another page, each query with a minimum of 30 impressions over the last 30 days? Doesn't matter if that's over 3 shared queries or 30.
Pull GA4 engagement rate, average engagement time, and sessions for [url] and [url]. Flag it if either is ahead on both engagement metrics.
Has Google recrawled [url] since I last edited it? If not, don't compare before/after numbers yet, tell me when it's safe to check.
Give me our top 10 cannibalized page pairs, ranked by overlap percentage. For each, show how much traffic each page gets, last crawl date, publish date, and engagement metrics for both.
These are ordinary questions once the sources are connected. Before that, three logins and three URL formats had to be reconciled before the analysis could start.
🚀 See what your own Search Console, GA4, and WordPress data looks like connected in one place. Free forever plan, no credit card. Try Windsor.ai.
What I’m doing about this pair
Before redirecting, I check whether Page B has any content Page A doesn’t, something worth merging in rather than losing. Here it doesn’t: everything Page B ranks for, Page A already ranks for too, and outranks it on average. Page A keeps 1,367 impressions from outside the overlap. Page B’s contribution outside the overlap rounds to nothing, nothing meaningful to lose by redirecting it in.
Conclusion
Keyword cannibalization and measuring an update too early are both common, and both stay hidden unless Search Console, GA4 and WordPress are read together. The three connectors this runs on:
I’ve built this kind of dashboard at previous jobs, more than once, and each time step one was authenticating every source myself, which takes a few minutes per connector and does not always go smoothly. Through Windsor’s connectors I can also share the dashboard, or a Claude skill built on it, with a teammate without handing over an API key, and with anyone who already has the same connectors set up on their side.
The tools behind this
I pull this from our own Google Search Console, Google Analytics 4, and WordPress connectors. If you also want this queryable from Claude, here’s the Windsor MCP setup guide.
FAQs
How do I find keyword cannibalization in Search Console?
Pull query and page together with impressions and average position for the last 30 days, keep non-branded queries with at least 30 impressions, and find the queries where two of your pages both show. For each page in a pair, work out the share of its own impressions inside that shared set and its average position on the shared queries only. The Performance report shows one page’s queries at a time and exports 1,000 rows, so this is a spreadsheet job on the export, or a question to ask once Search Console is connected to Claude.
Is this a real example or a made-up one?
Real, pulled from our own internal dashboard against windsor.ai’s own Search Console and GA4 data. URLs and queries are relabeled Page A/B and Query 1-8 so this doesn’t expose our keyword targeting.
Why check GA4 if Search Console already shows which page is winning?
Because Search Console only shows visibility, not what happens after the click. GA4’s engagement rate and average engagement time fill that gap: a page can be losing badly on visibility while still having better engagement metrics. If that is the case, update the winning page’s format to match before redirecting.
How do I decide whether to redirect a cannibalized page, or leave both live?
Check what’s unique to each page outside the shared queries, and check GA4 engagement for both. If one page has close to zero unique traffic and no engagement edge, redirecting costs almost nothing. If both carry real unique traffic, look at the overlapping queries themselves, whether they matter to each page on its own terms and how much of each page’s unique impressions they bring in, before deciding to differentiate the two or leave them as is.
Can I get the exact date Google recrawled a specific page?
Yes, from the URL Inspection tool in the GSC UI, but only one URL at a time, with no way to export it. Search Console’s standard Performance data doesn’t have it at all, only sitemap-level timestamps. Checking many URLs means the URL Inspection API instead.
Why don't Search Console, GA4, and WordPress URLs match automatically?
Search Console stores full URLs, GA4 stores paths, WordPress stores permalinks. Query strings, trailing slashes, and casing differ across all three, so every row needs to be normalized to the same key before anything joins correctly.
Is there a keyword cannibalization tool inside Search Console?
No. Search Console shows queries per page and pages per query, one filter at a time, and doesn’t calculate overlap shares or the position gap on shared queries. The first prompt in this article runs that calculation once Search Console is connected to Claude, and the cannibalization module in the dashboard I built does the same across every pair on the site.
Do I need a custom tool to do this?
No. Connecting Search Console, GA4, and WordPress through Windsor.ai gives you the same joined, normalized data without building or maintaining the integration yourself, and once connected, you can ask Claude or ChatGPT about the blended data directly in plain language.

