Google Search Console flagged something odd on one of my Next.js blogs this week: three different pages were all ranking for the same keyword at positions 5 to 8 — but not one of them had a single click.
That is textbook keyword cannibalization, and it took me about thirty minutes to fix. The part I found interesting is that the fix is almost entirely at the content layer, not the technical layer. Next.js already gives you the tools you need — the question is whether your frontmatter and internal linking are doing what they should.
Here is the full walkthrough with the actual data.
What GSC Actually Showed
Pulling the queries report for the last 28 days and filtering to the problematic term, I got something like this:
| URL slug | Position | Impressions | Clicks | CTR |
|---|---|---|---|---|
| page-a | 5.1 | 14 | 0 | 0% |
| page-b | 5.2 | 12 | 0 | 0% |
| page-c | 5.6 | 11 | 0 | 0% |
For the same four-word query, Google was seeing three pages it thought were roughly equally relevant and none of them were clearly best. Searchers got confused, none of them clicked.
Meanwhile the same three pages had different keyword wins elsewhere:
| URL slug | Winning KW | CTR |
|---|---|---|
| page-b | "X vs Y" | 9.1% |
| page-c | "Z vs Y" | 22.2% |
So the pages themselves were fine. Each had a specific comparison angle that was working. The problem was the shared, broader keyword — they were all undifferentiated on it, and Google could not decide which to rank.
The Diagnosis (5 minutes)
Open each page's frontmatter. Look at the title and description. Do any of them look nearly identical?
Mine looked like this:
# page-a
title: "X Tools Hong Kong: Y vs Z vs W"
description: "X tools in Hong Kong compared. Y, Z, W fees, returns..."
# page-b
title: "X Tools Hong Kong — Y vs Z vs W Fees and Returns Compared"
description: "Hong Kong X tools comparison. Y 0.2-0.8%, Z 0.35-0.65%..."
# page-c
title: "Y vs Z Hong Kong: Which One Fits Your Style"
description: "Y vs Z vs W Hong Kong comparison. Fees 0.1-0.8%, honest downsides..."
Page A and B were nearly identical. Both listed Y, Z, and W in the title. Google saw them as the same intent page. Page C was doing better on its specific 2-way compare term (9.1% CTR) but the description still mentioned W, which pulled it into the broader three-way competition.
The Fix: Differentiate Intent, Don't Canonicalize
The first instinct is to add canonical meta pointing everything at one page. I decided against that for two reasons:
- The pages have different specific-term wins (9.1% and 22.2% CTR on their own terms). Canonicalizing everything to page B would lose those.
- Once you canonical a page, Google treats it like a duplicate and may stop crawling it meaningfully. Reversible but not cheap.
Instead: differentiate the titles and descriptions to match different search intents, and let internal linking consolidate topic authority on a pillar.
Page A became beginner-focused (new to the space):
title: "X for Beginners: Y's $0 Minimum, Z Core, and W Fund Smart"
description: "First-time X in Hong Kong? Y's $0 min, Z Core, W Fund Smart compared for beginners — plus when DIY is actually cheaper."
Page B became the pillar (canonical target for the broad term):
title: "X Hong Kong Comparison: Y vs Z vs W Fees, Returns, and MPF"
description: "Hong Kong X comparison, April 2026. Y 0.2-0.8%, Z 0.35-0.65%, W 0.25-0.6% — full fee stack, MPF integration..."
Page C stayed narrow (the 2-way compare winner):
title: "Y vs Z Hong Kong: Which One Fits Your Investment Style"
description: "Y vs Z Hong Kong head-to-head, April 2026. Fees from 0.1-0.8%, real portfolio allocations, ERAA vs Core philosophy..."
Notice the description change on page C — I removed vs W from the description. That single change narrowed the search-intent match so the page stops competing for the broad term.
The Internal Linking Piece
Differentiated titles are only half. The pillar page (B) needs to accumulate topical authority from the satellite pages (A and C). So I added a Related Reading callout at the top of A and C:
> **Want the full Hong Kong X landscape?** This article is a head-to-head between Y and Z only. For W added to the mix, see our [X Hong Kong Comparison](/en/blog/pillar-slug) pillar.
In Next.js markdown/MDX, this is just a standard link — remark-gfm handles blockquotes, and the <Link> component in your layout picks up internal URLs. No special config.
Two reasons this matters more than most people think:
- It signals pillar intent to Google. When satellite pages consistently link to a specific page as the "full" version, Google consolidates ranking signals there.
- It improves UX. Someone landing on page C who actually wanted the three-way compare now has one click to the pillar.
What I Did Not Do
I did not:
- Change slugs (costs 301 redirects and rankings).
- Add
rel=canonicalacross pages. - Touch the sitemap.
- Request reindexing manually (IndexNow handled it automatically — more on that below).
The fix is title frontmatter + description frontmatter + one markdown callout per satellite page. That is a 10-line diff per file.
Pushing the Fix Live
Next.js blog, deployed via GitHub Actions to a VPS. The commit was three file edits. CI ran in 5 minutes 36 seconds. Page built, deployed, verified with curl.
curl -s https://mysite.example.com/en/blog/pillar-slug | grep -oE "Hong Kong Comparison"
Returns the new title. Good.
IndexNow for Fast Re-crawl
One thing I did want: fast re-crawl, because Google's existing cached version of those three pages still showed the old titles. If a searcher saw the stale cached result, they would click based on old framing. I wanted Google to refresh those specific URLs today, not in two weeks.
IndexNow does this. It is a simple API supported by Bing, Yandex, and others (Google still does not endorse it but rumor has it they read the signals). The request is one POST with a key file at your root.
import requests
payload = {
"host": "mysite.example.com",
"key": "YOUR_KEY",
"keyLocation": "https://mysite.example.com/YOUR_KEY.txt",
"urlList": [
"https://mysite.example.com/en/blog/page-a",
"https://mysite.example.com/en/blog/pillar",
"https://mysite.example.com/en/blog/page-c",
],
}
for endpoint in [
"https://api.indexnow.org/indexnow",
"https://www.bing.com/indexnow",
"https://yandex.com/indexnow",
]:
r = requests.post(endpoint, json=payload, timeout=20)
print(f"{endpoint}: {r.status_code}")
Three endpoints, three 200/202 responses, done in under a second. Bing typically re-crawls within 24-48 hours. In my experience, Googlebot follows Bingbot traffic spikes surprisingly closely, so the effect often shows up indirectly within a week.
The Thing I Almost Missed
Before shipping, I triple-checked one thing: the page with the 22.2% CTR on its specific 2-way compare term. That was the best-performing page on the whole site for that angle. Canonical-ing it, changing its slug, or even over-editing its title could destroy that win.
So that page got zero changes except the Related Reading callout at the top. Description stayed the same. Title stayed the same. I only changed the other two pages' titles to deflect the broad-term competition away from it.
It is easy to over-engineer an SEO fix. Find the page that is working and leave it alone. Change the pages that are stealing its share.
Results Timeline
Position re-calibration on cannibalization fixes typically takes 2 to 4 weeks for Google to settle on which page wins for which intent. I will know by early May whether the pillar consolidates or whether the three pages re-split.
What I am watching in GSC:
- Pillar page (B) impressions on the broad term — should go up
- Beginner page (A) and specific compare (C) impressions on the broad term — should go down (by design)
- Specific compare (C) on its 2-way term — should stay flat or go up slightly
- Clicks on the pillar's CTR — should be the biggest win, from 0% to 2-5% range
If all four move that direction, the fix worked. If the pillar's impressions drop instead, something else is wrong — either the title is too narrow now, or the internal links need stronger anchor text.
The Takeaway for Next.js Devs
Keyword cannibalization is almost always a content-layer problem masquerading as a technical one. Most stacks give you what you need:
-
titleanddescriptionin frontmatter - Internal linking via
<Link>or markdown - Canonical URLs derived automatically from file path
The work is in the audit and the differentiation, not in the code. Read your own descriptions out loud — if two of them are answering the same question with the same words, Google is going to think the same thing.
Ten minutes of honest editing and your GSC report starts looking different in a month.
Top comments (0)