indexgap 1.5.0__tar.gz
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.
- indexgap-1.5.0/LICENSE +21 -0
- indexgap-1.5.0/PKG-INFO +399 -0
- indexgap-1.5.0/README.md +372 -0
- indexgap-1.5.0/indexgap/__init__.py +2 -0
- indexgap-1.5.0/indexgap/__main__.py +9 -0
- indexgap-1.5.0/indexgap/aeo.py +365 -0
- indexgap-1.5.0/indexgap/checks.py +678 -0
- indexgap-1.5.0/indexgap/cite.py +335 -0
- indexgap-1.5.0/indexgap/cli.py +1083 -0
- indexgap-1.5.0/indexgap/content.py +517 -0
- indexgap-1.5.0/indexgap/core.py +835 -0
- indexgap-1.5.0/indexgap/doctor.py +461 -0
- indexgap-1.5.0/indexgap/engines.py +179 -0
- indexgap-1.5.0/indexgap/freshness.py +130 -0
- indexgap-1.5.0/indexgap/generate.py +453 -0
- indexgap-1.5.0/indexgap/hreflang.py +324 -0
- indexgap-1.5.0/indexgap/i18n.py +133 -0
- indexgap-1.5.0/indexgap/install.py +409 -0
- indexgap-1.5.0/indexgap/locale/__init__.py +8 -0
- indexgap-1.5.0/indexgap/locale/en/__init__.py +14 -0
- indexgap-1.5.0/indexgap/locale/en/checks.py +163 -0
- indexgap-1.5.0/indexgap/locale/en/cite.py +100 -0
- indexgap-1.5.0/indexgap/locale/en/cli.py +281 -0
- indexgap-1.5.0/indexgap/locale/en/content.py +89 -0
- indexgap-1.5.0/indexgap/locale/en/core.py +135 -0
- indexgap-1.5.0/indexgap/locale/en/doctor.py +168 -0
- indexgap-1.5.0/indexgap/locale/en/hreflang.py +116 -0
- indexgap-1.5.0/indexgap/locale/en/repair.py +229 -0
- indexgap-1.5.0/indexgap/locale/en/report.py +223 -0
- indexgap-1.5.0/indexgap/portfolio.py +268 -0
- indexgap-1.5.0/indexgap/profiles.py +198 -0
- indexgap-1.5.0/indexgap/publish.py +313 -0
- indexgap-1.5.0/indexgap/repair.py +343 -0
- indexgap-1.5.0/indexgap/report.py +431 -0
- indexgap-1.5.0/indexgap/settings.py +222 -0
- indexgap-1.5.0/indexgap/skills/indexgap-plan/SKILL.en.md +97 -0
- indexgap-1.5.0/indexgap/skills/indexgap-plan/SKILL.md +97 -0
- indexgap-1.5.0/indexgap/skills/indexgap-portfolio/SKILL.en.md +94 -0
- indexgap-1.5.0/indexgap/skills/indexgap-portfolio/SKILL.md +90 -0
- indexgap-1.5.0/indexgap/skills/indexgap-publish/SKILL.en.md +151 -0
- indexgap-1.5.0/indexgap/skills/indexgap-publish/SKILL.md +147 -0
- indexgap-1.5.0/indexgap/skills/indexgap-review/SKILL.en.md +139 -0
- indexgap-1.5.0/indexgap/skills/indexgap-review/SKILL.md +137 -0
- indexgap-1.5.0/indexgap/sources.py +421 -0
- indexgap-1.5.0/indexgap.egg-info/PKG-INFO +399 -0
- indexgap-1.5.0/indexgap.egg-info/SOURCES.txt +59 -0
- indexgap-1.5.0/indexgap.egg-info/dependency_links.txt +1 -0
- indexgap-1.5.0/indexgap.egg-info/entry_points.txt +2 -0
- indexgap-1.5.0/indexgap.egg-info/top_level.txt +1 -0
- indexgap-1.5.0/pyproject.toml +44 -0
- indexgap-1.5.0/setup.cfg +4 -0
- indexgap-1.5.0/tests/test_cite.py +214 -0
- indexgap-1.5.0/tests/test_i18n.py +302 -0
- indexgap-1.5.0/tests/test_install.py +299 -0
- indexgap-1.5.0/tests/test_live.py +226 -0
- indexgap-1.5.0/tests/test_multilingual.py +288 -0
- indexgap-1.5.0/tests/test_portfolio.py +246 -0
- indexgap-1.5.0/tests/test_regressions.py +733 -0
- indexgap-1.5.0/tests/test_repair.py +336 -0
- indexgap-1.5.0/tests/test_sources.py +250 -0
- indexgap-1.5.0/tests/test_wave2.py +466 -0
indexgap-1.5.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
indexgap-1.5.0/PKG-INFO
ADDED
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: indexgap
|
|
3
|
+
Version: 1.5.0
|
|
4
|
+
Summary: Lint your programmatic SEO pipeline — from keywords to indexed pages. No dependencies.
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/borisowlexa2010-star/IndexGap
|
|
7
|
+
Project-URL: Changelog, https://github.com/borisowlexa2010-star/IndexGap/blob/main/CHANGELOG.md
|
|
8
|
+
Project-URL: Issues, https://github.com/borisowlexa2010-star/IndexGap/issues
|
|
9
|
+
Keywords: seo,programmatic-seo,sitemap,indexnow,aeo,geo,search-console,site-audit,lint
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Site Management
|
|
22
|
+
Classifier: Topic :: Text Processing :: Markup :: HTML
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# indexgap
|
|
29
|
+
|
|
30
|
+
**Lint your programmatic SEO pipeline — from keywords to indexed pages.**
|
|
31
|
+
|
|
32
|
+
> [Русская версия](README.ru.md)
|
|
33
|
+
|
|
34
|
+
You generated three thousand pages from a dataset. They're on disk, they're in
|
|
35
|
+
the sitemap, and there's no traffic. `indexgap` tells you where they were lost.
|
|
36
|
+
|
|
37
|
+
Pure Python 3.9+ standard library. No dependencies, no API keys, no paid
|
|
38
|
+
subscriptions. Nothing leaves your machine without an explicit flag.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Why this exists
|
|
43
|
+
|
|
44
|
+
Plenty of tools audit *a page*. A generated pipeline breaks differently —
|
|
45
|
+
systematically and quietly:
|
|
46
|
+
|
|
47
|
+
* a page is in the sitemap, but no internal link points to it, so the crawler
|
|
48
|
+
never arrives;
|
|
49
|
+
* three hundred pages differ by five words, and the search engine keeps one;
|
|
50
|
+
* the model wrote "12 years in business" and "3,500 orders" — neither number
|
|
51
|
+
exists in any row of your data, but it reads convincingly;
|
|
52
|
+
* `lastmod` equals the build date on every page, so it means nothing;
|
|
53
|
+
* two keywords with the same intent produced two competing pages.
|
|
54
|
+
|
|
55
|
+
None of this is visible page by page. Each one looks fine on its own.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Install
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install indexgap # once the first PyPI release is out
|
|
63
|
+
|
|
64
|
+
# until then — from source:
|
|
65
|
+
git clone https://github.com/borisowlexa2010-star/IndexGap && cd IndexGap
|
|
66
|
+
pip install -e .
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Then, once per project:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
cd ~/projects/my-site
|
|
73
|
+
indexgap init
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`init` reads the project and records what makes it different: where the pages
|
|
77
|
+
are, the site URL, the content type, the dataset. It installs skills into
|
|
78
|
+
`.claude/skills/` so your coding agent picks them up on its own, and adds the
|
|
79
|
+
working files to `.gitignore`.
|
|
80
|
+
|
|
81
|
+
Nothing project-specific is ever copied between projects — it is detected
|
|
82
|
+
fresh each time. **The IndexNow key in particular is never carried over:**
|
|
83
|
+
it is bound to one domain by a file at that site's root, and a borrowed key
|
|
84
|
+
returns 403. `indexgap init --key` mints a new one for this project.
|
|
85
|
+
|
|
86
|
+
After that, the daily command is just:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
indexgap check
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Commands
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
indexgap init # install into this project
|
|
98
|
+
indexgap plan keywords.csv # audit the keyword set before generating
|
|
99
|
+
indexgap check # everything local: text, structure, machine-readability
|
|
100
|
+
indexgap brief --write # turn the findings into work orders beside the pages
|
|
101
|
+
indexgap sitemap --out-dir ./public # sitemap with sharding and an honest lastmod
|
|
102
|
+
indexgap notify --key <your-key> # tell IndexNow what actually changed
|
|
103
|
+
indexgap doctor --sitemap ./public/sitemap.xml --indexed gsc.csv
|
|
104
|
+
indexgap portfolio projects.json # every site you own, in one run
|
|
105
|
+
indexgap profiles # what the content-type presets change
|
|
106
|
+
indexgap cite --domain example.com # do AI answers cite you? (your own API keys)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Output is English by default, Russian with `--lang ru` (or `INDEXGAP_LANG`, or
|
|
110
|
+
your system locale). That includes the HTML report, every finding description,
|
|
111
|
+
`--help`, and the skills `indexgap init` installs into your project.
|
|
112
|
+
|
|
113
|
+
Understands both built HTML and Markdown sources with frontmatter. Input file
|
|
114
|
+
encoding is detected, not assumed: UTF-16, BOM, and the cp1251 CSVs that
|
|
115
|
+
Russian Excel produces all read correctly. Exports are read as they come —
|
|
116
|
+
CSV with any delimiter, XLSX straight from Ahrefs or Semrush without
|
|
117
|
+
re-saving, JSON, NDJSON, an XML sitemap, or a plain list of URLs — and the
|
|
118
|
+
keyword column is found whether the export calls it `Keyword`, `Фраза`,
|
|
119
|
+
`Запрос` or `Search Term`.
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## What it checks
|
|
124
|
+
|
|
125
|
+
**Before generating** — exact duplicate keys, slug collisions and *same-intent*
|
|
126
|
+
keys. That last one matters most: two keywords meaning the same thing will
|
|
127
|
+
produce two competing pages, and it is cheaper never to create the second.
|
|
128
|
+
|
|
129
|
+
**After generating — the text**
|
|
130
|
+
|
|
131
|
+
| Check | Why it matters |
|
|
132
|
+
|---|---|
|
|
133
|
+
| **Numbers absent from your data** | prices, terms and counts are verified against the source row. A number that appears nowhere in the dataset is never forgiven, no matter how many pages repeat it |
|
|
134
|
+
| Identical heading skeletons | different words, same structure — a stamping tell |
|
|
135
|
+
| Identical opening sentences | the second tell |
|
|
136
|
+
| Leftover brief, `status: draft` | unfinished pages never reach the sitemap or the IndexNow queue |
|
|
137
|
+
| "Click here" anchors | a link with no meaning in it |
|
|
138
|
+
|
|
139
|
+
**After generating — the structure**
|
|
140
|
+
|
|
141
|
+
Near-duplicates (exact pairwise below 400 pages, MinHash + LSH above), share of
|
|
142
|
+
unique text measured *by bigrams* rather than words, thin pages, orphans,
|
|
143
|
+
click depth, `noindex`, `nosnippet`, foreign canonicals, missing or duplicate
|
|
144
|
+
H1, duplicate titles and descriptions.
|
|
145
|
+
|
|
146
|
+
**Machine readability for AI search**
|
|
147
|
+
|
|
148
|
+
Snippet controls (`nosnippet`, `max-snippet:0`), `robots.txt` rules for
|
|
149
|
+
OAI-SearchBot, PerplexityBot, ClaudeBot, GPTBot and Google-Extended (they are
|
|
150
|
+
not interchangeable — blocking OAI-SearchBot removes you from ChatGPT search
|
|
151
|
+
answers, while Google-Extended does not affect AI Overviews), empty JS shells,
|
|
152
|
+
a direct answer in the first paragraph, question-shaped subheadings, valid
|
|
153
|
+
JSON-LD that matches the visible text, machine-readable dates and author.
|
|
154
|
+
|
|
155
|
+
**At publish time** — sitemap sharding past 45,000 URLs and a `lastmod` that
|
|
156
|
+
changes only when the text, title or description changed. Editing one menu item
|
|
157
|
+
does not mark the whole site as modified. IndexNow sends only what changed.
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## From findings to work orders
|
|
162
|
+
|
|
163
|
+
A report answers "what is wrong with me". `indexgap brief` answers "what do I
|
|
164
|
+
do" — it lays the same findings out as markdown files next to the pages they
|
|
165
|
+
belong to, each with an imperative fix, the thresholds the fix has to meet, and
|
|
166
|
+
the dataset row as the only permitted source of numbers.
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
indexgap brief --dataset keywords.csv # dry run: says what it would write
|
|
170
|
+
indexgap brief --dataset keywords.csv --write
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
On the live 2,970-page catalogue the check reports 6,074 findings and `brief`
|
|
174
|
+
writes 966 work orders. The difference is three placement rules, and they are
|
|
175
|
+
the whole point:
|
|
176
|
+
|
|
177
|
+
* **A property of the template gets one brief, not 2,919.** `no-question-headings`
|
|
178
|
+
fired on 2,919 of 2,970 pages. That is one edit to one template, and 2,919
|
|
179
|
+
identical tasks would bury the 51 pages that actually differ. The same holds
|
|
180
|
+
per language: a finding on every page of the Chinese version is 10% of the
|
|
181
|
+
site but still one template fix.
|
|
182
|
+
* **Near-duplicates are fixed as a group.** 588 duplicate findings are 72
|
|
183
|
+
groups. One page out of a group of 17 cannot be fixed alone — the brief goes
|
|
184
|
+
to the group: keep one, pull the rest apart by intent, and never link them to
|
|
185
|
+
each other.
|
|
186
|
+
* **robots.txt, markup and hreflang clusters belong to the site**, not to a
|
|
187
|
+
page, and live in their own file.
|
|
188
|
+
|
|
189
|
+
`--limit` (50 by default) writes the heaviest pages first; 892 briefs is not a
|
|
190
|
+
task list, it is a second report. Nothing is created without `--write`.
|
|
191
|
+
|
|
192
|
+
**The package writes no text, on purpose.** Its central check compares the
|
|
193
|
+
numbers on a page against the dataset row that produced it. If the package
|
|
194
|
+
supplied those numbers itself, the check would be checking its own output and
|
|
195
|
+
would always be green. So `brief` states the task and stops; a person or an
|
|
196
|
+
agent writes the words.
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## The funnel
|
|
201
|
+
|
|
202
|
+
The reason to install it at all:
|
|
203
|
+
|
|
204
|
+
```
|
|
205
|
+
Generated 58
|
|
206
|
+
Indexable 56 (−2: noindex or a canonical pointing elsewhere)
|
|
207
|
+
In sitemap 56
|
|
208
|
+
In at least one index 28 (−28: the engine knows the URL and didn't add it)
|
|
209
|
+
|
|
210
|
+
Why pages are not indexed:
|
|
211
|
+
7 orphaned or unreachable → link them from hub pages
|
|
212
|
+
12 deeper than the click budget → move them up
|
|
213
|
+
9 no local explanation → check status in Search Console
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Index data comes from ordinary exports — no API keys. A webmaster panel is the
|
|
217
|
+
direct source (Search Console, Bing Webmaster Tools, Yandex.Webmaster, Naver,
|
|
218
|
+
Seznam), but not everyone has one, so exports from **Ahrefs, Semrush, Serpstat,
|
|
219
|
+
Moz, Screaming Frog, Sitebulb, JetOctopus, OnCrawl, Netpeak, GA4, Matomo,
|
|
220
|
+
Plausible** — or a plain list of URLs — are read too, in CSV, XLSX, JSON,
|
|
221
|
+
NDJSON or XML.
|
|
222
|
+
|
|
223
|
+
They are not interchangeable, and the tool refuses to pretend otherwise. A
|
|
224
|
+
panel answers "does the engine know this page". Analytics proves a page is
|
|
225
|
+
indexed, but only for pages someone actually visited — its silence proves
|
|
226
|
+
nothing. A crawler proves reachability, not indexation. Ahrefs and Semrush are
|
|
227
|
+
*their* index, not Google's. So the funnel step is renamed to match the
|
|
228
|
+
evidence — "at least in one index" versus "known to a third-party service" —
|
|
229
|
+
and when a crawler export sits next to a panel, the report says out loud that
|
|
230
|
+
the step count is higher than real indexation. Engine-vs-engine comparison
|
|
231
|
+
runs over panels only.
|
|
232
|
+
|
|
233
|
+
**Ask for all the panels you have, not just Google.** A page missing *everywhere* is a technical problem. A page missing
|
|
234
|
+
*only in one engine* was crawled and accepted by the others, which makes it a
|
|
235
|
+
quality or speed question that technical fixes rarely solve. Without the split
|
|
236
|
+
the two look identical and people fix the wrong thing.
|
|
237
|
+
|
|
238
|
+
One honest caveat the tool states out loud: the Search Console "Pages" export
|
|
239
|
+
is an *impressions* report, not an index report. A page that is indexed but has
|
|
240
|
+
no impressions won't appear in it, so on a young site the funnel overstates
|
|
241
|
+
losses.
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## Multilingual and multi-region
|
|
246
|
+
|
|
247
|
+
A programmatic pipeline is usually multilingual, and that breaks in ways a
|
|
248
|
+
single-language site never does.
|
|
249
|
+
|
|
250
|
+
Text volume and title/description length are measured by each page's **script**,
|
|
251
|
+
in display width — not by one language detected for the whole project. That
|
|
252
|
+
distinction is not academic: on a live 2,970-page catalogue in ten languages the
|
|
253
|
+
project language came out as `en`, every Chinese page was counted as English,
|
|
254
|
+
and all 174 `thin` findings were false. Width also handles mixed strings, which
|
|
255
|
+
is what real pages are: those Chinese titles are 43% Han and the rest Latin.
|
|
256
|
+
|
|
257
|
+
Anchor length is judged only in scripts where length means something. All 914
|
|
258
|
+
`vague-anchor` findings on that same site were false — "यमन" (Yemen) and "হোম"
|
|
259
|
+
(Home) are three characters and complete words.
|
|
260
|
+
|
|
261
|
+
**hreflang** is checked as a cluster: a missing self-reference, a one-way link
|
|
262
|
+
(Google discards the whole cluster rather than counting it partly), an alternate
|
|
263
|
+
that is noindex or canonicalised elsewhere, a canonical that leaves the language
|
|
264
|
+
and cancels the cluster, a missing `x-default`, and language codes where a
|
|
265
|
+
country code was meant — `uk` is Ukrainian, not the United Kingdom. On a
|
|
266
|
+
monolingual site none of this runs.
|
|
267
|
+
|
|
268
|
+
And `en-us` versus `en-gb` is **not** a duplicate. Those pages are legitimately
|
|
269
|
+
near-identical; "keep one and set a canonical" would delete the regional
|
|
270
|
+
version. Pairs inside one hreflang cluster are excluded from the duplicate
|
|
271
|
+
check and reported separately.
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## Content-type profiles
|
|
276
|
+
|
|
277
|
+
Thresholds differ across content types by substance, not taste. 250 words is
|
|
278
|
+
normal for a guide and absurd for an event card; fact-checking is meaningless
|
|
279
|
+
where there is no dataset at all.
|
|
280
|
+
|
|
281
|
+
| Profile | For | What changes |
|
|
282
|
+
|---|---|---|
|
|
283
|
+
| `catalog` | pages generated from data rows | fact-checking is primary; duplicate threshold 0.80; thin under 250 words |
|
|
284
|
+
| `events` | listings, schedules, venues | threshold 0.88 — two dates of one tour are legitimately similar; adds `stale-event` |
|
|
285
|
+
| `ugc` | feeds, threads, reviews | fact-checking is switched off *and says so*; threshold 0.92 |
|
|
286
|
+
| `product` | dozens of landing pages, not thousands | duplicates aren't the issue; the AI-readability checks are |
|
|
287
|
+
|
|
288
|
+
`stale-event` catches an event whose date has passed while the page stays open
|
|
289
|
+
to indexing. That isn't a traffic problem, it's a trust problem: someone drives
|
|
290
|
+
to a concert that no longer exists.
|
|
291
|
+
|
|
292
|
+
For `ugc`, silence is not a clean bill of health — it means there was nothing to
|
|
293
|
+
check against, and the tool prints that as a line rather than leaving you to
|
|
294
|
+
assume.
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
## Portfolio
|
|
299
|
+
|
|
300
|
+
Separate reports answer "what's wrong with this site". A portfolio answers the
|
|
301
|
+
question you can't see in them: **what breaks the same way everywhere.**
|
|
302
|
+
|
|
303
|
+
```
|
|
304
|
+
· visa 2933 pages critical 412 [catalog]
|
|
305
|
+
· events 840 pages critical 61 [events]
|
|
306
|
+
· feed 5100 pages critical 0 [ugc]
|
|
307
|
+
· product 34 pages critical 2 [product]
|
|
308
|
+
|
|
309
|
+
Shared problems, as a share of each project's pages:
|
|
310
|
+
orphan 3 projects: visa 41%, events 38%, product 35%
|
|
311
|
+
same-opening 2 projects: visa 62%, events 55%
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Shares, not counts: a hundred findings across three thousand pages and ten
|
|
315
|
+
across twenty are the same disease at different volumes. One project failing
|
|
316
|
+
doesn't stop the run — it becomes a line in the report.
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
## What it does *not* claim
|
|
321
|
+
|
|
322
|
+
It does not promise citations in AI search, and it says so in the output.
|
|
323
|
+
Ahrefs, across 75,000 brands, found AI visibility correlates most with mentions
|
|
324
|
+
*off* your site (0.66–0.74) and with page count at **0.19** — which is exactly
|
|
325
|
+
what a programmatic pipeline produces. 76% of AI Overview citations come from
|
|
326
|
+
pages already ranking in the classic top 10.
|
|
327
|
+
|
|
328
|
+
So: machine readability is a necessary condition and this tool's job. Getting
|
|
329
|
+
cited is decided by work outside your files, and that is not a code problem.
|
|
330
|
+
|
|
331
|
+
`indexgap cite` measures the thermometer, and says so: it asks Perplexity, the
|
|
332
|
+
OpenAI Responses API, the Gemini API and Grok a set of real questions and counts
|
|
333
|
+
how often your domain comes back in the sources. It reports a share of runs, not
|
|
334
|
+
yes/no, because these answers are not deterministic. And it never says "ChatGPT
|
|
335
|
+
cites you" — what it measured is the API, which OpenAI's own docs describe as
|
|
336
|
+
behaving differently from the product. It is the only command that needs API
|
|
337
|
+
keys, it is off by default, and it sends nothing without `--send`.
|
|
338
|
+
|
|
339
|
+
No `llms.txt` generator either. Google has stated it does not support it and
|
|
340
|
+
has no plans to; no engine has confirmed using it for ranking. Generating a
|
|
341
|
+
file nobody reads is a ritual, not a feature.
|
|
342
|
+
|
|
343
|
+
It also doesn't write or rewrite content, check rankings, or call paid services.
|
|
344
|
+
`indexgap brief` formulates the task and stops there — on purpose. The central
|
|
345
|
+
check compares the numbers on a page against the dataset row that produced it;
|
|
346
|
+
if the package supplied those numbers itself, the check would be checking its
|
|
347
|
+
own output and would always be green.
|
|
348
|
+
Rejecting keywords and acting on contested findings is always confirmed by a human.
|
|
349
|
+
|
|
350
|
+
And it does not replace Search Console — it leads you there. If the tool says
|
|
351
|
+
one thing and Search Console says another, Search Console is right.
|
|
352
|
+
|
|
353
|
+
---
|
|
354
|
+
|
|
355
|
+
## Tests
|
|
356
|
+
|
|
357
|
+
```bash
|
|
358
|
+
python3 -m unittest discover -s tests
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
301 scenarios. Each one is a reproduced defect found by two waves of adversarial
|
|
362
|
+
review and one run against six live sites, plus the behaviour of profiles,
|
|
363
|
+
portfolio and project installation. The rule: a finding without a test comes back.
|
|
364
|
+
|
|
365
|
+
## Calibrated on live sites, not fixtures
|
|
366
|
+
|
|
367
|
+
Thresholds are not guesses. They were checked against six production sites —
|
|
368
|
+
7,149 sitemap URLs, 5,041 pages fetched and parsed — and the run changed the
|
|
369
|
+
tool in three ways:
|
|
370
|
+
|
|
371
|
+
* **One cause, not four findings.** Two of the six sites served every page as an
|
|
372
|
+
empty JavaScript shell. The tool reported 1,099 `js-shell` *and* 1,099
|
|
373
|
+
`low-uniqueness` *and* 1,098 `orphan` — one disease counted four times. Checks
|
|
374
|
+
that need text or links are now skipped on a shell, and the run says how many
|
|
375
|
+
shells there were.
|
|
376
|
+
* **Duplicates are groups, not pages.** 588 near-duplicate pages turned out to be
|
|
377
|
+
72 groups, the largest holding 24. "Rewrite 588 pages" is a sentence;
|
|
378
|
+
"untangle 72 topics" is a task.
|
|
379
|
+
* **A finding on every page is a template property.** `vague-anchor` fired on
|
|
380
|
+
2,970 of 2,970 pages — the culprits were a language switcher (`中文`) and a
|
|
381
|
+
social link (`VK`), short in characters and perfectly informative. Anchor
|
|
382
|
+
length is now measured in the right unit, and any code that hits ≥90% of pages
|
|
383
|
+
is labelled as something to fix once in the template.
|
|
384
|
+
|
|
385
|
+
---
|
|
386
|
+
|
|
387
|
+
## For agents
|
|
388
|
+
|
|
389
|
+
`SKILL.md` at the root is the overview. `indexgap/skills/` holds four
|
|
390
|
+
stage-specific skills — `indexgap-plan`, `indexgap-review`, `indexgap-publish`,
|
|
391
|
+
`indexgap-portfolio` — which `indexgap init` copies into your project's
|
|
392
|
+
`.claude/skills/`. A narrow skill fires more accurately than a broad one:
|
|
393
|
+
the agent sees only the commands and finding codes for the stage it's in.
|
|
394
|
+
|
|
395
|
+
Codex users: `indexgap init --agents` writes a marked block into `AGENTS.md`.
|
|
396
|
+
|
|
397
|
+
---
|
|
398
|
+
|
|
399
|
+
MIT.
|