Search console skill

Pull Google Search Console data and perform search performance analysis.

by OpenClaudia·MIT license·★ 705 Stars on the repo·GitHub ↗

Use now

Files of Search console

OpenClaudia/main1 file shown
SKILL.md
Show the full text404 lines

Google Search Console

Pull search performance data, index coverage, and Core Web Vitals from Google Search Console API.

Prerequisites

Requires Google OAuth credentials:

  • GOOGLE_CLIENT_ID
  • GOOGLE_CLIENT_SECRET
  • A valid OAuth access token with https://www.googleapis.com/auth/webmasters.readonly scope

Set credentials in .env, .env.local, or ~/.claude/.env.global.

Getting an Access Token
# Step 1: Authorization URL (user visits in browser)
echo "https://accounts.google.com/o/oauth2/v2/auth?client_id=${GOOGLE_CLIENT_ID}&redirect_uri=urn:ietf:wg:oauth:2.0:oob&scope=https://www.googleapis.com/auth/webmasters.readonly&response_type=code&access_type=offline"

# Step 2: Exchange code for tokens
curl -s -X POST "https://oauth2.googleapis.com/token" \
  -d "code={AUTH_CODE}" \
  -d "client_id=${GOOGLE_CLIENT_ID}" \
  -d "client_secret=${GOOGLE_CLIENT_SECRET}" \
  -d "redirect_uri=urn:ietf:wg:oauth:2.0:oob" \
  -d "grant_type=authorization_code"

# Step 3: Refresh expired token
curl -s -X POST "https://oauth2.googleapis.com/token" \
  -d "refresh_token={REFRESH_TOKEN}" \
  -d "client_id=${GOOGLE_CLIENT_ID}" \
  -d "client_secret=${GOOGLE_CLIENT_SECRET}" \
  -d "grant_type=refresh_token"
Listing Available Sites
curl -s -H "Authorization: Bearer ${GSC_ACCESS_TOKEN}" \
  "https://www.googleapis.com/webmasters/v3/sites" \
  | python3 -c "
import json, sys
data = json.load(sys.stdin)
for site in data.get('siteEntry', []):
    print(f\"{site['siteUrl']}  |  Permission: {site['permissionLevel']}\")
"

The site URL format is either https://example.com/ (URL prefix) or sc-domain:example.com (domain property).


1. Search Performance Report

The core report: queries, pages, clicks, impressions, CTR, and average position.

API Endpoint
POST https://www.googleapis.com/webmasters/v3/sites/{siteUrl}/searchAnalytics/query

Note: The {siteUrl} must be URL-encoded (e.g., https%3A%2F%2Fexample.com%2F or sc-domain%3Aexample.com).

Top Queries
curl -s -X POST \
  "https://www.googleapis.com/webmasters/v3/sites/sc-domain%3Aexample.com/searchAnalytics/query" \
  -H "Authorization: Bearer ${GSC_ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "startDate": "2024-01-01",
    "endDate": "2024-03-31",
    "dimensions": ["query"],
    "rowLimit": 50,
    "startRow": 0
  }'
Top Pages
curl -s -X POST \
  "https://www.googleapis.com/webmasters/v3/sites/sc-domain%3Aexample.com/searchAnalytics/query" \
  -H "Authorization: Bearer ${GSC_ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "startDate": "2024-01-01",
    "endDate": "2024-03-31",
    "dimensions": ["page"],
    "rowLimit": 50
  }'
Query + Page Combination
curl -s -X POST \
  "https://www.googleapis.com/webmasters/v3/sites/sc-domain%3Aexample.com/searchAnalytics/query" \
  -H "Authorization: Bearer ${GSC_ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "startDate": "2024-01-01",
    "endDate": "2024-03-31",
    "dimensions": ["query", "page"],
    "rowLimit": 100,
    "dimensionFilterGroups": [{
      "filters": [{
        "dimension": "page",
        "operator": "contains",
        "expression": "/blog/"
      }]
    }]
  }'
