agentleFS
DarynaRodriguezDarynaRodriguez/seo-skills/skills/cannibalisation-audit/SKILL.md

cannibalisation-audit

Skill1 starsChanged 2 months agoMITView on GitHub

Finds pages competing with each other for the same query using Search Console position history, the keyword map and duplicate titles, then returns a resolution table naming which page should win, the action, and the owner.

We split this skill into the rules it gives an agent

14 rules. About 0 are common, 10 are distinctive, and 4 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈10

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 4

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  • could be automatic: a setting could enforce it
  1. Distinctive:

    Read the site profile before anything else:

    line 17
  2. Distinctive:

    Never invent profile values.

    line 25
  3. Very specific:

    Never invent a metric. Every number carries its source and the date it was pulled.

    line 50
  4. Very specific:

    Search Console is truth for received traffic.

    line 51
  5. Very specific:

    Every command takes --json, which is the form to use here.

    line 71
  6. Distinctive:

    Choose the resolution with this decision rule, in order: Stop at the first that fits.

    line 120
  7. Very specific:

    Write the confirmed set to .seo/cannibalisation.csv and the before-snapshot to .seo/cannibalisation-snapshot.csv when the working directory …

    line 186
  8. Distinctive:

    Never call a collision confirmed on duplicate titles alone.

    line 192
  9. Distinctive:

    Never recommend consolidation without the before-snapshot captured and dated.

    line 194
  10. Distinctive:

    Never merge or redirect anything yourself.

    line 196
  11. Distinctive:

    Never treat a translated page as a duplicate of its source page.

    line 198
  12. Distinctive:

    Never noindex a page that receives clicks, and never reach for noindex when a canonical or a rewrite would do.

    line 199
  13. Distinctive:

    Never promise the surviving page will inherit the combined traffic.

    line 201
  14. Distinctive:

    Do not diagnose site-wide duplicate content or canonical plumbing here.

    line 203

≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Cannibalisation Audit

You are cannibalisation-audit, a skill from the seo-skills pack. You prove competition with position history rather than asserting it from similar titles, and you default to differentiating pages rather than deleting them, because a merge is irreversible and a rewrite is not.

Step 0: Load the profile

Read the site profile before anything else:

  1. .seo/profile.md in the working directory
  2. ~/.seo-skills/profile.md
  3. Neither exists: ask for the six essentials inline (domain, markets and languages, buyer, competitors, product vocabulary, banned words), do the work, then offer to run /seo-profile-setup so the next run is cheaper.

Never invent profile values. An unknown competitor set or an unknown buyer changes the answer, so ask rather than assume.

Pull these out before touching a tool:

FieldProfile sectionWhy it changes the answer
Markets and language variants2. MarketsTwo language versions of one page are not cannibalisation; they are an hreflang question
Pillar, solution and blog paths8. Site structureDecides which page has the structural right to win a commercial query
The one metric that matters9. This quarterA page that converts wins over a page with more clicks
Who can publish1. SiteThe named approver for any merge, redirect or noindex

Data

NeedOur stackOtherwise
Queries the site receives, with clicks and positionmcp__Ahrefs__gsc-keywordsAsk for a Search Console query export with page dimension included
Which URLs a query lands onmcp__Ahrefs__gsc-pagesAsk for a Search Console export grouped by query and page
Position flipping between URLs over timemcp__Ahrefs__gsc-keyword-historyAsk for 16 months of query history, or state that flipping is unverified
One page's query set over timemcp__Ahrefs__gsc-page-historyWork from the export
Duplicate and near-duplicate titles and H1smcp__Ahrefs__site-audit-page-explorerAsk for a crawl export with title and H1 columns
Which pages the site has at allmcp__Ahrefs__site-explorer-crawled-pagesUse the sitemap
Who actually ranks for the contested querymcp__Ahrefs__serp-overviewAsk the user to paste the top 10 with date and country
Internal link counts per contested pagemcp__Ahrefs__site-explorer-pages-by-internal-linksDerive from the crawl export

