DEV Community

Jim L
Jim L

Posted on

How I Fixed 3 Cannibalizing Blog Pages — Real GSC Data + Next.js Fix

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..."
Enter fullscreen mode Exit fullscreen mode

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:

  1. 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.
  2. 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."
Enter fullscreen mode Exit fullscreen mode

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..."
Enter fullscreen mode Exit fullscreen mode

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..."
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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:

  1. It signals pillar intent to Google. When satellite pages consistently link to a specific page as the "full" version, Google consolidates ranking signals there.
  2. 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=canonical across 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"
Enter fullscreen mode Exit fullscreen mode

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}")
Enter fullscreen mode Exit fullscreen mode

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:

  • title and description in 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)