Available Dimensions
Dimension Description
query Search query
page URL
country Country code (ISO 3166-1 alpha-3)
device DESKTOP, MOBILE, TABLET
date Individual date
searchAppearance Rich result type
Response Parsing
curl -s -X POST "..." | python3 -c "
import json, sys
data = json.load(sys.stdin)
print(f\"{'Query':<50} {'Clicks':>8} {'Impr':>8} {'CTR':>8} {'Pos':>6}\")
print('-' * 82)
for row in data.get('rows', []):
    keys = ' + '.join(row.get('keys', []))
    print(f\"{keys:<50} {row['clicks']:>8} {row['impressions']:>8} {row['ctr']*100:>7.1f}% {row['position']:>6.1f}\")
"

2. Search Performance by Date

Track daily trends for queries and pages.

curl -s -X POST \
  "https://www.googleapis.com/webmasters/v3/sites/sc-domain%3Aexample.com/searchAnalytics/query" \
  -H "Authorization: Bearer ${GSC_ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "startDate": "2024-01-01",
    "endDate": "2024-03-31",
    "dimensions": ["date"],
    "rowLimit": 1000
  }'

To track a specific query over time:

curl -s -X POST \
  "https://www.googleapis.com/webmasters/v3/sites/sc-domain%3Aexample.com/searchAnalytics/query" \
  -H "Authorization: Bearer ${GSC_ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "startDate": "2024-01-01",
    "endDate": "2024-03-31",
    "dimensions": ["date"],
    "dimensionFilterGroups": [{
      "filters": [{
        "dimension": "query",
        "operator": "equals",
        "expression": "your target keyword"
      }]
    }]
  }'

3. Index Coverage (URL Inspection API)

Check if a specific URL is indexed.

Endpoint
POST https://searchconsole.googleapis.com/v1/urlInspection/index:inspect
Example curl
curl -s -X POST \
  "https://searchconsole.googleapis.com/v1/urlInspection/index:inspect" \
  -H "Authorization: Bearer ${GSC_ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "inspectionUrl": "https://example.com/page-to-check",
    "siteUrl": "sc-domain:example.com"
  }'
Response Fields
Field Description
inspectionResult.indexStatusResult.coverageState Submitted and indexed, Crawled - currently not indexed, etc.
inspectionResult.indexStatusResult.robotsTxtState ALLOWED or DISALLOWED
inspectionResult.indexStatusResult.indexingState INDEXING_ALLOWED or INDEXING_NOT_ALLOWED
inspectionResult.indexStatusResult.lastCrawlTime When Googlebot last crawled
inspectionResult.indexStatusResult.crawledAs DESKTOP or MOBILE
inspectionResult.mobileUsabilityResult.verdict PASS, FAIL, or VERDICT_UNSPECIFIED

4. Sitemaps

List and check sitemap status.

List Sitemaps
curl -s -H "Authorization: Bearer ${GSC_ACCESS_TOKEN}" \
  "https://www.googleapis.com/webmasters/v3/sites/sc-domain%3Aexample.com/sitemaps" \
  | python3 -c "
