Free tool · no account · runs in your browser

Keyword cannibalization check

Which queries put two or more of your pages in front of searchers, and how is the visibility split between them? Upload a table with a Query column and a Page column from your own Search Console data. You get the queries where a second page holds a real share, with the split and the positions, and, if the table has dates, how often the page on top changed. It is a list to read, not a diagnosis, and it says so.

Your file never leaves your browser. It is read by JavaScript in this tab; there is no upload, and this page's security policy blocks fetch, XHR, WebSocket, EventSource and beacon calls and form posts.

A CSV with one row per query and page (and, if you can, per day), with Clicks, Impressions and Position beside them. Search Console’s own report cannot show one table of queries and pages, so the four routes below are how you get it. You can drop files here.

What it tells you

The split, shown rather than diagnosed

Who holds the impressions

For each listed query, every page that holds at least a tenth of its page impressions and at least 100 impressions, with its share, its clicks and its average position. Pages under those thresholds are counted but not named.

Whether the top page changes

With a Date column, the number of days on which the page with the most impressions was not the one on top the day before, and how many days each page led. A query where one page always leads and a query where two pages trade places are different situations.

Why it is not a verdict

Two pages for one query can be correct: different intents, or sitelinks. Google counts an impression for each of your pages in a result, so the shares are of the pages’ impressions, not of searches. The page says this before you act on any row.

How to get the table: four routes

You need one table in which each row is a query on a page. Search Console’s Performance report cannot show that in one table: its Queries tab and its Pages tab are separate, and which page earned which query is in neither (you can click a query and then open the Pages tab for one query at a time). These routes were read from Google’s own documentation on 21 Oct 2026, except the Sheets add-on, which is not Google’s. The tool reads columns by what they hold, in any language, so the names below do not have to match.

1. Data Studio (formerly Looker Studio)

  1. Google says the product returned to its original name, Data Studio, in April 2026; older guides call it Looker Studio.
  2. Create a data source with the Search Console connector, choose your site, and in the Tables panel choose URL Impression (the table aggregated by page) and a default search type of web.
  3. In a report, add a table with the query field and the page field as dimensions, and clicks, impressions and average position as metrics. For the days the top page switched, add the date as a third dimension.
  4. Turn Show pagination on in the table’s Style tab: Google says that with it on all of the table’s data is exported, and with it off only the rows shown. Then right-click the table, choose Export data, and export as CSV. A chart exports up to 750,000 rows, and the report’s date range and filters apply.
  5. Google notes that the connector may aggregate queries it does not show into a single row with a blank query. This tool leaves blank-query rows out and says how many.

2. The Search Analytics API

  1. Query with dimensions: ["query", "page"], adding "date" for the days the top page switched, and the search type web.
  2. rowLimit is 1 to 25,000 and defaults to 1,000, so ask for more and page through with startRow until a response has no rows. Google recommends one query a day for one day’s data.
  3. Google says the API “does not guarantee to return all data rows but rather top ones”, exposes at most 50,000 rows a day per search type, sorted by clicks, and may drop data when you group by page or query. A table from the API is a table of the top rows.
  4. Write the rows out as CSV with a header row.

3. A Google Sheets add-on

  1. Search Analytics for Sheets is a third-party add-on, not Google’s. Its own site describes it as requesting Search Console performance data into a spreadsheet and grouping by several dimensions at once.
  2. Group by Query and Page, and by Date too if you want the switch count. The menu wording is the add-on’s and may change.
  3. Download the sheet as CSV. Check how many rows you got: a table of exactly 1,000 rows is probably cut, and this tool says so.

4. The BigQuery bulk export

  1. Google says a bulk export sends all the performance data available for your property to BigQuery, except anonymized queries, and that it starts from the day you set it up: for earlier days use the API or the reports. The table is searchdata_url_impression.
  2. Google says rows can repeat a key, so always aggregate, and that sum_position is zero-based: average position is SUM(sum_position) / SUM(impressions) + 1. This query is adapted from Google’s own sample:
    SELECT
      query,
      url,
      SUM(clicks) AS clicks,
      SUM(impressions) AS impressions,
      (SUM(sum_position) / SUM(impressions)) + 1.0 AS avg_position
    FROM searchconsole.searchdata_url_impression
    WHERE search_type = 'WEB'
      AND query != ''
      AND data_date BETWEEN DATE_SUB(CURRENT_DATE(), INTERVAL 90 DAY) AND CURRENT_DATE()
    GROUP BY query, url
    Add data_date to the SELECT and the GROUP BY for one row per day. Do not export sum_position as the position column: it is not a position.
  3. Run it, and download the result as CSV.

How big a file it takes. This page opens up to 64 MB of files at once and reads up to 200,000 rows or 12 MB of table text. A table of queries by pages by days grows fast: use a shorter range or a section of the site if the page refuses it, and it will say which limit it hit.