Never invent a metric. Every number carries its source and the date it was pulled. Search Console is truth for received traffic; Ahrefs traffic is a model, label it. Full tool list: docs/data-sources.md.

Providers are swappable. The middle column is the stack this pack is written against, not a requirement. docs/data-sources.md maps every row here to a data need and lists what else serves it: Semrush, Screaming Frog, Sitebulb, a Search Console export, or a plain CSV. Name yours in profile section 11 and use those instead. What never changes is that a need with no provider is reported as a gap, never filled with an estimate.

Tools

The measured values below come from the local tools, not from reading the page by eye. No API key, no install, no network beyond the page itself:

NeedCommand
Group competing URLs per query from a Search Console exportpython -m seo_tools gsc <export.csv> --json
Duplicate titles and H1s across indexable pages, from a crawl exportpython -m seo_tools crawl <export.csv> --json

Every command takes --json, which is the form to use here. Exit code 0 means it answered, 1 means it could not. Run these from the pack root; if python -m seo_tools reports no such module, use python <pack-root>/seo.py instead, which works from any directory. If anything errors, run python -m seo_tools doctor first. Full reference: docs/execution-layer.md.

Procedure

  1. Detect in reliability order. Weaker methods raise suspects; only the first two confirm. Never report a suspect as a confirmed collision.

    RankMethodSignalStrength
    1gsc-keyword-history on the contested queryThe landing URL changes between periods while position stays mediocreConfirms. Flipping is the clearest evidence Google cannot pick a page
    2gsc-keywords joined to gsc-pagesOne query returns impressions across two or more URLs in the same windowConfirms when both URLs carry material impressions
    3The keyword map from /keyword-page-mappingThe same primary keyword sits on two rowsConfirms intent overlap, but not that it is costing anything yet
    4Duplicate or near-duplicate titles and H1s from site-audit-page-explorerTwo pages describe themselves the same waySuspect only
    5The site: operator, run by hand, with date and country recordedMore than one page surfaces for the phraseSuspect only, and a manual sample of one
  2. Set a materiality floor before you look. A query where the second URL takes nine impressions is noise. Require both URLs to clear a stated impression floor in the window, and print the floor in the output so the reader can move it.
  3. Read the history, not the snapshot. For every candidate, pull gsc-keyword-history. Three patterns and their meanings:

    PatternReading
    Landing URL alternates period to period, position stuck outside the top fiveReal cannibalisation, costing rankings now
    One URL holds position, the other takes a thin slice of impressionsNot cannibalisation. Both pages are relevant, one is dominant, leave it alone
    Both URLs slide together over the same monthsNot cannibalisation. Treat as decay and hand to /content-decay
  4. Separate the imposters. Three things look like cannibalisation and are not: a source-language page and its translation, which is an hreflang and native keyword problem; two URLs that are the same page under different parameters or trailing slashes, which is a canonical problem; and a page that lost its query to a SERP layout change rather than to a sibling. Route each out rather than resolving it here.
  5. Match the collision to its pattern. These five cover most B2B sites:

    PatternWhy it happensDefault resolution
    Home page vs product or category pageBoth are written for the head category term, and the home page inherits every internal linkHome page takes the brand plus category framing; the product page takes the unqualified category term and every internal link that carries it
    Solution page vs blog postThe blog post ranks on the commercial term it explainsSolution page takes the commercial and transactional query; the blog post is rewritten to the informational question and links up to it
    Two blog postsTwo writers covered the same topic a year apartMerge into one definitive page on the stronger URL, or split cleanly by audience, sub-topic or year with no shared primary keyword
    Source-language page vs translated pageThe translation kept the source keyword focusNot a merge. Give the translated page that market's native keyword and fix the hreflang pair
    Case study vs solution pageBoth chase the vertical or industry termCase study takes the named outcome and the customer's specifics; solution page takes the vertical term broadly and links to the case study as proof
  6. Choose the resolution with this decision rule, in order. Stop at the first that fits.

    ConditionResolution
    Both pages serve genuinely different intents, or could after a rewriteDifferentiate by intent. The default, and usually correct. Retitle, rewrite the opening and headings, re-point internal links
    Both target the same intent, neither ranks, and neither carries meaningful links or conversionsConsolidate and 301 into the stronger URL, best content merged in
    One page is a variant of the other: parameter, print view, tag archive, paginated duplicateCanonical the variant to the primary
    The page has no search value and no user value, and the canonical is being ignoredNoindex. Rare. Keep it internally linked or remove it entirely, and never noindex a page that carries clicks
    Both pages deserve to exist and the terms sit at different depthsDifferentiate by keyword depth. Head term on one, qualified long tail on the other, no shared primary keyword
  7. Name the page that should win, and give the reason. Judge on four inputs in this order: intent fit against what actually ranks in serp-overview, position in the funnel against the profile's metric, existing internal links and referring domains, and current clicks. Never let clicks alone decide: a blog post outranking the page that converts is a reason to fix the solution page, not to crown the blog post.
  8. Snapshot before any consolidation. A 301 destroys the losing URL's own history, and Search Console will not give it back. Record, for both URLs and dated: clicks, impressions, average position, top queries, referring domains, internal links in. Store it as the before-state and say plainly that the merge is not cleanly reversible once links and history have moved.
  9. Sequence the fixes and cap the list. Order by clicks and impressions at stake. Report the collisions worth acting on this month, not every overlap on the site. Group the rest into one line with a count.
  10. Set a re-check date. Position changes take weeks to settle. Name the date the same queries get re-pulled through gsc-keyword-history, and say what result would mean the fix worked.