import json, sys
data = json.load(sys.stdin)
for sm in data.get('sitemap', []):
    print(f\"URL: {sm['path']}\")
    print(f\"  Type: {sm.get('type','')}  |  Submitted: {sm.get('lastSubmitted','')}\")
    print(f\"  URLs discovered: {sm.get('contents',[{}])[0].get('submitted','?')}  |  Indexed: {sm.get('contents',[{}])[0].get('indexed','?')}\")
    print()
"
Submit a Sitemap
curl -s -X PUT -H "Authorization: Bearer ${GSC_ACCESS_TOKEN}" \
  "https://www.googleapis.com/webmasters/v3/sites/sc-domain%3Aexample.com/sitemaps/https%3A%2F%2Fexample.com%2Fsitemap.xml"

5. Opportunity Identification

Use Search Console data to find SEO opportunities.

Low-Hanging Fruit: High Impressions, Low CTR

Queries with many impressions but low CTR suggest the title/description needs optimization.

# Pull queries, then filter for: impressions > 100 AND ctr < 0.03 AND position < 20
curl -s -X POST \
  "https://www.googleapis.com/webmasters/v3/sites/sc-domain%3Aexample.com/searchAnalytics/query" \
  -H "Authorization: Bearer ${GSC_ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "startDate": "2024-01-01",
    "endDate": "2024-03-31",
    "dimensions": ["query", "page"],
    "rowLimit": 1000
  }' | python3 -c "
import json, sys
data = json.load(sys.stdin)
print('== Low CTR Opportunities (High impressions, low CTR, good position) ==')
print(f\"{'Query':<40} {'Page':<40} {'Impr':>6} {'CTR':>7} {'Pos':>5}\")
for row in data.get('rows', []):
    if row['impressions'] > 100 and row['ctr'] < 0.03 and row['position'] < 20:
        print(f\"{row['keys'][0]:<40} {row['keys'][1][-40:]:<40} {row['impressions']:>6} {row['ctr']*100:>6.1f}% {row['position']:>5.1f}\")
"
Striking Distance: Position 5-20

Queries ranking on page 1-2 that could be pushed to top 5 with content optimization.

# Filter for position between 5 and 20 with decent impressions
curl -s -X POST "..." | python3 -c "
import json, sys
data = json.load(sys.stdin)
print('== Striking Distance Keywords (Position 5-20) ==')
opps = [r for r in data.get('rows',[]) if 5 <= r['position'] <= 20 and r['impressions'] > 50]
opps.sort(key=lambda x: x['impressions'], reverse=True)
for row in opps[:30]:
    print(f\"{row['keys'][0]:<50} Pos: {row['position']:>5.1f}  Impr: {row['impressions']:>6}  Clicks: {row['clicks']:>4}\")
"
Cannibalization Detection

Find queries where multiple pages compete for the same keyword.

# Pull query+page data, then group by query to find duplicates
curl -s -X POST "..." | python3 -c "
import json, sys
from collections import defaultdict
data = json.load(sys.stdin)
query_pages = defaultdict(list)
for row in data.get('rows', []):
    query_pages[row['keys'][0]].append({
        'page': row['keys'][1],
        'clicks': row['clicks'],
        'impressions': row['impressions'],
        'position': row['position']
    })
print('== Keyword Cannibalization (multiple pages for same query) ==')
for query, pages in sorted(query_pages.items(), key=lambda x: -sum(p['impressions'] for p in x[1])):
    if len(pages) > 1:
        total_impr = sum(p['impressions'] for p in pages)
        if total_impr > 100:
            print(f\"\nQuery: {query} ({total_impr} total impressions)\")
            for p in sorted(pages, key=lambda x: -x['impressions']):
                print(f\"  {p['page'][-60:]}  Pos: {p['position']:.1f}  Impr: {p['impressions']}  Clicks: {p['clicks']}\")
"

Workflow: Full Search Performance Audit

When asked for a complete GSC audit:

  1. Overall Metrics: Total clicks, impressions, avg CTR, avg position for last 90 days vs previous 90 days
  2. Top 30 Queries: By clicks, with CTR and position
  3. Top 20 Pages: By clicks, with CTR and position
  4. Device Breakdown: Desktop vs mobile performance
  5. Low-Hanging Fruit: High impressions + low CTR opportunities
  6. Striking Distance: Position 5-20 keywords with optimization potential
  7. Cannibalization: Queries with multiple competing pages
  8. Index Coverage: Spot-check important URLs
  9. Sitemap Health: Verify sitemaps are submitted and indexed
Report Format
## Search Console Audit: {domain}
### Period: {date range}

### Summary
| Metric | Current | Previous | Change |
|--------|---------|----------|--------|
| Clicks | X | Y | +Z% |
| Impressions | X | Y | +Z% |
| Avg CTR | X% | Y% | +Z pp |
| Avg Position | X | Y | +Z |

### Top Queries
| Query | Clicks | Impressions | CTR | Position |
|-------|--------|-------------|-----|----------|
| ...   | ...    | ...         | ... | ...      |

### Optimization Opportunities

#### Title/Description Optimization (High Impressions, Low CTR)
1. "{query}" - {impressions} impressions, {ctr}% CTR, position {pos}
   - Page: {url}
   - Recommendation: ...

#### Content Optimization (Striking Distance)
1. "{query}" - position {pos}, {impressions} impressions
   - Action: Add {query} to H2, expand section on {topic}

#### Cannibalization Fixes
1. "{query}" appears on {n} pages
   - Consolidate to: {best_url}
   - Redirect/noindex: {other_urls}

Rate Limits

  • Search Analytics API: 1,200 queries per minute
  • URL Inspection API: 2,000 inspections per day per property
  • Data freshness: Search data is typically 2-3 days behind

Common Errors

Error Cause Fix
403 No access to this property Verify ownership in GSC
400 Invalid date range Dates must be within last 16 months
Empty rows No data matching filters Broaden date range or remove filters
1---
2name: search-console
3description: >
4 Pull Google Search Console data and perform search performance analysis.
5 Use when asked about search rankings, clicks, impressions, CTR, index coverage,
6 Core Web Vitals, or sitemap status. Trigger phrases: "search console", "GSC",
7 "search performance", "clicks and impressions", "CTR analysis", "index coverage",
8 "core web vitals", "URL inspection", "sitemap status", "ranking data",
9 "search queries", "keyword positions".
10---
11 
12# Google Search Console
13 
14Pull search performance data, index coverage, and Core Web Vitals from Google Search Console API.
15 
16## Prerequisites
17 
18Requires Google OAuth credentials:
19- `GOOGLE_CLIENT_ID`
20- `GOOGLE_CLIENT_SECRET`
21- A valid OAuth access token with `https://www.googleapis.com/auth/webmasters.readonly` scope
22 
23Set credentials in `.env`, `.env.local`, or `~/.claude/.env.global`.
24 
25### Getting an Access Token
26 
27```bash
28# Step 1: Authorization URL (user visits in browser)
29echo "https://accounts.google.com/o/oauth2/v2/auth?client_id=${GOOGLE_CLIENT_ID}&redirect_uri=urn:ietf:wg:oauth:2.0:oob&scope=https://www.googleapis.com/auth/webmasters.readonly&response_type=code&access_type=offline"
30 
31# Step 2: Exchange code for tokens
32curl -s -X POST "https://oauth2.googleapis.com/token" \
33 -d "code={AUTH_CODE}" \
34 -d "client_id=${GOOGLE_CLIENT_ID}" \
35 -d "client_secret=${GOOGLE_CLIENT_SECRET}" \
36 -d "redirect_uri=urn:ietf:wg:oauth:2.0:oob" \
37 -d "grant_type=authorization_code"
38 
39# Step 3: Refresh expired token
40curl -s -X POST "https://oauth2.googleapis.com/token" \
41 -d "refresh_token={REFRESH_TOKEN}" \
42 -d "client_id=${GOOGLE_CLIENT_ID}" \
43 -d "client_secret=${GOOGLE_CLIENT_SECRET}" \
44 -d "grant_type=refresh_token"
45```
46 
47### Listing Available Sites
48 
49```bash
50curl -s -H "Authorization: Bearer ${GSC_ACCESS_TOKEN}" \
51 "https://www.googleapis.com/webmasters/v3/sites" \
52 | python3 -c "
53import json, sys
54data = json.load(sys.stdin)
55for site in data.get('siteEntry', []):
56 print(f\"{site['siteUrl']} | Permission: {site['permissionLevel']}\")
57"
58```
59 
60The site URL format is either `https://example.com/` (URL prefix) or `sc-domain:example.com` (domain property).
61 
62---
63 
64## 1. Search Performance Report
65 
66The core report: queries, pages, clicks, impressions, CTR, and average position.
67 
68### API Endpoint
69 
70```
71POST https://www.googleapis.com/webmasters/v3/sites/{siteUrl}/searchAnalytics/query
72```
73 
74Note: The `{siteUrl}` must be URL-encoded (e.g., `https%3A%2F%2Fexample.com%2F` or `sc-domain%3Aexample.com`).
75 
76### Top Queries
77 
78```bash
79curl -s -X POST \
80 "https://www.googleapis.com/webmasters/v3/sites/sc-domain%3Aexample.com/searchAnalytics/query" \
81 -H "Authorization: Bearer ${GSC_ACCESS_TOKEN}" \
82 -H "Content-Type: application/json" \
83 -d '{
84 "startDate": "2024-01-01",
85 "endDate": "2024-03-31",
86 "dimensions": ["query"],
87 "rowLimit": 50,
88 "startRow": 0
89 }'
90```
91 
92### Top Pages
93 
94```bash
95curl -s -X POST \
96 "https://www.googleapis.com/webmasters/v3/sites/sc-domain%3Aexample.com/searchAnalytics/query" \
97 -H "Authorization: Bearer ${GSC_ACCESS_TOKEN}" \
98 -H "Content-Type: application/json" \
99 -d '{
100 "startDate": "2024-01-01",
101 "endDate": "2024-03-31",
102 "dimensions": ["page"],
103 "rowLimit": 50
104 }'
105```
106 
107### Query + Page Combination
108 
109```bash
110curl -s -X POST \
111 "https://www.googleapis.com/webmasters/v3/sites/sc-domain%3Aexample.com/searchAnalytics/query" \
112 -H "Authorization: Bearer ${GSC_ACCESS_TOKEN}" \
113 -H "Content-Type: application/json" \
114 -d '{
115 "startDate": "2024-01-01",
116 "endDate": "2024-03-31",
117 "dimensions": ["query", "page"],
118 "rowLimit": 100,
119 "dimensionFilterGroups": [{
120 "filters": [{
121 "dimension": "page",
122 "operator": "contains",
123 "expression": "/blog/"
124 }]
125 }]
126 }'
127```
128 
129### Available Dimensions
130 
131| Dimension | Description |
132|-----------|-------------|
133| `query` | Search query |
134| `page` | URL |
135| `country` | Country code (ISO 3166-1 alpha-3) |
136| `device` | `DESKTOP`, `MOBILE`, `TABLET` |
137| `date` | Individual date |
138| `searchAppearance` | Rich result type |
139 
140### Response Parsing
141 
142```bash
143curl -s -X POST "..." | python3 -c "
144import json, sys
145data = json.load(sys.stdin)
146print(f\"{'Query':<50} {'Clicks':>8} {'Impr':>8} {'CTR':>8} {'Pos':>6}\")
147print('-' * 82)
148for row in data.get('rows', []):
149 keys = ' + '.join(row.get('keys', []))
150 print(f\"{keys:<50} {row['clicks']:>8} {row['impressions']:>8} {row['ctr']*100:>7.1f}% {row['position']:>6.1f}\")
151"
152```
153 
154---
155 
156## 2. Search Performance by Date
157 
158Track daily trends for queries and pages.
159 
160```bash
161curl -s -X POST \
162 "https://www.googleapis.com/webmasters/v3/sites/sc-domain%3Aexample.com/searchAnalytics/query" \
163 -H "Authorization: Bearer ${GSC_ACCESS_TOKEN}" \
164 -H "Content-Type: application/json" \
165 -d '{
166 "startDate": "2024-01-01",
167 "endDate": "2024-03-31",
168 "dimensions": ["date"],
169 "rowLimit": 1000
170 }'
171```
172 
173To track a specific query over time:
174 
175```bash
176curl -s -X POST \
177 "https://www.googleapis.com/webmasters/v3/sites/sc-domain%3Aexample.com/searchAnalytics/query" \
178 -H "Authorization: Bearer ${GSC_ACCESS_TOKEN}" \
179 -H "Content-Type: application/json" \
180 -d '{
181 "startDate": "2024-01-01",
182 "endDate": "2024-03-31",
183 "dimensions": ["date"],
184 "dimensionFilterGroups": [{
185 "filters": [{
186 "dimension": "query",
187 "operator": "equals",
188 "expression": "your target keyword"
189 }]
190 }]
191 }'
192```
193 
194---
195 
196## 3. Index Coverage (URL Inspection API)
197 
198Check if a specific URL is indexed.
199 
200### Endpoint
201 
202```
203POST https://searchconsole.googleapis.com/v1/urlInspection/index:inspect
204```
205 
206### Example curl
207 
208```bash
209curl -s -X POST \
210 "https://searchconsole.googleapis.com/v1/urlInspection/index:inspect" \
211 -H "Authorization: Bearer ${GSC_ACCESS_TOKEN}" \
212 -H "Content-Type: application/json" \
213 -d '{
214 "inspectionUrl": "https://example.com/page-to-check",
215 "siteUrl": "sc-domain:example.com"
216 }'
217```
218 
219### Response Fields
220 
221| Field | Description |
222|-------|-------------|
223| `inspectionResult.indexStatusResult.coverageState` | `Submitted and indexed`, `Crawled - currently not indexed`, etc. |
224| `inspectionResult.indexStatusResult.robotsTxtState` | `ALLOWED` or `DISALLOWED` |
225| `inspectionResult.indexStatusResult.indexingState` | `INDEXING_ALLOWED` or `INDEXING_NOT_ALLOWED` |
226| `inspectionResult.indexStatusResult.lastCrawlTime` | When Googlebot last crawled |
227| `inspectionResult.indexStatusResult.crawledAs` | `DESKTOP` or `MOBILE` |
228| `inspectionResult.mobileUsabilityResult.verdict` | `PASS`, `FAIL`, or `VERDICT_UNSPECIFIED` |
229 
230---
231 
232## 4. Sitemaps
233 
234List and check sitemap status.
235 
236### List Sitemaps
237 
238```bash
239curl -s -H "Authorization: Bearer ${GSC_ACCESS_TOKEN}" \
240 "https://www.googleapis.com/webmasters/v3/sites/sc-domain%3Aexample.com/sitemaps" \
241 | python3 -c "
242import json, sys
243data = json.load(sys.stdin)
244for sm in data.get('sitemap', []):
245 print(f\"URL: {sm['path']}\")
246 print(f\" Type: {sm.get('type','')} | Submitted: {sm.get('lastSubmitted','')}\")
247 print(f\" URLs discovered: {sm.get('contents',[{}])[0].get('submitted','?')} | Indexed: {sm.get('contents',[{}])[0].get('indexed','?')}\")
248 print()
249"
250```
251 
252### Submit a Sitemap
253 
254```bash
255curl -s -X PUT -H "Authorization: Bearer ${GSC_ACCESS_TOKEN}" \
256 "https://www.googleapis.com/webmasters/v3/sites/sc-domain%3Aexample.com/sitemaps/https%3A%2F%2Fexample.com%2Fsitemap.xml"
257```
258 
259---
260 
261## 5. Opportunity Identification
262 
263Use Search Console data to find SEO opportunities.
264 
265### Low-Hanging Fruit: High Impressions, Low CTR
266 
267Queries with many impressions but low CTR suggest the title/description needs optimization.
268 
269```bash
270# Pull queries, then filter for: impressions > 100 AND ctr < 0.03 AND position < 20
271curl -s -X POST \
272 "https://www.googleapis.com/webmasters/v3/sites/sc-domain%3Aexample.com/searchAnalytics/query" \
273 -H "Authorization: Bearer ${GSC_ACCESS_TOKEN}" \
274 -H "Content-Type: application/json" \
275 -d '{
276 "startDate": "2024-01-01",
277 "endDate": "2024-03-31",
278 "dimensions": ["query", "page"],
279 "rowLimit": 1000
280 }' | python3 -c "
281import json, sys
282data = json.load(sys.stdin)
283print('== Low CTR Opportunities (High impressions, low CTR, good position) ==')
284print(f\"{'Query':<40} {'Page':<40} {'Impr':>6} {'CTR':>7} {'Pos':>5}\")
285for row in data.get('rows', []):
286 if row['impressions'] > 100 and row['ctr'] < 0.03 and row['position'] < 20:
287 print(f\"{row['keys'][0]:<40} {row['keys'][1][-40:]:<40} {row['impressions']:>6} {row['ctr']*100:>6.1f}% {row['position']:>5.1f}\")
288"
289```
290 
291### Striking Distance: Position 5-20
292 
293Queries ranking on page 1-2 that could be pushed to top 5 with content optimization.
294 
295```bash
296# Filter for position between 5 and 20 with decent impressions
297curl -s -X POST "..." | python3 -c "
298import json, sys
299data = json.load(sys.stdin)
300print('== Striking Distance Keywords (Position 5-20) ==')
301opps = [r for r in data.get('rows',[]) if 5 <= r['position'] <= 20 and r['impressions'] > 50]
302opps.sort(key=lambda x: x['impressions'], reverse=True)
303for row in opps[:30]:
304 print(f\"{row['keys'][0]:<50} Pos: {row['position']:>5.1f} Impr: {row['impressions']:>6} Clicks: {row['clicks']:>4}\")
305"
306```
307 
308### Cannibalization Detection
309 
310Find queries where multiple pages compete for the same keyword.
311 
312```bash
313# Pull query+page data, then group by query to find duplicates
314curl -s -X POST "..." | python3 -c "
315import json, sys
316from collections import defaultdict
317data = json.load(sys.stdin)
318query_pages = defaultdict(list)
319for row in data.get('rows', []):
320 query_pages[row['keys'][0]].append({
321 'page': row['keys'][1],
322 'clicks': row['clicks'],
323 'impressions': row['impressions'],
324 'position': row['position']
325 })
326print('== Keyword Cannibalization (multiple pages for same query) ==')
327for query, pages in sorted(query_pages.items(), key=lambda x: -sum(p['impressions'] for p in x[1])):
328 if len(pages) > 1:
329 total_impr = sum(p['impressions'] for p in pages)
330 if total_impr > 100:
331 print(f\"\nQuery: {query} ({total_impr} total impressions)\")
332 for p in sorted(pages, key=lambda x: -x['impressions']):
333 print(f\" {p['page'][-60:]} Pos: {p['position']:.1f} Impr: {p['impressions']} Clicks: {p['clicks']}\")
334"
335```
336 
337---
338 
339## Workflow: Full Search Performance Audit
340 
341When asked for a complete GSC audit:
342 
3431. **Overall Metrics**: Total clicks, impressions, avg CTR, avg position for last 90 days vs previous 90 days
3442. **Top 30 Queries**: By clicks, with CTR and position
3453. **Top 20 Pages**: By clicks, with CTR and position
3464. **Device Breakdown**: Desktop vs mobile performance
3475. **Low-Hanging Fruit**: High impressions + low CTR opportunities
3486. **Striking Distance**: Position 5-20 keywords with optimization potential
3497. **Cannibalization**: Queries with multiple competing pages
3508. **Index Coverage**: Spot-check important URLs
3519. **Sitemap Health**: Verify sitemaps are submitted and indexed
352 
353### Report Format
354 
355```
356## Search Console Audit: {domain}
357### Period: {date range}
358 
359### Summary
360| Metric | Current | Previous | Change |
361|--------|---------|----------|--------|
362| Clicks | X | Y | +Z% |
363| Impressions | X | Y | +Z% |
364| Avg CTR | X% | Y% | +Z pp |
365| Avg Position | X | Y | +Z |
366 
367### Top Queries
368| Query | Clicks | Impressions | CTR | Position |
369|-------|--------|-------------|-----|----------|
370| ... | ... | ... | ... | ... |
371 
372### Optimization Opportunities
373 
374#### Title/Description Optimization (High Impressions, Low CTR)
3751. "{query}" - {impressions} impressions, {ctr}% CTR, position {pos}
376 - Page: {url}
377 - Recommendation: ...
378 
379#### Content Optimization (Striking Distance)
3801. "{query}" - position {pos}, {impressions} impressions
381 - Action: Add {query} to H2, expand section on {topic}
382 
383#### Cannibalization Fixes
3841. "{query}" appears on {n} pages
385 - Consolidate to: {best_url}
386 - Redirect/noindex: {other_urls}
387```
388 
389---
390 
391## Rate Limits
392 
393- Search Analytics API: 1,200 queries per minute
394- URL Inspection API: 2,000 inspections per day per property
395- Data freshness: Search data is typically 2-3 days behind
396 
397## Common Errors
398 
399| Error | Cause | Fix |
400|-------|-------|-----|
401| 403 | No access to this property | Verify ownership in GSC |
402| 400 | Invalid date range | Dates must be within last 16 months |
403| Empty rows | No data matching filters | Broaden date range or remove filters |
404 

Discussion

Alternatives

Agentic Browsing ReadinessAudit and fix agent readiness: the Lighthouse Agentic Browsing fraction, accessibility tree for agents, robots.txt and Content-Signal for AI agents, WAF treatment of agent traffic, llms.txt, Markdown delivery, ai-catalog.json, /.well-known discovery files, and WebMCP tools. Exclude AI citability and brand signals (seo-geo) and commerce protocol depth (seo-ecommerce).Marketing · MITBacklink Profile AnalysisBacklink profile analysis: referring domains, anchor text distribution, toxic link detection, competitor gap analysis. Works with free APIs (Moz, Bing Webmaster, Common Crawl) and DataForSEO extension. Use when user says backlinks, link profile, referring domains, anchor text, toxic links, link gap, link building, disavow, or backlink audit.Marketing · MIT/setup-cmsConnect a CMS to notfair SEO tools. Guides users through configuring WordPress, Strapi, Contentful, or Ghost — tests the connection, and writes credentials to .env.local. Once set up, seo-analysis automatically cross- references CMS content against Google Search Console data. Use whenever the user says "connect my CMS", "set up WordPress", "configure Strapi", "add Contentful", "connect Ghost", or "CMS setup". Also trigger if the user asks why no CMS data appears in a seo-analysis report. · MITBacklink checkBacklink profile for any domain — referring domains, authority, anchors, new/lost links, and a side-by-side vs a competitor. Use when asked "check my backlinks", "backlink profile of X", "who links to them", or "link gap vs competitor".Marketing · MIT