What the table cannot tell you

  • It is a heuristic. A query is listed when a second page holds enough of the impressions. That is a description of where impressions went. It does not show that the pages compete, that either is held back by the other, or that merging them would raise clicks.
  • Two pages can be right. A guide and a product page can both serve one query because searchers want different things. Google describes sitelinks as links from the same domain clustered under a text result, so one result can carry more than one of your pages.
  • Impressions are counted by page. Google says that when data is aggregated by page, each unique URL in a result is counted separately, even if they point to the same site. Two of your pages in one result are two impressions. A page’s share here is its impressions over the sum for all of the query’s pages in your table, and that sum can exceed the number of times the query was searched.
  • Position is an average. Each page’s position is the impression-weighted average of the position in your rows, and a position averaged over a month can hide a page that moved from 3 to 40.
  • The table is incomplete. Google says Search Console stores and shows only the most important data rows, omits anonymized queries, and cuts a table at 1,000 rows; the API says it does not guarantee to return all data rows. A table of exactly 1,000 rows is warned about as probably cut. Queries and pages in the tail are not here, so “no query listed” describes the table, not your site.
  • Dates are Pacific Time, and the newest day can be preliminary, so it is left out of the switch count.
  • Case and spacing. Two spellings of a query that differ only in capitals or spacing are one query here. Two spellings of a page address that differ only in the scheme, www or a trailing slash are one page.

What happens to your file

Nothing leaves your browser. The file is read by JavaScript running in this tab and parsed in memory; it is not uploaded, there is no copy on a server, and nothing from it is written to a cookie or to local storage (the theme switch remembers light or dark and nothing else). The page loads no analytics and no third-party scripts.

You do not have to take our word for it. Open your browser's developer tools, choose the Network tab, run a check, and watch it make no request. Two things back that up. This page carries a content security policy, readable in its source, that blocks fetch, XHR, WebSocket, EventSource and beacon calls (connect-src 'none') and form posts (form-action 'none'), and allows images only as inline data: URIs. A content security policy does not cover every browser feature (WebRTC, for one), so the second check matters: the project's tests read this page's own script and every module it imports, and fail on any of the browser's network APIs.

The site-wide script penlid.js, which every page loads, contains network calls for the signed-in app's account switcher. This page never calls it, and a test checks that those calls sit only inside the switcher.

The one thing that leaves this page's memory is the one you ask for: the CSV you click to download.

Methods: the rules and thresholds

Each number below is a constant in public/assets/lib/page-tools.js (the read-size limits are in tool-ui.js and gscsv.js). The project's tests compare the numbers quoted here with those constants; that is a check on the numbers, and not on every sentence. They are judgement calls, and they are the owner's to change.

Reading the file
A table is recognised by its columns, not its name. It needs a query column, a page column and an impressions column. A table taken with Compare, which holds two periods, is refused. A Search Console ZIP, whose Queries and Pages files are separate tables, is refused with the reason. A row with no query is left out and counted with its impressions; a row with a negative or fractional value is not used and is named. Rows with the same query, page and date are added together, as Google says to do with bulk-export rows. Anything the parser skipped or ignored is listed under “What the parser could not read” at the top of the result: rows it skipped, columns it did not recognise, a second file it did not use, and blank cells it read as zero. A skipped row also gets a warning, because a split could be hiding in it.
What counts as one query and one page
Queries are compared after Unicode normalisation (NFKC), lower-casing and collapsing spaces. Page addresses are compared after dropping the scheme, a leading www., trailing slashes and a #fragment, keeping the query string; percent-escapes are decoded, so %C3%A9 and an accented letter are one address.
When a query is listed
At least two of its pages each hold at least 10% of the query’s page impressions and at least 100 impressions. A page’s share is its impressions over the sum of the impressions of all of the query’s pages in the table. Queries are ordered by that sum, largest first; the page shows the first 100 and the CSV has all of them.
Positions
The average of the position column over a page’s rows, weighted by each row’s impressions. With no position column, none is shown.
Days the top page switched
Needs a Date column with one row a day, written so that the day is certain (2026-06-05); a column whose dates cannot be read is said to be unreadable, with the parser’s reason, and is not called a missing column. The top page of a day is the page with the most impressions that day. A day counts only when the query had at least 10 impressions that day over all its pages; a day on which two pages tie for the most has no top page and is skipped; the newest day in the file is left out. A switch is a counted day whose top page differs from that of the counted day before it, so a gap in the data does not split a run. This is a count of days in the file, not a count of ranking changes.
A table that is probably cut
A table of exactly 1,000 rows is warned about, because that is Search Console’s table limit and the API’s default row limit. A table of any other size is not called complete either.
The CSV
One header, then one row for every listed query and page. A cell that would start with =, +, -, @, a tab or a carriage return gets a leading apostrophe, so a spreadsheet does not run it as a formula. The file is plain UTF-8 with no byte-order mark; to open it in Excel with non-ASCII text intact, use Data > From Text/CSV and choose UTF-8.

Where the facts about Google come from

Each statement about Google on this page was read on 21 Oct 2026 from one of these pages, which Google can change.