@olegkoval/agent-skills 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +3 -2
- package/.cursor-plugin/index.json +5 -0
- package/README.md +3 -2
- package/catalog/skills.json +21 -0
- package/collections/marketing.json +2 -2
- package/package.json +1 -1
- package/packages/marketing/search-console-indexing-audit/SKILL.md +64 -0
- package/packages/marketing/search-console-indexing-audit/adapters/claude/plugin.json +5 -0
- package/packages/marketing/search-console-indexing-audit/adapters/claude/skills/search-console-indexing-audit/SKILL.md +66 -0
- package/packages/marketing/search-console-indexing-audit/adapters/claude/skills/search-console-indexing-audit/scripts/summarize_gsc_coverage.py +151 -0
- package/packages/marketing/search-console-indexing-audit/adapters/codex/README.md +3 -0
- package/packages/marketing/search-console-indexing-audit/adapters/cursor/plugin.json +6 -0
- package/packages/marketing/search-console-indexing-audit/adapters/cursor/skills/search-console-indexing-audit/SKILL.md +66 -0
- package/packages/marketing/search-console-indexing-audit/adapters/cursor/skills/search-console-indexing-audit/scripts/summarize_gsc_coverage.py +151 -0
- package/packages/marketing/search-console-indexing-audit/scripts/summarize_gsc_coverage.py +151 -0
- package/scripts/build-adapters.sh +13 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "olko-agent-skills",
|
|
3
3
|
"description": "Agent-agnostic skill catalog for Codex, Claude, Cursor, and other skill-aware tools.",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.1",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Oleg Koval"
|
|
7
7
|
},
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"./packages/software-development/promptctl",
|
|
19
19
|
"./packages/music/fill-music-player",
|
|
20
20
|
"./packages/software-development/product-builder",
|
|
21
|
-
"./packages/marketing/viral-launch"
|
|
21
|
+
"./packages/marketing/viral-launch",
|
|
22
|
+
"./packages/marketing/search-console-indexing-audit"
|
|
22
23
|
]
|
|
23
24
|
}
|
|
@@ -50,6 +50,11 @@
|
|
|
50
50
|
"name": "olko:viral-launch",
|
|
51
51
|
"source": "./packages/marketing/viral-launch/adapters/cursor",
|
|
52
52
|
"description": "Set up a project repository and launch plan for shareable marketing, public launch readiness, and growth loops."
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"name": "olko:search-console-indexing-audit",
|
|
56
|
+
"source": "./packages/marketing/search-console-indexing-audit/adapters/cursor",
|
|
57
|
+
"description": "Analyze Google Search Console Coverage CSV exports and correlate them with sitemap, robots, canonical, redirect, and noindex signals."
|
|
53
58
|
}
|
|
54
59
|
]
|
|
55
60
|
}
|
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<p><strong>Agent-agnostic skill catalog for Codex, Claude, Cursor, and other skill-aware tools.</strong></p>
|
|
4
4
|
<p>
|
|
5
5
|
<img src="https://img.shields.io/badge/license-MIT-16a34a" alt="MIT license">
|
|
6
|
-
<img src="https://img.shields.io/badge/skills-
|
|
6
|
+
<img src="https://img.shields.io/badge/skills-11-2563eb" alt="11 skills">
|
|
7
7
|
<img src="https://img.shields.io/badge/platforms-Codex%20%7C%20Claude%20%7C%20Cursor%20%7C%20Copilot-111827" alt="Codex Claude Cursor Copilot">
|
|
8
8
|
<img src="https://img.shields.io/badge/status-public%20catalog-16a34a" alt="Public catalog">
|
|
9
9
|
</p>
|
|
@@ -116,7 +116,7 @@ packages/{category}/{skill}/adapters/
|
|
|
116
116
|
|
|
117
117
|
</details>
|
|
118
118
|
|
|
119
|
-
## All
|
|
119
|
+
## All 11 Skills
|
|
120
120
|
|
|
121
121
|
These packages are the entry points. Each one is a structured workflow with concrete trigger conditions and execution steps. You can reference any skill directly by its `olko:*` lookup name.
|
|
122
122
|
|
|
@@ -143,6 +143,7 @@ These packages are the entry points. Each one is a structured workflow with conc
|
|
|
143
143
|
| Skill | What It Does | Use When |
|
|
144
144
|
|-------|-------------|----------|
|
|
145
145
|
| [viral-launch](packages/marketing/viral-launch/SKILL.md) | Sets up a project repository and launch plan for shareable marketing, public launch readiness, and growth loops | Preparing a repo, product, open-source package, waitlist, or creator tool for public launch |
|
|
146
|
+
| [search-console-indexing-audit](packages/marketing/search-console-indexing-audit/SKILL.md) | Audits Google Search Console Coverage exports against sitemap, robots, canonical, redirect, and noindex signals | Diagnosing GSC indexing issues such as redirects, canonical alternates, and discovered but not indexed pages |
|
|
146
147
|
|
|
147
148
|
### Photography
|
|
148
149
|
|
package/catalog/skills.json
CHANGED
|
@@ -197,6 +197,27 @@
|
|
|
197
197
|
"cursor",
|
|
198
198
|
"copilot"
|
|
199
199
|
]
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
"name": "search-console-indexing-audit",
|
|
203
|
+
"lookupName": "olko:search-console-indexing-audit",
|
|
204
|
+
"category": "marketing",
|
|
205
|
+
"path": "packages/marketing/search-console-indexing-audit",
|
|
206
|
+
"description": "Analyze Google Search Console Coverage CSV exports and correlate them with sitemap, robots, canonical, redirect, and noindex signals.",
|
|
207
|
+
"tags": [
|
|
208
|
+
"seo",
|
|
209
|
+
"google-search-console",
|
|
210
|
+
"indexing",
|
|
211
|
+
"sitemap",
|
|
212
|
+
"canonical",
|
|
213
|
+
"robots"
|
|
214
|
+
],
|
|
215
|
+
"adapters": [
|
|
216
|
+
"codex",
|
|
217
|
+
"claude",
|
|
218
|
+
"cursor",
|
|
219
|
+
"copilot"
|
|
220
|
+
]
|
|
200
221
|
}
|
|
201
222
|
]
|
|
202
223
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "marketing",
|
|
3
|
-
"description": "Skills for project marketing, public launches, positioning, and shareable growth workflows.",
|
|
4
|
-
"packages": ["viral-launch"]
|
|
3
|
+
"description": "Skills for project marketing, public launches, positioning, SEO, indexing, and shareable growth workflows.",
|
|
4
|
+
"packages": ["viral-launch", "search-console-indexing-audit"]
|
|
5
5
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
---
|
|
2
|
+
|
|
3
|
+
name: search-console-indexing-audit
|
|
4
|
+
description: Analyze Google Search Console Coverage or indexing CSV exports and correlate them with repo and live-site SEO signals. Use for Chart.csv, Metadata.csv, Critical issues.csv, Non-critical issues.csv, sitemap/robots/canonical indexing problems, "Discovered - currently not indexed", redirect indexing buckets, or requests to fix Google indexing coverage.
|
|
5
|
+
metadata:
|
|
6
|
+
author: Oleg Koval
|
|
7
|
+
tags:
|
|
8
|
+
- seo
|
|
9
|
+
- google-search-console
|
|
10
|
+
- indexing
|
|
11
|
+
- sitemap
|
|
12
|
+
- canonical
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# search-console-indexing-audit
|
|
17
|
+
|
|
18
|
+
Use this skill to turn Google Search Console Coverage exports into a concrete indexing fix plan.
|
|
19
|
+
|
|
20
|
+
## Workflow
|
|
21
|
+
|
|
22
|
+
1. Read the export directory first:
|
|
23
|
+
- `Chart.csv` for indexed/not-indexed trend and impressions
|
|
24
|
+
- `Metadata.csv` for property context such as sitemap scope
|
|
25
|
+
- `Critical issues.csv` and `Non-critical issues.csv` for issue buckets
|
|
26
|
+
2. Run the summarizer when the standard CSV files are present:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
python3 <skill-dir>/scripts/summarize_gsc_coverage.py <export-dir>
|
|
30
|
+
python3 <skill-dir>/scripts/summarize_gsc_coverage.py <export-dir> --json
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
3. Inspect the target repo for SEO sources of truth:
|
|
34
|
+
- sitemap generation and whether listed URLs are canonical, indexable HTML pages
|
|
35
|
+
- robots rules, sitemap URL, and host hints
|
|
36
|
+
- canonical metadata, `metadataBase`, Open Graph URL base, and per-route `noindex`
|
|
37
|
+
- redirects across apex/www, http/https, trailing slash, moved routes, and legacy paths
|
|
38
|
+
4. Check live URLs when network access is available:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
curl -sS -I https://example.com/sitemap.xml
|
|
42
|
+
curl -sS https://example.com/sitemap.xml
|
|
43
|
+
curl -sS https://example.com/robots.txt
|
|
44
|
+
curl -sS https://example.com/page | rg -o '<link rel="canonical"[^>]*>|<meta name="robots"[^>]*>'
|
|
45
|
+
curl -sS -I -L --max-redirs 5 https://example.com/page
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
5. Map GSC buckets to fixes:
|
|
49
|
+
- `Page with redirect`: remove redirected URLs from sitemap and canonicals; submit only final 200 URLs.
|
|
50
|
+
- `Redirect error`: inspect redirect chains, protocol/host loops, blocked destinations, and non-200 final responses.
|
|
51
|
+
- `Alternate page with proper canonical tag`: usually acceptable; verify sitemap does not include alternates.
|
|
52
|
+
- `Discovered - currently not indexed`: improve crawl signals first: clean sitemap, canonical consistency, internal links, content quality, stable 200 responses, and lastmod accuracy.
|
|
53
|
+
- `Excluded by noindex`: verify it is intentional and keep those URLs out of sitemap.
|
|
54
|
+
|
|
55
|
+
## Output
|
|
56
|
+
|
|
57
|
+
Report:
|
|
58
|
+
|
|
59
|
+
- Export summary: date range, indexed/not-indexed trend, issue buckets, and limitations.
|
|
60
|
+
- Likely root cause, with evidence from files or live HTTP responses.
|
|
61
|
+
- Prioritized fixes, smallest reliable change first.
|
|
62
|
+
- Verification steps for local tests, build, live sitemap/robots/canonical checks, and Search Console validation.
|
|
63
|
+
|
|
64
|
+
Call out when the export is aggregate-only and does not contain affected URL examples. Do not infer exact URLs unless the CSVs or live sitemap provide them.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<!-- Generated by scripts/build-adapters.sh. Do not edit directly. -->
|
|
2
|
+
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
name: search-console-indexing-audit
|
|
6
|
+
description: Analyze Google Search Console Coverage or indexing CSV exports and correlate them with repo and live-site SEO signals. Use for Chart.csv, Metadata.csv, Critical issues.csv, Non-critical issues.csv, sitemap/robots/canonical indexing problems, "Discovered - currently not indexed", redirect indexing buckets, or requests to fix Google indexing coverage.
|
|
7
|
+
metadata:
|
|
8
|
+
author: Oleg Koval
|
|
9
|
+
tags:
|
|
10
|
+
- seo
|
|
11
|
+
- google-search-console
|
|
12
|
+
- indexing
|
|
13
|
+
- sitemap
|
|
14
|
+
- canonical
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# search-console-indexing-audit
|
|
19
|
+
|
|
20
|
+
Use this skill to turn Google Search Console Coverage exports into a concrete indexing fix plan.
|
|
21
|
+
|
|
22
|
+
## Workflow
|
|
23
|
+
|
|
24
|
+
1. Read the export directory first:
|
|
25
|
+
- `Chart.csv` for indexed/not-indexed trend and impressions
|
|
26
|
+
- `Metadata.csv` for property context such as sitemap scope
|
|
27
|
+
- `Critical issues.csv` and `Non-critical issues.csv` for issue buckets
|
|
28
|
+
2. Run the summarizer when the standard CSV files are present:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
python3 <skill-dir>/scripts/summarize_gsc_coverage.py <export-dir>
|
|
32
|
+
python3 <skill-dir>/scripts/summarize_gsc_coverage.py <export-dir> --json
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
3. Inspect the target repo for SEO sources of truth:
|
|
36
|
+
- sitemap generation and whether listed URLs are canonical, indexable HTML pages
|
|
37
|
+
- robots rules, sitemap URL, and host hints
|
|
38
|
+
- canonical metadata, `metadataBase`, Open Graph URL base, and per-route `noindex`
|
|
39
|
+
- redirects across apex/www, http/https, trailing slash, moved routes, and legacy paths
|
|
40
|
+
4. Check live URLs when network access is available:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
curl -sS -I https://example.com/sitemap.xml
|
|
44
|
+
curl -sS https://example.com/sitemap.xml
|
|
45
|
+
curl -sS https://example.com/robots.txt
|
|
46
|
+
curl -sS https://example.com/page | rg -o '<link rel="canonical"[^>]*>|<meta name="robots"[^>]*>'
|
|
47
|
+
curl -sS -I -L --max-redirs 5 https://example.com/page
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
5. Map GSC buckets to fixes:
|
|
51
|
+
- `Page with redirect`: remove redirected URLs from sitemap and canonicals; submit only final 200 URLs.
|
|
52
|
+
- `Redirect error`: inspect redirect chains, protocol/host loops, blocked destinations, and non-200 final responses.
|
|
53
|
+
- `Alternate page with proper canonical tag`: usually acceptable; verify sitemap does not include alternates.
|
|
54
|
+
- `Discovered - currently not indexed`: improve crawl signals first: clean sitemap, canonical consistency, internal links, content quality, stable 200 responses, and lastmod accuracy.
|
|
55
|
+
- `Excluded by noindex`: verify it is intentional and keep those URLs out of sitemap.
|
|
56
|
+
|
|
57
|
+
## Output
|
|
58
|
+
|
|
59
|
+
Report:
|
|
60
|
+
|
|
61
|
+
- Export summary: date range, indexed/not-indexed trend, issue buckets, and limitations.
|
|
62
|
+
- Likely root cause, with evidence from files or live HTTP responses.
|
|
63
|
+
- Prioritized fixes, smallest reliable change first.
|
|
64
|
+
- Verification steps for local tests, build, live sitemap/robots/canonical checks, and Search Console validation.
|
|
65
|
+
|
|
66
|
+
Call out when the export is aggregate-only and does not contain affected URL examples. Do not infer exact URLs unless the CSVs or live sitemap provide them.
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Summarize Google Search Console Coverage CSV exports."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import csv
|
|
8
|
+
import json
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
STANDARD_FILES = {
|
|
14
|
+
"chart": "Chart.csv",
|
|
15
|
+
"metadata": "Metadata.csv",
|
|
16
|
+
"critical": "Critical issues.csv",
|
|
17
|
+
"non_critical": "Non-critical issues.csv",
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def read_rows(path: Path) -> list[dict[str, str]]:
|
|
22
|
+
if not path.exists():
|
|
23
|
+
return []
|
|
24
|
+
with path.open(newline="", encoding="utf-8-sig") as handle:
|
|
25
|
+
return [
|
|
26
|
+
{key: (value or "").strip() for key, value in row.items()}
|
|
27
|
+
for row in csv.DictReader(handle)
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def int_or_none(value: str) -> int | None:
|
|
32
|
+
if value == "":
|
|
33
|
+
return None
|
|
34
|
+
try:
|
|
35
|
+
return int(value.replace(",", ""))
|
|
36
|
+
except ValueError:
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def chart_summary(rows: list[dict[str, str]]) -> dict[str, Any]:
|
|
41
|
+
points = []
|
|
42
|
+
for row in rows:
|
|
43
|
+
points.append(
|
|
44
|
+
{
|
|
45
|
+
"date": row.get("Date", ""),
|
|
46
|
+
"not_indexed": int_or_none(row.get("Not indexed", "")),
|
|
47
|
+
"indexed": int_or_none(row.get("Indexed", "")),
|
|
48
|
+
"impressions": int_or_none(row.get("Impressions", "")),
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
indexed_points = [
|
|
53
|
+
point for point in points if point["indexed"] is not None or point["not_indexed"] is not None
|
|
54
|
+
]
|
|
55
|
+
first = indexed_points[0] if indexed_points else None
|
|
56
|
+
last = indexed_points[-1] if indexed_points else None
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
"date_range": [points[0]["date"], points[-1]["date"]] if points else None,
|
|
60
|
+
"first_indexing_point": first,
|
|
61
|
+
"latest_indexing_point": last,
|
|
62
|
+
"indexed_delta": None if not first or not last else (last["indexed"] or 0) - (first["indexed"] or 0),
|
|
63
|
+
"not_indexed_delta": None
|
|
64
|
+
if not first or not last
|
|
65
|
+
else (last["not_indexed"] or 0) - (first["not_indexed"] or 0),
|
|
66
|
+
"impressions_total": sum(point["impressions"] or 0 for point in points),
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def issue_summary(rows: list[dict[str, str]]) -> list[dict[str, Any]]:
|
|
71
|
+
issues = []
|
|
72
|
+
for row in rows:
|
|
73
|
+
reason = row.get("Reason", "")
|
|
74
|
+
if not reason:
|
|
75
|
+
continue
|
|
76
|
+
issues.append(
|
|
77
|
+
{
|
|
78
|
+
"reason": reason,
|
|
79
|
+
"source": row.get("Source", ""),
|
|
80
|
+
"validation": row.get("Validation", ""),
|
|
81
|
+
"pages": int_or_none(row.get("Pages", "")) or 0,
|
|
82
|
+
}
|
|
83
|
+
)
|
|
84
|
+
return sorted(issues, key=lambda issue: issue["pages"], reverse=True)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def load_export(export_dir: Path) -> dict[str, Any]:
|
|
88
|
+
files = {key: export_dir / filename for key, filename in STANDARD_FILES.items()}
|
|
89
|
+
metadata_rows = read_rows(files["metadata"])
|
|
90
|
+
return {
|
|
91
|
+
"export_dir": str(export_dir),
|
|
92
|
+
"metadata": {row.get("Property", ""): row.get("Value", "") for row in metadata_rows},
|
|
93
|
+
"chart": chart_summary(read_rows(files["chart"])),
|
|
94
|
+
"critical_issues": issue_summary(read_rows(files["critical"])),
|
|
95
|
+
"non_critical_issues": issue_summary(read_rows(files["non_critical"])),
|
|
96
|
+
"missing_files": [filename for filename in STANDARD_FILES.values() if not (export_dir / filename).exists()],
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def print_markdown(summary: dict[str, Any]) -> None:
|
|
101
|
+
chart = summary["chart"]
|
|
102
|
+
latest = chart["latest_indexing_point"] or {}
|
|
103
|
+
print("# Search Console Coverage Summary")
|
|
104
|
+
print()
|
|
105
|
+
print(f"- Export: `{summary['export_dir']}`")
|
|
106
|
+
if chart["date_range"]:
|
|
107
|
+
print(f"- Date range: {chart['date_range'][0]} to {chart['date_range'][1]}")
|
|
108
|
+
print(f"- Latest indexed: {latest.get('indexed', 'n/a')}")
|
|
109
|
+
print(f"- Latest not indexed: {latest.get('not_indexed', 'n/a')}")
|
|
110
|
+
print(f"- Total impressions in chart: {chart['impressions_total']}")
|
|
111
|
+
if summary["metadata"]:
|
|
112
|
+
print(f"- Metadata: {summary['metadata']}")
|
|
113
|
+
if summary["missing_files"]:
|
|
114
|
+
print(f"- Missing standard files: {', '.join(summary['missing_files'])}")
|
|
115
|
+
|
|
116
|
+
for label, issues in (
|
|
117
|
+
("Critical Issues", summary["critical_issues"]),
|
|
118
|
+
("Non-Critical Issues", summary["non_critical_issues"]),
|
|
119
|
+
):
|
|
120
|
+
print()
|
|
121
|
+
print(f"## {label}")
|
|
122
|
+
if not issues:
|
|
123
|
+
print("- None reported")
|
|
124
|
+
continue
|
|
125
|
+
for issue in issues:
|
|
126
|
+
print(
|
|
127
|
+
f"- {issue['reason']}: {issue['pages']} pages"
|
|
128
|
+
f" ({issue['source']}, validation: {issue['validation']})"
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
print()
|
|
132
|
+
print("## Notes")
|
|
133
|
+
print("- Standard Coverage exports are aggregate reports; they may not include affected URL examples.")
|
|
134
|
+
print("- Redirect and canonical buckets should be checked against sitemap URLs and live canonical tags.")
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def main() -> None:
|
|
138
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
139
|
+
parser.add_argument("export_dir", type=Path)
|
|
140
|
+
parser.add_argument("--json", action="store_true", help="Print machine-readable JSON")
|
|
141
|
+
args = parser.parse_args()
|
|
142
|
+
|
|
143
|
+
summary = load_export(args.export_dir)
|
|
144
|
+
if args.json:
|
|
145
|
+
print(json.dumps(summary, indent=2, sort_keys=True))
|
|
146
|
+
else:
|
|
147
|
+
print_markdown(summary)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
if __name__ == "__main__":
|
|
151
|
+
main()
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<!-- Generated by scripts/build-adapters.sh. Do not edit directly. -->
|
|
2
|
+
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
name: search-console-indexing-audit
|
|
6
|
+
description: Analyze Google Search Console Coverage or indexing CSV exports and correlate them with repo and live-site SEO signals. Use for Chart.csv, Metadata.csv, Critical issues.csv, Non-critical issues.csv, sitemap/robots/canonical indexing problems, "Discovered - currently not indexed", redirect indexing buckets, or requests to fix Google indexing coverage.
|
|
7
|
+
metadata:
|
|
8
|
+
author: Oleg Koval
|
|
9
|
+
tags:
|
|
10
|
+
- seo
|
|
11
|
+
- google-search-console
|
|
12
|
+
- indexing
|
|
13
|
+
- sitemap
|
|
14
|
+
- canonical
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# search-console-indexing-audit
|
|
19
|
+
|
|
20
|
+
Use this skill to turn Google Search Console Coverage exports into a concrete indexing fix plan.
|
|
21
|
+
|
|
22
|
+
## Workflow
|
|
23
|
+
|
|
24
|
+
1. Read the export directory first:
|
|
25
|
+
- `Chart.csv` for indexed/not-indexed trend and impressions
|
|
26
|
+
- `Metadata.csv` for property context such as sitemap scope
|
|
27
|
+
- `Critical issues.csv` and `Non-critical issues.csv` for issue buckets
|
|
28
|
+
2. Run the summarizer when the standard CSV files are present:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
python3 <skill-dir>/scripts/summarize_gsc_coverage.py <export-dir>
|
|
32
|
+
python3 <skill-dir>/scripts/summarize_gsc_coverage.py <export-dir> --json
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
3. Inspect the target repo for SEO sources of truth:
|
|
36
|
+
- sitemap generation and whether listed URLs are canonical, indexable HTML pages
|
|
37
|
+
- robots rules, sitemap URL, and host hints
|
|
38
|
+
- canonical metadata, `metadataBase`, Open Graph URL base, and per-route `noindex`
|
|
39
|
+
- redirects across apex/www, http/https, trailing slash, moved routes, and legacy paths
|
|
40
|
+
4. Check live URLs when network access is available:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
curl -sS -I https://example.com/sitemap.xml
|
|
44
|
+
curl -sS https://example.com/sitemap.xml
|
|
45
|
+
curl -sS https://example.com/robots.txt
|
|
46
|
+
curl -sS https://example.com/page | rg -o '<link rel="canonical"[^>]*>|<meta name="robots"[^>]*>'
|
|
47
|
+
curl -sS -I -L --max-redirs 5 https://example.com/page
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
5. Map GSC buckets to fixes:
|
|
51
|
+
- `Page with redirect`: remove redirected URLs from sitemap and canonicals; submit only final 200 URLs.
|
|
52
|
+
- `Redirect error`: inspect redirect chains, protocol/host loops, blocked destinations, and non-200 final responses.
|
|
53
|
+
- `Alternate page with proper canonical tag`: usually acceptable; verify sitemap does not include alternates.
|
|
54
|
+
- `Discovered - currently not indexed`: improve crawl signals first: clean sitemap, canonical consistency, internal links, content quality, stable 200 responses, and lastmod accuracy.
|
|
55
|
+
- `Excluded by noindex`: verify it is intentional and keep those URLs out of sitemap.
|
|
56
|
+
|
|
57
|
+
## Output
|
|
58
|
+
|
|
59
|
+
Report:
|
|
60
|
+
|
|
61
|
+
- Export summary: date range, indexed/not-indexed trend, issue buckets, and limitations.
|
|
62
|
+
- Likely root cause, with evidence from files or live HTTP responses.
|
|
63
|
+
- Prioritized fixes, smallest reliable change first.
|
|
64
|
+
- Verification steps for local tests, build, live sitemap/robots/canonical checks, and Search Console validation.
|
|
65
|
+
|
|
66
|
+
Call out when the export is aggregate-only and does not contain affected URL examples. Do not infer exact URLs unless the CSVs or live sitemap provide them.
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Summarize Google Search Console Coverage CSV exports."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import csv
|
|
8
|
+
import json
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
STANDARD_FILES = {
|
|
14
|
+
"chart": "Chart.csv",
|
|
15
|
+
"metadata": "Metadata.csv",
|
|
16
|
+
"critical": "Critical issues.csv",
|
|
17
|
+
"non_critical": "Non-critical issues.csv",
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def read_rows(path: Path) -> list[dict[str, str]]:
|
|
22
|
+
if not path.exists():
|
|
23
|
+
return []
|
|
24
|
+
with path.open(newline="", encoding="utf-8-sig") as handle:
|
|
25
|
+
return [
|
|
26
|
+
{key: (value or "").strip() for key, value in row.items()}
|
|
27
|
+
for row in csv.DictReader(handle)
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def int_or_none(value: str) -> int | None:
|
|
32
|
+
if value == "":
|
|
33
|
+
return None
|
|
34
|
+
try:
|
|
35
|
+
return int(value.replace(",", ""))
|
|
36
|
+
except ValueError:
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def chart_summary(rows: list[dict[str, str]]) -> dict[str, Any]:
|
|
41
|
+
points = []
|
|
42
|
+
for row in rows:
|
|
43
|
+
points.append(
|
|
44
|
+
{
|
|
45
|
+
"date": row.get("Date", ""),
|
|
46
|
+
"not_indexed": int_or_none(row.get("Not indexed", "")),
|
|
47
|
+
"indexed": int_or_none(row.get("Indexed", "")),
|
|
48
|
+
"impressions": int_or_none(row.get("Impressions", "")),
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
indexed_points = [
|
|
53
|
+
point for point in points if point["indexed"] is not None or point["not_indexed"] is not None
|
|
54
|
+
]
|
|
55
|
+
first = indexed_points[0] if indexed_points else None
|
|
56
|
+
last = indexed_points[-1] if indexed_points else None
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
"date_range": [points[0]["date"], points[-1]["date"]] if points else None,
|
|
60
|
+
"first_indexing_point": first,
|
|
61
|
+
"latest_indexing_point": last,
|
|
62
|
+
"indexed_delta": None if not first or not last else (last["indexed"] or 0) - (first["indexed"] or 0),
|
|
63
|
+
"not_indexed_delta": None
|
|
64
|
+
if not first or not last
|
|
65
|
+
else (last["not_indexed"] or 0) - (first["not_indexed"] or 0),
|
|
66
|
+
"impressions_total": sum(point["impressions"] or 0 for point in points),
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def issue_summary(rows: list[dict[str, str]]) -> list[dict[str, Any]]:
|
|
71
|
+
issues = []
|
|
72
|
+
for row in rows:
|
|
73
|
+
reason = row.get("Reason", "")
|
|
74
|
+
if not reason:
|
|
75
|
+
continue
|
|
76
|
+
issues.append(
|
|
77
|
+
{
|
|
78
|
+
"reason": reason,
|
|
79
|
+
"source": row.get("Source", ""),
|
|
80
|
+
"validation": row.get("Validation", ""),
|
|
81
|
+
"pages": int_or_none(row.get("Pages", "")) or 0,
|
|
82
|
+
}
|
|
83
|
+
)
|
|
84
|
+
return sorted(issues, key=lambda issue: issue["pages"], reverse=True)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def load_export(export_dir: Path) -> dict[str, Any]:
|
|
88
|
+
files = {key: export_dir / filename for key, filename in STANDARD_FILES.items()}
|
|
89
|
+
metadata_rows = read_rows(files["metadata"])
|
|
90
|
+
return {
|
|
91
|
+
"export_dir": str(export_dir),
|
|
92
|
+
"metadata": {row.get("Property", ""): row.get("Value", "") for row in metadata_rows},
|
|
93
|
+
"chart": chart_summary(read_rows(files["chart"])),
|
|
94
|
+
"critical_issues": issue_summary(read_rows(files["critical"])),
|
|
95
|
+
"non_critical_issues": issue_summary(read_rows(files["non_critical"])),
|
|
96
|
+
"missing_files": [filename for filename in STANDARD_FILES.values() if not (export_dir / filename).exists()],
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def print_markdown(summary: dict[str, Any]) -> None:
|
|
101
|
+
chart = summary["chart"]
|
|
102
|
+
latest = chart["latest_indexing_point"] or {}
|
|
103
|
+
print("# Search Console Coverage Summary")
|
|
104
|
+
print()
|
|
105
|
+
print(f"- Export: `{summary['export_dir']}`")
|
|
106
|
+
if chart["date_range"]:
|
|
107
|
+
print(f"- Date range: {chart['date_range'][0]} to {chart['date_range'][1]}")
|
|
108
|
+
print(f"- Latest indexed: {latest.get('indexed', 'n/a')}")
|
|
109
|
+
print(f"- Latest not indexed: {latest.get('not_indexed', 'n/a')}")
|
|
110
|
+
print(f"- Total impressions in chart: {chart['impressions_total']}")
|
|
111
|
+
if summary["metadata"]:
|
|
112
|
+
print(f"- Metadata: {summary['metadata']}")
|
|
113
|
+
if summary["missing_files"]:
|
|
114
|
+
print(f"- Missing standard files: {', '.join(summary['missing_files'])}")
|
|
115
|
+
|
|
116
|
+
for label, issues in (
|
|
117
|
+
("Critical Issues", summary["critical_issues"]),
|
|
118
|
+
("Non-Critical Issues", summary["non_critical_issues"]),
|
|
119
|
+
):
|
|
120
|
+
print()
|
|
121
|
+
print(f"## {label}")
|
|
122
|
+
if not issues:
|
|
123
|
+
print("- None reported")
|
|
124
|
+
continue
|
|
125
|
+
for issue in issues:
|
|
126
|
+
print(
|
|
127
|
+
f"- {issue['reason']}: {issue['pages']} pages"
|
|
128
|
+
f" ({issue['source']}, validation: {issue['validation']})"
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
print()
|
|
132
|
+
print("## Notes")
|
|
133
|
+
print("- Standard Coverage exports are aggregate reports; they may not include affected URL examples.")
|
|
134
|
+
print("- Redirect and canonical buckets should be checked against sitemap URLs and live canonical tags.")
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def main() -> None:
|
|
138
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
139
|
+
parser.add_argument("export_dir", type=Path)
|
|
140
|
+
parser.add_argument("--json", action="store_true", help="Print machine-readable JSON")
|
|
141
|
+
args = parser.parse_args()
|
|
142
|
+
|
|
143
|
+
summary = load_export(args.export_dir)
|
|
144
|
+
if args.json:
|
|
145
|
+
print(json.dumps(summary, indent=2, sort_keys=True))
|
|
146
|
+
else:
|
|
147
|
+
print_markdown(summary)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
if __name__ == "__main__":
|
|
151
|
+
main()
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Summarize Google Search Console Coverage CSV exports."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import csv
|
|
8
|
+
import json
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
STANDARD_FILES = {
|
|
14
|
+
"chart": "Chart.csv",
|
|
15
|
+
"metadata": "Metadata.csv",
|
|
16
|
+
"critical": "Critical issues.csv",
|
|
17
|
+
"non_critical": "Non-critical issues.csv",
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def read_rows(path: Path) -> list[dict[str, str]]:
|
|
22
|
+
if not path.exists():
|
|
23
|
+
return []
|
|
24
|
+
with path.open(newline="", encoding="utf-8-sig") as handle:
|
|
25
|
+
return [
|
|
26
|
+
{key: (value or "").strip() for key, value in row.items()}
|
|
27
|
+
for row in csv.DictReader(handle)
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def int_or_none(value: str) -> int | None:
|
|
32
|
+
if value == "":
|
|
33
|
+
return None
|
|
34
|
+
try:
|
|
35
|
+
return int(value.replace(",", ""))
|
|
36
|
+
except ValueError:
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def chart_summary(rows: list[dict[str, str]]) -> dict[str, Any]:
|
|
41
|
+
points = []
|
|
42
|
+
for row in rows:
|
|
43
|
+
points.append(
|
|
44
|
+
{
|
|
45
|
+
"date": row.get("Date", ""),
|
|
46
|
+
"not_indexed": int_or_none(row.get("Not indexed", "")),
|
|
47
|
+
"indexed": int_or_none(row.get("Indexed", "")),
|
|
48
|
+
"impressions": int_or_none(row.get("Impressions", "")),
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
indexed_points = [
|
|
53
|
+
point for point in points if point["indexed"] is not None or point["not_indexed"] is not None
|
|
54
|
+
]
|
|
55
|
+
first = indexed_points[0] if indexed_points else None
|
|
56
|
+
last = indexed_points[-1] if indexed_points else None
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
"date_range": [points[0]["date"], points[-1]["date"]] if points else None,
|
|
60
|
+
"first_indexing_point": first,
|
|
61
|
+
"latest_indexing_point": last,
|
|
62
|
+
"indexed_delta": None if not first or not last else (last["indexed"] or 0) - (first["indexed"] or 0),
|
|
63
|
+
"not_indexed_delta": None
|
|
64
|
+
if not first or not last
|
|
65
|
+
else (last["not_indexed"] or 0) - (first["not_indexed"] or 0),
|
|
66
|
+
"impressions_total": sum(point["impressions"] or 0 for point in points),
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def issue_summary(rows: list[dict[str, str]]) -> list[dict[str, Any]]:
|
|
71
|
+
issues = []
|
|
72
|
+
for row in rows:
|
|
73
|
+
reason = row.get("Reason", "")
|
|
74
|
+
if not reason:
|
|
75
|
+
continue
|
|
76
|
+
issues.append(
|
|
77
|
+
{
|
|
78
|
+
"reason": reason,
|
|
79
|
+
"source": row.get("Source", ""),
|
|
80
|
+
"validation": row.get("Validation", ""),
|
|
81
|
+
"pages": int_or_none(row.get("Pages", "")) or 0,
|
|
82
|
+
}
|
|
83
|
+
)
|
|
84
|
+
return sorted(issues, key=lambda issue: issue["pages"], reverse=True)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def load_export(export_dir: Path) -> dict[str, Any]:
|
|
88
|
+
files = {key: export_dir / filename for key, filename in STANDARD_FILES.items()}
|
|
89
|
+
metadata_rows = read_rows(files["metadata"])
|
|
90
|
+
return {
|
|
91
|
+
"export_dir": str(export_dir),
|
|
92
|
+
"metadata": {row.get("Property", ""): row.get("Value", "") for row in metadata_rows},
|
|
93
|
+
"chart": chart_summary(read_rows(files["chart"])),
|
|
94
|
+
"critical_issues": issue_summary(read_rows(files["critical"])),
|
|
95
|
+
"non_critical_issues": issue_summary(read_rows(files["non_critical"])),
|
|
96
|
+
"missing_files": [filename for filename in STANDARD_FILES.values() if not (export_dir / filename).exists()],
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def print_markdown(summary: dict[str, Any]) -> None:
|
|
101
|
+
chart = summary["chart"]
|
|
102
|
+
latest = chart["latest_indexing_point"] or {}
|
|
103
|
+
print("# Search Console Coverage Summary")
|
|
104
|
+
print()
|
|
105
|
+
print(f"- Export: `{summary['export_dir']}`")
|
|
106
|
+
if chart["date_range"]:
|
|
107
|
+
print(f"- Date range: {chart['date_range'][0]} to {chart['date_range'][1]}")
|
|
108
|
+
print(f"- Latest indexed: {latest.get('indexed', 'n/a')}")
|
|
109
|
+
print(f"- Latest not indexed: {latest.get('not_indexed', 'n/a')}")
|
|
110
|
+
print(f"- Total impressions in chart: {chart['impressions_total']}")
|
|
111
|
+
if summary["metadata"]:
|
|
112
|
+
print(f"- Metadata: {summary['metadata']}")
|
|
113
|
+
if summary["missing_files"]:
|
|
114
|
+
print(f"- Missing standard files: {', '.join(summary['missing_files'])}")
|
|
115
|
+
|
|
116
|
+
for label, issues in (
|
|
117
|
+
("Critical Issues", summary["critical_issues"]),
|
|
118
|
+
("Non-Critical Issues", summary["non_critical_issues"]),
|
|
119
|
+
):
|
|
120
|
+
print()
|
|
121
|
+
print(f"## {label}")
|
|
122
|
+
if not issues:
|
|
123
|
+
print("- None reported")
|
|
124
|
+
continue
|
|
125
|
+
for issue in issues:
|
|
126
|
+
print(
|
|
127
|
+
f"- {issue['reason']}: {issue['pages']} pages"
|
|
128
|
+
f" ({issue['source']}, validation: {issue['validation']})"
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
print()
|
|
132
|
+
print("## Notes")
|
|
133
|
+
print("- Standard Coverage exports are aggregate reports; they may not include affected URL examples.")
|
|
134
|
+
print("- Redirect and canonical buckets should be checked against sitemap URLs and live canonical tags.")
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def main() -> None:
|
|
138
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
139
|
+
parser.add_argument("export_dir", type=Path)
|
|
140
|
+
parser.add_argument("--json", action="store_true", help="Print machine-readable JSON")
|
|
141
|
+
args = parser.parse_args()
|
|
142
|
+
|
|
143
|
+
summary = load_export(args.export_dir)
|
|
144
|
+
if args.json:
|
|
145
|
+
print(json.dumps(summary, indent=2, sort_keys=True))
|
|
146
|
+
else:
|
|
147
|
+
print_markdown(summary)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
if __name__ == "__main__":
|
|
151
|
+
main()
|
|
@@ -17,6 +17,17 @@ const promptNameFor = (pkg) => `${pkg.name}.prompt.md`
|
|
|
17
17
|
const generatedHeader = '<!-- Generated by scripts/build-adapters.sh. Do not edit directly. -->'
|
|
18
18
|
const stripFrontmatter = (content) => content.replace(/^---\n[\s\S]*?\n---\n?/, '')
|
|
19
19
|
const stripTrailingLineWhitespace = (content) => content.replace(/[ \t]+$/gm, '')
|
|
20
|
+
const copySkillResources = (pkg, destDir) => {
|
|
21
|
+
for (const resourceDir of ['scripts']) {
|
|
22
|
+
const source = path.join(root, pkg.path, resourceDir)
|
|
23
|
+
const dest = path.join(destDir, resourceDir)
|
|
24
|
+
if (!fs.existsSync(source)) {
|
|
25
|
+
continue
|
|
26
|
+
}
|
|
27
|
+
fs.rmSync(dest, { recursive: true, force: true })
|
|
28
|
+
fs.cpSync(source, dest, { recursive: true })
|
|
29
|
+
}
|
|
30
|
+
}
|
|
20
31
|
|
|
21
32
|
const claudeSkillPaths = catalog.packages
|
|
22
33
|
.filter((pkg) => pkg.adapters.includes('claude'))
|
|
@@ -147,6 +158,7 @@ for (const pkg of catalog.packages) {
|
|
|
147
158
|
) + '\n',
|
|
148
159
|
)
|
|
149
160
|
fs.writeFileSync(path.join(claudeDestDir, 'SKILL.md'), [generatedHeader, '', canonical, ''].join('\n'))
|
|
161
|
+
copySkillResources(pkg, claudeDestDir)
|
|
150
162
|
}
|
|
151
163
|
|
|
152
164
|
if (!pkg.adapters.includes('cursor')) {
|
|
@@ -157,6 +169,7 @@ for (const pkg of catalog.packages) {
|
|
|
157
169
|
const destPath = path.join(destDir, 'SKILL.md')
|
|
158
170
|
fs.mkdirSync(destDir, { recursive: true })
|
|
159
171
|
fs.writeFileSync(destPath, [generatedHeader, '', canonical, ''].join('\n'))
|
|
172
|
+
copySkillResources(pkg, destDir)
|
|
160
173
|
}
|
|
161
174
|
|
|
162
175
|
console.log('generated marketplace manifests, Copilot prompts, and Cursor adapter SKILL.md files')
|