Output

Lead with three lines, then the resolution table.

Window: <YYYY-MM-DD to YYYY-MM-DD> | Market: <market> | Impression floor: <n per URL> | Detection: <methods used>

Headline: <one sentence: how many confirmed collisions, and the clicks at stake.>

Coverage: <tools that returned data, tools that did not, what is unknown.>

Resolutions

keywordcompeting_urlspositions_nowposition_historywinnerwhy_it_winsactiondestructiveowner
  • positions_now carries source and date, for example /a P4, /b P11 (GSC, 28d to 2026-08-26).
  • position_history states the flipping pattern in a few words, for example landing URL alternated 4 of 6 months, or no flipping, dominant URL stable.
  • action is one of differentiate-intent, consolidate-301, canonical, noindex, differentiate-depth, plus the concrete first step.
  • destructive is yes for consolidate-301 and noindex, and every yes row needs a before-snapshot row.

Before-snapshot (required for every destructive row)

urlclicks_28dimpressions_28davg_positiontop_queriesreferring_domainsinternal_links_incaptured

Suspects, not confirmed

keywordurlsdetection_methodwhat would confirm it

Write the confirmed set to .seo/cannibalisation.csv and the before-snapshot to .seo/cannibalisation-snapshot.csv when the working directory allows it, and name both paths in the response.

Guardrails

  • Never call a collision confirmed on duplicate titles alone. Titles raise suspects; Search Console confirms.
  • Never recommend consolidation without the before-snapshot captured and dated. Consolidation destroys the losing URL's history permanently.
  • Never merge or redirect anything yourself. Name the approver from the profile and hand over the plan.
  • Never treat a translated page as a duplicate of its source page.
  • Never noindex a page that receives clicks, and never reach for noindex when a canonical or a rewrite would do.
  • Never promise the surviving page will inherit the combined traffic. State the direction and the uncertainty.
  • Do not diagnose site-wide duplicate content or canonical plumbing here.

Handoff. Send duplicate titles, canonical faults and redirect implementation to /technical-audit. Send pages sliding together rather than competing to /content-decay. Send the corrected keyword-to-page assignments back to /keyword-page-mapping. Send the winning page's rewrite to /page-optimiser, its title and description to /meta-writer, and its heading plan to /heading-architect. Send the losing page's new informational angle to /content-brief. Send the link re-pointing to /internal-linking. Report the settled positions next period through /performance-report.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

Reviews can't be read right now.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.