aivisible-fix 0.2.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.
- aivisible_fix-0.2.0/LICENSE +21 -0
- aivisible_fix-0.2.0/PKG-INFO +96 -0
- aivisible_fix-0.2.0/README.md +86 -0
- aivisible_fix-0.2.0/aivisible_fix/__init__.py +1 -0
- aivisible_fix-0.2.0/aivisible_fix/audit.py +110 -0
- aivisible_fix-0.2.0/aivisible_fix/badge.py +50 -0
- aivisible_fix-0.2.0/aivisible_fix/cli.py +101 -0
- aivisible_fix-0.2.0/aivisible_fix/crawlers.py +24 -0
- aivisible_fix-0.2.0/aivisible_fix/fetch.py +33 -0
- aivisible_fix-0.2.0/aivisible_fix/fix.py +62 -0
- aivisible_fix-0.2.0/aivisible_fix/robots.py +80 -0
- aivisible_fix-0.2.0/aivisible_fix.egg-info/PKG-INFO +96 -0
- aivisible_fix-0.2.0/aivisible_fix.egg-info/SOURCES.txt +16 -0
- aivisible_fix-0.2.0/aivisible_fix.egg-info/dependency_links.txt +1 -0
- aivisible_fix-0.2.0/aivisible_fix.egg-info/entry_points.txt +2 -0
- aivisible_fix-0.2.0/aivisible_fix.egg-info/top_level.txt +1 -0
- aivisible_fix-0.2.0/pyproject.toml +15 -0
- aivisible_fix-0.2.0/setup.cfg +4 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 aivisible-fix contributors
|
|
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.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aivisible-fix
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Audit and auto-fix a website's visibility to AI crawlers and answer engines (GPTBot, ClaudeBot, PerplexityBot, and more).
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Dynamic: license-file
|
|
10
|
+
|
|
11
|
+
# aivisible-fix
|
|
12
|
+
|
|
13
|
+
Audits a website for the specific things that make it invisible to AI
|
|
14
|
+
crawlers and AI answer engines (ChatGPT, Claude, Perplexity, Gemini/AI
|
|
15
|
+
Overviews, and others) — and, unlike the free "checker" tools already on
|
|
16
|
+
the market, **generates the fix**, not just a score.
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
$ aivisible-fix audit https://example.com
|
|
20
|
+
AI-visibility audit for https://example.com
|
|
21
|
+
Score: 40/100
|
|
22
|
+
|
|
23
|
+
[FAIL] robots_reachable: robots.txt missing or unreachable ...
|
|
24
|
+
[PASS] no_ai_crawlers_blocked: No known AI crawler is explicitly blocked.
|
|
25
|
+
[FAIL] llms_txt_present: No /llms.txt ...
|
|
26
|
+
...
|
|
27
|
+
|
|
28
|
+
$ aivisible-fix fix https://example.com --name "Example Co" --description "..."
|
|
29
|
+
Wrote robots.fixed.txt
|
|
30
|
+
Wrote llms.txt
|
|
31
|
+
Review both files, then deploy them at your site root. Nothing was auto-published.
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## What it checks
|
|
35
|
+
|
|
36
|
+
1. **robots.txt reachable** — the baseline signal every crawler reads first.
|
|
37
|
+
2. **No AI crawler is explicitly blocked** — checked per-agent (GPTBot,
|
|
38
|
+
ChatGPT-User, OAI-SearchBot, ClaudeBot, Claude-Web, anthropic-ai,
|
|
39
|
+
PerplexityBot, Perplexity-User, Google-Extended, CCBot, Bytespider,
|
|
40
|
+
Amazonbot, Applebot-Extended, cohere-ai, Meta-ExternalAgent, Diffbot,
|
|
41
|
+
YouBot — see `aivisible_fix/crawlers.py`), not just a blanket `Disallow: /`.
|
|
42
|
+
3. **llms.txt present** — the emerging convention (llmstxt.org) for a short,
|
|
43
|
+
LLM-readable summary of what a site is and which pages matter.
|
|
44
|
+
4. **sitemap.xml present and valid**, and **referenced from robots.txt**.
|
|
45
|
+
5. **JSON-LD structured data on the homepage** — what answer engines lean on
|
|
46
|
+
to extract facts (org, product, FAQ, etc.) without guessing from prose.
|
|
47
|
+
|
|
48
|
+
Score is out of 100, weighted toward the two things that, per current
|
|
49
|
+
published research, cause the overwhelming majority of AI-search invisibility:
|
|
50
|
+
blocked crawlers (40 pts) and missing discovery trail — sitemap + llms.txt
|
|
51
|
+
(40 pts combined).
|
|
52
|
+
|
|
53
|
+
## Why this instead of the free checkers
|
|
54
|
+
|
|
55
|
+
Search "AI visibility checker" and you'll find eight-plus free tools
|
|
56
|
+
(isvisible.ai, llmpulse.ai, siftly.ai, and others) that already do the
|
|
57
|
+
scoring for free — that part of the market is a commodity and not worth
|
|
58
|
+
competing on head to head.
|
|
59
|
+
|
|
60
|
+
None of them **write the fix**. This tool's differentiator is that
|
|
61
|
+
`aivisible-fix fix` outputs a ready-to-deploy patched `robots.txt` (every
|
|
62
|
+
existing line preserved, blocked AI agents explicitly re-allowed) and a
|
|
63
|
+
starter `llms.txt` — output, not just diagnosis. The natural extension
|
|
64
|
+
(not built yet — see ROADMAP below) is a GitHub App that opens this as a
|
|
65
|
+
pull request automatically, the way Dependabot does for dependency bumps,
|
|
66
|
+
plus scheduled re-audits so a clean report doesn't silently rot as new
|
|
67
|
+
crawlers ship.
|
|
68
|
+
|
|
69
|
+
## Badge
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
$ aivisible-fix badge https://example.com
|
|
73
|
+
Wrote aivisible-badge.svg (score 40/100). Host it yourself and embed:
|
|
74
|
+
<a href="https://example.com"><img src="/aivisible-badge.svg" alt="AI-visible: 40/100"></a>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Static SVG, generated locally — no server or account required. Color tracks
|
|
78
|
+
score (red under 50, amber under 80, green at 80+).
|
|
79
|
+
|
|
80
|
+
## Install
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
pip install -e .
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
No dependencies beyond the Python 3.10+ standard library — deliberately, so
|
|
87
|
+
it's trivial to also ship as a single-file GitHub Action.
|
|
88
|
+
|
|
89
|
+
## Status
|
|
90
|
+
|
|
91
|
+
v0.1 — functional CLI, tested against live sites (see commit history / this
|
|
92
|
+
was built and smoke-tested against example.com and nytimes.com, correctly
|
|
93
|
+
detecting NYT's well-documented blanket block of AI crawlers). Not yet
|
|
94
|
+
published to PyPI or packaged as a GitHub Action — see BUSINESS_PLAN.md for
|
|
95
|
+
what's next and what needs a human to do it (PyPI account, GitHub App
|
|
96
|
+
registration, a payment processor for the paid tier).
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# aivisible-fix
|
|
2
|
+
|
|
3
|
+
Audits a website for the specific things that make it invisible to AI
|
|
4
|
+
crawlers and AI answer engines (ChatGPT, Claude, Perplexity, Gemini/AI
|
|
5
|
+
Overviews, and others) — and, unlike the free "checker" tools already on
|
|
6
|
+
the market, **generates the fix**, not just a score.
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
$ aivisible-fix audit https://example.com
|
|
10
|
+
AI-visibility audit for https://example.com
|
|
11
|
+
Score: 40/100
|
|
12
|
+
|
|
13
|
+
[FAIL] robots_reachable: robots.txt missing or unreachable ...
|
|
14
|
+
[PASS] no_ai_crawlers_blocked: No known AI crawler is explicitly blocked.
|
|
15
|
+
[FAIL] llms_txt_present: No /llms.txt ...
|
|
16
|
+
...
|
|
17
|
+
|
|
18
|
+
$ aivisible-fix fix https://example.com --name "Example Co" --description "..."
|
|
19
|
+
Wrote robots.fixed.txt
|
|
20
|
+
Wrote llms.txt
|
|
21
|
+
Review both files, then deploy them at your site root. Nothing was auto-published.
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## What it checks
|
|
25
|
+
|
|
26
|
+
1. **robots.txt reachable** — the baseline signal every crawler reads first.
|
|
27
|
+
2. **No AI crawler is explicitly blocked** — checked per-agent (GPTBot,
|
|
28
|
+
ChatGPT-User, OAI-SearchBot, ClaudeBot, Claude-Web, anthropic-ai,
|
|
29
|
+
PerplexityBot, Perplexity-User, Google-Extended, CCBot, Bytespider,
|
|
30
|
+
Amazonbot, Applebot-Extended, cohere-ai, Meta-ExternalAgent, Diffbot,
|
|
31
|
+
YouBot — see `aivisible_fix/crawlers.py`), not just a blanket `Disallow: /`.
|
|
32
|
+
3. **llms.txt present** — the emerging convention (llmstxt.org) for a short,
|
|
33
|
+
LLM-readable summary of what a site is and which pages matter.
|
|
34
|
+
4. **sitemap.xml present and valid**, and **referenced from robots.txt**.
|
|
35
|
+
5. **JSON-LD structured data on the homepage** — what answer engines lean on
|
|
36
|
+
to extract facts (org, product, FAQ, etc.) without guessing from prose.
|
|
37
|
+
|
|
38
|
+
Score is out of 100, weighted toward the two things that, per current
|
|
39
|
+
published research, cause the overwhelming majority of AI-search invisibility:
|
|
40
|
+
blocked crawlers (40 pts) and missing discovery trail — sitemap + llms.txt
|
|
41
|
+
(40 pts combined).
|
|
42
|
+
|
|
43
|
+
## Why this instead of the free checkers
|
|
44
|
+
|
|
45
|
+
Search "AI visibility checker" and you'll find eight-plus free tools
|
|
46
|
+
(isvisible.ai, llmpulse.ai, siftly.ai, and others) that already do the
|
|
47
|
+
scoring for free — that part of the market is a commodity and not worth
|
|
48
|
+
competing on head to head.
|
|
49
|
+
|
|
50
|
+
None of them **write the fix**. This tool's differentiator is that
|
|
51
|
+
`aivisible-fix fix` outputs a ready-to-deploy patched `robots.txt` (every
|
|
52
|
+
existing line preserved, blocked AI agents explicitly re-allowed) and a
|
|
53
|
+
starter `llms.txt` — output, not just diagnosis. The natural extension
|
|
54
|
+
(not built yet — see ROADMAP below) is a GitHub App that opens this as a
|
|
55
|
+
pull request automatically, the way Dependabot does for dependency bumps,
|
|
56
|
+
plus scheduled re-audits so a clean report doesn't silently rot as new
|
|
57
|
+
crawlers ship.
|
|
58
|
+
|
|
59
|
+
## Badge
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
$ aivisible-fix badge https://example.com
|
|
63
|
+
Wrote aivisible-badge.svg (score 40/100). Host it yourself and embed:
|
|
64
|
+
<a href="https://example.com"><img src="/aivisible-badge.svg" alt="AI-visible: 40/100"></a>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Static SVG, generated locally — no server or account required. Color tracks
|
|
68
|
+
score (red under 50, amber under 80, green at 80+).
|
|
69
|
+
|
|
70
|
+
## Install
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
pip install -e .
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
No dependencies beyond the Python 3.10+ standard library — deliberately, so
|
|
77
|
+
it's trivial to also ship as a single-file GitHub Action.
|
|
78
|
+
|
|
79
|
+
## Status
|
|
80
|
+
|
|
81
|
+
v0.1 — functional CLI, tested against live sites (see commit history / this
|
|
82
|
+
was built and smoke-tested against example.com and nytimes.com, correctly
|
|
83
|
+
detecting NYT's well-documented blanket block of AI crawlers). Not yet
|
|
84
|
+
published to PyPI or packaged as a GitHub Action — see BUSINESS_PLAN.md for
|
|
85
|
+
what's next and what needs a human to do it (PyPI account, GitHub App
|
|
86
|
+
registration, a payment processor for the paid tier).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.0"
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from urllib.parse import urljoin
|
|
6
|
+
|
|
7
|
+
from .crawlers import AI_CRAWLERS
|
|
8
|
+
from .fetch import fetch
|
|
9
|
+
from .robots import agent_status, parse_robots_txt
|
|
10
|
+
|
|
11
|
+
# Each check's point value. Weighted toward the two things that, per current
|
|
12
|
+
# published data, cause the overwhelming majority of AI-search invisibility:
|
|
13
|
+
# blocked crawlers and a missing sitemap/structured data trail to follow.
|
|
14
|
+
POINTS = {
|
|
15
|
+
"robots_reachable": 5,
|
|
16
|
+
"no_ai_crawlers_blocked": 40,
|
|
17
|
+
"llms_txt_present": 20,
|
|
18
|
+
"sitemap_present": 15,
|
|
19
|
+
"sitemap_referenced_in_robots": 5,
|
|
20
|
+
"structured_data_present": 15,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class Finding:
|
|
26
|
+
check: str
|
|
27
|
+
passed: bool
|
|
28
|
+
points: int
|
|
29
|
+
detail: str
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class AuditReport:
|
|
34
|
+
base_url: str
|
|
35
|
+
findings: list[Finding] = field(default_factory=list)
|
|
36
|
+
blocked_agents: list[str] = field(default_factory=list)
|
|
37
|
+
score: int = 0
|
|
38
|
+
|
|
39
|
+
def add(self, check: str, passed: bool, detail: str) -> None:
|
|
40
|
+
points = POINTS[check] if passed else 0
|
|
41
|
+
self.findings.append(Finding(check, passed, points, detail))
|
|
42
|
+
self.score += points
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def run_audit(base_url: str) -> AuditReport:
|
|
46
|
+
base_url = base_url.rstrip("/")
|
|
47
|
+
if not base_url.lower().startswith("https://"):
|
|
48
|
+
raise ValueError(
|
|
49
|
+
f"Refusing to audit '{base_url}': only https:// URLs are supported. "
|
|
50
|
+
"This tool never fetches over plaintext HTTP, for both your safety "
|
|
51
|
+
"and the target site's."
|
|
52
|
+
)
|
|
53
|
+
report = AuditReport(base_url=base_url)
|
|
54
|
+
|
|
55
|
+
robots_res = fetch(urljoin(base_url + "/", "robots.txt"))
|
|
56
|
+
if not robots_res.ok:
|
|
57
|
+
report.add("robots_reachable", False, "robots.txt missing or unreachable — AI crawlers default to allowed, but you have no way to signal intent or steer them to a sitemap.")
|
|
58
|
+
groups = []
|
|
59
|
+
else:
|
|
60
|
+
report.add("robots_reachable", True, "robots.txt found.")
|
|
61
|
+
groups = parse_robots_txt(robots_res.text)
|
|
62
|
+
|
|
63
|
+
blocked = []
|
|
64
|
+
for agent in AI_CRAWLERS:
|
|
65
|
+
status = agent_status(groups, agent)
|
|
66
|
+
if status == "blocked":
|
|
67
|
+
blocked.append(agent)
|
|
68
|
+
report.blocked_agents = blocked
|
|
69
|
+
if blocked:
|
|
70
|
+
report.add(
|
|
71
|
+
"no_ai_crawlers_blocked",
|
|
72
|
+
False,
|
|
73
|
+
f"{len(blocked)} AI crawler(s) explicitly blocked: {', '.join(blocked)}.",
|
|
74
|
+
)
|
|
75
|
+
else:
|
|
76
|
+
report.add("no_ai_crawlers_blocked", True, "No known AI crawler is explicitly blocked.")
|
|
77
|
+
|
|
78
|
+
llms_res = fetch(urljoin(base_url + "/", "llms.txt"))
|
|
79
|
+
report.add(
|
|
80
|
+
"llms_txt_present",
|
|
81
|
+
llms_res.ok and len(llms_res.text.strip()) > 0,
|
|
82
|
+
"llms.txt found." if llms_res.ok else "No /llms.txt — the emerging convention for telling an LLM what your site is and which pages matter most.",
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
sitemap_url = None
|
|
86
|
+
m = re.search(r"(?im)^sitemap:\s*(\S+)", robots_res.text) if robots_res.ok else None
|
|
87
|
+
if m:
|
|
88
|
+
sitemap_url = m.group(1).strip()
|
|
89
|
+
sitemap_res = fetch(sitemap_url or urljoin(base_url + "/", "sitemap.xml"))
|
|
90
|
+
sitemap_looks_valid = sitemap_res.ok and ("<urlset" in sitemap_res.text or "<sitemapindex" in sitemap_res.text)
|
|
91
|
+
report.add(
|
|
92
|
+
"sitemap_present",
|
|
93
|
+
sitemap_looks_valid,
|
|
94
|
+
"sitemap.xml found and looks valid." if sitemap_looks_valid else "No reachable/valid sitemap.xml — AI crawlers (and search engines) have no efficient way to discover your full page list.",
|
|
95
|
+
)
|
|
96
|
+
report.add(
|
|
97
|
+
"sitemap_referenced_in_robots",
|
|
98
|
+
m is not None,
|
|
99
|
+
"robots.txt points crawlers at your sitemap." if m else "robots.txt doesn't reference a Sitemap: line, even if sitemap.xml exists at the default path.",
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
home_res = fetch(base_url + "/")
|
|
103
|
+
has_jsonld = home_res.ok and bool(re.search(r'<script[^>]+type=["\']application/ld\+json["\']', home_res.text, re.I))
|
|
104
|
+
report.add(
|
|
105
|
+
"structured_data_present",
|
|
106
|
+
has_jsonld,
|
|
107
|
+
"JSON-LD structured data found on the homepage." if has_jsonld else "No JSON-LD structured data on the homepage — AI answer engines lean on schema.org markup to extract facts (org name, product, FAQ, etc.) reliably.",
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
return report
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Static SVG badge generator — a shields.io-style badge, generated locally
|
|
2
|
+
and hosted by whoever runs the tool (no server of ours required). This is
|
|
3
|
+
the zero-account distribution mechanic: a site that scores well has a
|
|
4
|
+
concrete, embeddable reason to link back, the same way "Deploys on Vercel"
|
|
5
|
+
or CI-passing badges spread.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
LABEL = "AI-visible"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _color_for(score: int) -> str:
|
|
13
|
+
if score >= 80:
|
|
14
|
+
return "#2E8B57" # allow-green
|
|
15
|
+
if score >= 50:
|
|
16
|
+
return "#E8A23A" # amber
|
|
17
|
+
return "#C6432E" # block-red
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _text_width(text: str) -> int:
|
|
21
|
+
# Rough monospace-ish estimate — good enough for a badge, no font
|
|
22
|
+
# metrics library required (this stays stdlib-only by design).
|
|
23
|
+
return max(6, int(len(text) * 6.7)) + 10
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def generate_badge_svg(score: int) -> str:
|
|
27
|
+
value_text = f"{score}/100"
|
|
28
|
+
label_text = LABEL
|
|
29
|
+
label_w = _text_width(label_text)
|
|
30
|
+
value_w = _text_width(value_text)
|
|
31
|
+
total_w = label_w + value_w
|
|
32
|
+
color = _color_for(score)
|
|
33
|
+
|
|
34
|
+
return f'''<svg xmlns="http://www.w3.org/2000/svg" width="{total_w}" height="20" role="img" aria-label="{label_text}: {value_text}">
|
|
35
|
+
<linearGradient id="s" x2="0" y2="100%">
|
|
36
|
+
<stop offset="0" stop-color="#fff" stop-opacity=".08"/>
|
|
37
|
+
<stop offset="1" stop-opacity=".08"/>
|
|
38
|
+
</linearGradient>
|
|
39
|
+
<clipPath id="r"><rect width="{total_w}" height="20" rx="3" fill="#fff"/></clipPath>
|
|
40
|
+
<g clip-path="url(#r)">
|
|
41
|
+
<rect width="{label_w}" height="20" fill="#3a3f3c"/>
|
|
42
|
+
<rect x="{label_w}" width="{value_w}" height="20" fill="{color}"/>
|
|
43
|
+
<rect width="{total_w}" height="20" fill="url(#s)"/>
|
|
44
|
+
</g>
|
|
45
|
+
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="11">
|
|
46
|
+
<text x="{label_w / 2}" y="14">{label_text}</text>
|
|
47
|
+
<text x="{label_w + value_w / 2}" y="14">{value_text}</text>
|
|
48
|
+
</g>
|
|
49
|
+
</svg>
|
|
50
|
+
'''
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
from urllib.parse import urljoin
|
|
6
|
+
|
|
7
|
+
from .audit import run_audit
|
|
8
|
+
from .badge import generate_badge_svg
|
|
9
|
+
from .fetch import fetch
|
|
10
|
+
from .fix import generate_llms_txt, generate_robots_patch
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def cmd_audit(args: argparse.Namespace) -> int:
|
|
14
|
+
try:
|
|
15
|
+
report = run_audit(args.url)
|
|
16
|
+
except ValueError as e:
|
|
17
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
18
|
+
return 2
|
|
19
|
+
print(f"\nAI-visibility audit for {report.base_url}")
|
|
20
|
+
print(f"Score: {report.score}/100\n")
|
|
21
|
+
for f in report.findings:
|
|
22
|
+
mark = "PASS" if f.passed else "FAIL"
|
|
23
|
+
print(f" [{mark}] {f.check}: {f.detail}")
|
|
24
|
+
if report.blocked_agents:
|
|
25
|
+
print(f"\nBlocked crawlers: {', '.join(report.blocked_agents)}")
|
|
26
|
+
print()
|
|
27
|
+
return 0 if report.score >= 70 else 1
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def cmd_fix(args: argparse.Namespace) -> int:
|
|
31
|
+
try:
|
|
32
|
+
report = run_audit(args.url)
|
|
33
|
+
except ValueError as e:
|
|
34
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
35
|
+
return 2
|
|
36
|
+
robots_res = fetch(urljoin(report.base_url + "/", "robots.txt"))
|
|
37
|
+
existing = robots_res.text if robots_res.ok else ""
|
|
38
|
+
|
|
39
|
+
patched_robots = generate_robots_patch(existing, report)
|
|
40
|
+
robots_out = args.out_robots or "robots.fixed.txt"
|
|
41
|
+
with open(robots_out, "w") as fh:
|
|
42
|
+
fh.write(patched_robots)
|
|
43
|
+
print(f"Wrote {robots_out}")
|
|
44
|
+
|
|
45
|
+
if not any(f.check == "llms_txt_present" and f.passed for f in report.findings):
|
|
46
|
+
llms_out = args.out_llms or "llms.txt"
|
|
47
|
+
llms_txt = generate_llms_txt(
|
|
48
|
+
site_name=args.name or report.base_url,
|
|
49
|
+
site_description=args.description or f"{args.name or report.base_url} — description needed, edit this file.",
|
|
50
|
+
key_pages=[],
|
|
51
|
+
)
|
|
52
|
+
with open(llms_out, "w") as fh:
|
|
53
|
+
fh.write(llms_txt)
|
|
54
|
+
print(f"Wrote {llms_out} (starter — edit key pages in before publishing)")
|
|
55
|
+
|
|
56
|
+
print("\nReview both files, then deploy them at your site root. Nothing was auto-published.")
|
|
57
|
+
return 0
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def cmd_badge(args: argparse.Namespace) -> int:
|
|
61
|
+
try:
|
|
62
|
+
report = run_audit(args.url)
|
|
63
|
+
except ValueError as e:
|
|
64
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
65
|
+
return 2
|
|
66
|
+
svg = generate_badge_svg(report.score)
|
|
67
|
+
out = args.out or "aivisible-badge.svg"
|
|
68
|
+
with open(out, "w") as fh:
|
|
69
|
+
fh.write(svg)
|
|
70
|
+
print(f"Wrote {out} (score {report.score}/100). Host it yourself and embed:")
|
|
71
|
+
print(f' <a href="https://example.com"><img src="/aivisible-badge.svg" alt="AI-visible: {report.score}/100"></a>')
|
|
72
|
+
return 0
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def main(argv: list[str] | None = None) -> int:
|
|
76
|
+
parser = argparse.ArgumentParser(prog="aivisible-fix", description="Audit and fix a site's visibility to AI crawlers/answer engines.")
|
|
77
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
78
|
+
|
|
79
|
+
p_audit = sub.add_parser("audit", help="Score a site's AI-crawler visibility")
|
|
80
|
+
p_audit.add_argument("url", help="e.g. https://example.com")
|
|
81
|
+
p_audit.set_defaults(func=cmd_audit)
|
|
82
|
+
|
|
83
|
+
p_fix = sub.add_parser("fix", help="Generate a patched robots.txt and starter llms.txt")
|
|
84
|
+
p_fix.add_argument("url")
|
|
85
|
+
p_fix.add_argument("--name", help="Site/company name for llms.txt")
|
|
86
|
+
p_fix.add_argument("--description", help="One-line description for llms.txt")
|
|
87
|
+
p_fix.add_argument("--out-robots", help="Output path for patched robots.txt")
|
|
88
|
+
p_fix.add_argument("--out-llms", help="Output path for generated llms.txt")
|
|
89
|
+
p_fix.set_defaults(func=cmd_fix)
|
|
90
|
+
|
|
91
|
+
p_badge = sub.add_parser("badge", help="Generate a static embeddable score badge (SVG)")
|
|
92
|
+
p_badge.add_argument("url")
|
|
93
|
+
p_badge.add_argument("--out", help="Output path for the badge SVG")
|
|
94
|
+
p_badge.set_defaults(func=cmd_badge)
|
|
95
|
+
|
|
96
|
+
args = parser.parse_args(argv)
|
|
97
|
+
return args.func(args)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
if __name__ == "__main__":
|
|
101
|
+
sys.exit(main())
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Known AI crawler / agent user-agent tokens, as used in robots.txt.
|
|
2
|
+
# Kept as a single source of truth so audit.py and fix.py never drift apart.
|
|
3
|
+
# Sources: each vendor's own published crawler docs (OpenAI, Anthropic,
|
|
4
|
+
# Perplexity, Google, Common Crawl, ByteDance, Amazon, Apple, Cohere, Meta).
|
|
5
|
+
|
|
6
|
+
AI_CRAWLERS = {
|
|
7
|
+
"GPTBot": "OpenAI — trains/improves models on crawled content",
|
|
8
|
+
"ChatGPT-User": "OpenAI — fetches pages a ChatGPT user links to",
|
|
9
|
+
"OAI-SearchBot": "OpenAI — powers ChatGPT search result citations",
|
|
10
|
+
"ClaudeBot": "Anthropic — trains/improves Claude on crawled content",
|
|
11
|
+
"Claude-Web": "Anthropic — fetches pages referenced in a Claude chat",
|
|
12
|
+
"anthropic-ai": "Anthropic — legacy training crawler token",
|
|
13
|
+
"PerplexityBot": "Perplexity — indexes content for AI search answers",
|
|
14
|
+
"Perplexity-User": "Perplexity — fetches pages a user asked about",
|
|
15
|
+
"Google-Extended": "Google — controls use in Gemini / AI Overviews (separate from Googlebot)",
|
|
16
|
+
"CCBot": "Common Crawl — public dataset used to train many LLMs",
|
|
17
|
+
"Bytespider": "ByteDance — crawls for AI training",
|
|
18
|
+
"Amazonbot": "Amazon — crawls for Alexa/AI features",
|
|
19
|
+
"Applebot-Extended": "Apple — controls use in Apple Intelligence (separate from Applebot)",
|
|
20
|
+
"cohere-ai": "Cohere — trains/improves models on crawled content",
|
|
21
|
+
"Meta-ExternalAgent": "Meta — crawls for AI training and search features",
|
|
22
|
+
"Diffbot": "Diffbot — powers third-party AI/knowledge-graph products",
|
|
23
|
+
"YouBot": "You.com — indexes content for AI search answers",
|
|
24
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Tiny stdlib-only HTTP helper — no third-party deps required to run this tool."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import urllib.error
|
|
5
|
+
import urllib.request
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
USER_AGENT = "aivisible-fix/0.1 (+https://github.com/; audit tool, respects robots.txt itself)"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class FetchResult:
|
|
13
|
+
ok: bool
|
|
14
|
+
status: int | None
|
|
15
|
+
text: str
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def fetch(url: str, timeout: float = 10.0) -> FetchResult:
|
|
19
|
+
if not url.lower().startswith("https://"):
|
|
20
|
+
# Refuse plaintext HTTP: this tool fetches third-party sites and must
|
|
21
|
+
# not be usable to relay/downgrade requests over an unencrypted
|
|
22
|
+
# channel, and it never has a legitimate reason to fetch anything
|
|
23
|
+
# other than a public https:// URL.
|
|
24
|
+
return FetchResult(ok=False, status=None, text="")
|
|
25
|
+
req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
|
|
26
|
+
try:
|
|
27
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
28
|
+
body = resp.read().decode("utf-8", errors="replace")
|
|
29
|
+
return FetchResult(ok=200 <= resp.status < 300, status=resp.status, text=body)
|
|
30
|
+
except urllib.error.HTTPError as e:
|
|
31
|
+
return FetchResult(ok=False, status=e.code, text="")
|
|
32
|
+
except (urllib.error.URLError, TimeoutError, OSError):
|
|
33
|
+
return FetchResult(ok=False, status=None, text="")
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .audit import AuditReport
|
|
4
|
+
from .crawlers import AI_CRAWLERS
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def generate_robots_patch(existing_robots_text: str, report: AuditReport) -> str:
|
|
8
|
+
"""Return a full replacement robots.txt: keeps every line the site already
|
|
9
|
+
had, and appends an explicit Allow-all group for each blocked AI crawler.
|
|
10
|
+
Non-destructive by construction — nothing existing is deleted or edited.
|
|
11
|
+
"""
|
|
12
|
+
if not report.blocked_agents:
|
|
13
|
+
return existing_robots_text
|
|
14
|
+
|
|
15
|
+
lines = existing_robots_text.rstrip("\n").splitlines() if existing_robots_text.strip() else []
|
|
16
|
+
lines.append("")
|
|
17
|
+
lines.append("# --- Added by aivisible-fix: explicitly allow AI crawlers ---")
|
|
18
|
+
lines.append("# Remove any of these blocks if you deliberately don't want that")
|
|
19
|
+
lines.append("# crawler training on / citing your content.")
|
|
20
|
+
for agent in report.blocked_agents:
|
|
21
|
+
lines.append("")
|
|
22
|
+
lines.append(f"User-agent: {agent}")
|
|
23
|
+
lines.append("Allow: /")
|
|
24
|
+
return "\n".join(lines) + "\n"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def generate_llms_txt(site_name: str, site_description: str, key_pages: list[tuple[str, str]]) -> str:
|
|
28
|
+
"""key_pages: list of (title, url) tuples for the handful of pages worth
|
|
29
|
+
pointing an LLM at directly (docs, pricing, about) — per the llms.txt spec
|
|
30
|
+
(llmstxt.org): an H1, a one-line summary, then link sections.
|
|
31
|
+
"""
|
|
32
|
+
lines = [f"# {site_name}", "", f"> {site_description}", ""]
|
|
33
|
+
if key_pages:
|
|
34
|
+
lines.append("## Key pages")
|
|
35
|
+
for title, url in key_pages:
|
|
36
|
+
lines.append(f"- [{title}]({url})")
|
|
37
|
+
lines.append("")
|
|
38
|
+
lines.append(
|
|
39
|
+
"<!-- Generated by aivisible-fix. Edit freely — this is a starting point, "
|
|
40
|
+
"not a final file. Keep it short: link out rather than duplicating content. -->"
|
|
41
|
+
)
|
|
42
|
+
return "\n".join(lines) + "\n"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def suggest_jsonld_snippet(site_name: str, site_url: str, description: str) -> str:
|
|
46
|
+
return f"""<script type="application/ld+json">
|
|
47
|
+
{{
|
|
48
|
+
"@context": "https://schema.org",
|
|
49
|
+
"@type": "Organization",
|
|
50
|
+
"name": "{site_name}",
|
|
51
|
+
"url": "{site_url}",
|
|
52
|
+
"description": "{description}"
|
|
53
|
+
}}
|
|
54
|
+
</script>"""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def summarize_unknown_crawler_coverage() -> str:
|
|
58
|
+
return (
|
|
59
|
+
f"This tool checks {len(AI_CRAWLERS)} known AI crawler user-agents. "
|
|
60
|
+
"New ones ship periodically — re-run the audit monthly rather than treating "
|
|
61
|
+
"a clean report as permanent."
|
|
62
|
+
)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Minimal robots.txt parser that groups directives by user-agent.
|
|
2
|
+
|
|
3
|
+
Deliberately not using urllib.robotparser: that API answers "can THIS
|
|
4
|
+
one agent fetch THIS one URL", but an audit needs to enumerate, for every
|
|
5
|
+
known AI crawler, whether it has its own group and whether that group (or
|
|
6
|
+
the wildcard group) disallows the whole site.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class Group:
|
|
15
|
+
agents: list[str] = field(default_factory=list)
|
|
16
|
+
disallow: list[str] = field(default_factory=list)
|
|
17
|
+
allow: list[str] = field(default_factory=list)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def parse_robots_txt(text: str) -> list[Group]:
|
|
21
|
+
groups: list[Group] = []
|
|
22
|
+
current: Group | None = None
|
|
23
|
+
seen_directive_since_agent = True # forces first "User-agent" to start a group
|
|
24
|
+
|
|
25
|
+
for raw_line in text.splitlines():
|
|
26
|
+
line = raw_line.split("#", 1)[0].strip()
|
|
27
|
+
if not line or ":" not in line:
|
|
28
|
+
continue
|
|
29
|
+
field_name, _, value = line.partition(":")
|
|
30
|
+
field_name = field_name.strip().lower()
|
|
31
|
+
value = value.strip()
|
|
32
|
+
|
|
33
|
+
if field_name == "user-agent":
|
|
34
|
+
if current is None or not seen_directive_since_agent:
|
|
35
|
+
current = Group()
|
|
36
|
+
groups.append(current)
|
|
37
|
+
current.agents.append(value)
|
|
38
|
+
seen_directive_since_agent = False
|
|
39
|
+
elif field_name == "disallow" and current is not None:
|
|
40
|
+
current.disallow.append(value)
|
|
41
|
+
seen_directive_since_agent = True
|
|
42
|
+
elif field_name == "allow" and current is not None:
|
|
43
|
+
current.allow.append(value)
|
|
44
|
+
seen_directive_since_agent = True
|
|
45
|
+
# sitemap / crawl-delay / other fields intentionally ignored here;
|
|
46
|
+
# sitemap presence is checked separately by audit.py.
|
|
47
|
+
|
|
48
|
+
return groups
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def blocks_everything(group: Group) -> bool:
|
|
52
|
+
"""True if this group disallows the whole site and doesn't carve out
|
|
53
|
+
an override Allow for it (Allow: / or Allow: with empty/root path)."""
|
|
54
|
+
has_blanket_disallow = any(d.strip() == "/" for d in group.disallow)
|
|
55
|
+
if not has_blanket_disallow:
|
|
56
|
+
return False
|
|
57
|
+
has_override_allow = any(a.strip() == "/" for a in group.allow)
|
|
58
|
+
return not has_override_allow
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def agent_status(groups: list[Group], agent: str) -> str:
|
|
62
|
+
"""Return 'blocked', 'allowed-explicit', or 'allowed-default' for one AI agent.
|
|
63
|
+
|
|
64
|
+
'allowed-default' means the site has no group naming this agent and no
|
|
65
|
+
wildcard (*) group blocking everything — the crawler is free to fetch
|
|
66
|
+
under robots.txt's default-allow behaviour, but the site owner never
|
|
67
|
+
made an explicit decision either way.
|
|
68
|
+
"""
|
|
69
|
+
agent_lower = agent.lower()
|
|
70
|
+
named_group = next(
|
|
71
|
+
(g for g in groups if any(a.lower() == agent_lower for a in g.agents)),
|
|
72
|
+
None,
|
|
73
|
+
)
|
|
74
|
+
if named_group is not None:
|
|
75
|
+
return "blocked" if blocks_everything(named_group) else "allowed-explicit"
|
|
76
|
+
|
|
77
|
+
wildcard_group = next((g for g in groups if "*" in g.agents), None)
|
|
78
|
+
if wildcard_group is not None and blocks_everything(wildcard_group):
|
|
79
|
+
return "blocked"
|
|
80
|
+
return "allowed-default"
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aivisible-fix
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Audit and auto-fix a website's visibility to AI crawlers and answer engines (GPTBot, ClaudeBot, PerplexityBot, and more).
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Dynamic: license-file
|
|
10
|
+
|
|
11
|
+
# aivisible-fix
|
|
12
|
+
|
|
13
|
+
Audits a website for the specific things that make it invisible to AI
|
|
14
|
+
crawlers and AI answer engines (ChatGPT, Claude, Perplexity, Gemini/AI
|
|
15
|
+
Overviews, and others) — and, unlike the free "checker" tools already on
|
|
16
|
+
the market, **generates the fix**, not just a score.
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
$ aivisible-fix audit https://example.com
|
|
20
|
+
AI-visibility audit for https://example.com
|
|
21
|
+
Score: 40/100
|
|
22
|
+
|
|
23
|
+
[FAIL] robots_reachable: robots.txt missing or unreachable ...
|
|
24
|
+
[PASS] no_ai_crawlers_blocked: No known AI crawler is explicitly blocked.
|
|
25
|
+
[FAIL] llms_txt_present: No /llms.txt ...
|
|
26
|
+
...
|
|
27
|
+
|
|
28
|
+
$ aivisible-fix fix https://example.com --name "Example Co" --description "..."
|
|
29
|
+
Wrote robots.fixed.txt
|
|
30
|
+
Wrote llms.txt
|
|
31
|
+
Review both files, then deploy them at your site root. Nothing was auto-published.
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## What it checks
|
|
35
|
+
|
|
36
|
+
1. **robots.txt reachable** — the baseline signal every crawler reads first.
|
|
37
|
+
2. **No AI crawler is explicitly blocked** — checked per-agent (GPTBot,
|
|
38
|
+
ChatGPT-User, OAI-SearchBot, ClaudeBot, Claude-Web, anthropic-ai,
|
|
39
|
+
PerplexityBot, Perplexity-User, Google-Extended, CCBot, Bytespider,
|
|
40
|
+
Amazonbot, Applebot-Extended, cohere-ai, Meta-ExternalAgent, Diffbot,
|
|
41
|
+
YouBot — see `aivisible_fix/crawlers.py`), not just a blanket `Disallow: /`.
|
|
42
|
+
3. **llms.txt present** — the emerging convention (llmstxt.org) for a short,
|
|
43
|
+
LLM-readable summary of what a site is and which pages matter.
|
|
44
|
+
4. **sitemap.xml present and valid**, and **referenced from robots.txt**.
|
|
45
|
+
5. **JSON-LD structured data on the homepage** — what answer engines lean on
|
|
46
|
+
to extract facts (org, product, FAQ, etc.) without guessing from prose.
|
|
47
|
+
|
|
48
|
+
Score is out of 100, weighted toward the two things that, per current
|
|
49
|
+
published research, cause the overwhelming majority of AI-search invisibility:
|
|
50
|
+
blocked crawlers (40 pts) and missing discovery trail — sitemap + llms.txt
|
|
51
|
+
(40 pts combined).
|
|
52
|
+
|
|
53
|
+
## Why this instead of the free checkers
|
|
54
|
+
|
|
55
|
+
Search "AI visibility checker" and you'll find eight-plus free tools
|
|
56
|
+
(isvisible.ai, llmpulse.ai, siftly.ai, and others) that already do the
|
|
57
|
+
scoring for free — that part of the market is a commodity and not worth
|
|
58
|
+
competing on head to head.
|
|
59
|
+
|
|
60
|
+
None of them **write the fix**. This tool's differentiator is that
|
|
61
|
+
`aivisible-fix fix` outputs a ready-to-deploy patched `robots.txt` (every
|
|
62
|
+
existing line preserved, blocked AI agents explicitly re-allowed) and a
|
|
63
|
+
starter `llms.txt` — output, not just diagnosis. The natural extension
|
|
64
|
+
(not built yet — see ROADMAP below) is a GitHub App that opens this as a
|
|
65
|
+
pull request automatically, the way Dependabot does for dependency bumps,
|
|
66
|
+
plus scheduled re-audits so a clean report doesn't silently rot as new
|
|
67
|
+
crawlers ship.
|
|
68
|
+
|
|
69
|
+
## Badge
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
$ aivisible-fix badge https://example.com
|
|
73
|
+
Wrote aivisible-badge.svg (score 40/100). Host it yourself and embed:
|
|
74
|
+
<a href="https://example.com"><img src="/aivisible-badge.svg" alt="AI-visible: 40/100"></a>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Static SVG, generated locally — no server or account required. Color tracks
|
|
78
|
+
score (red under 50, amber under 80, green at 80+).
|
|
79
|
+
|
|
80
|
+
## Install
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
pip install -e .
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
No dependencies beyond the Python 3.10+ standard library — deliberately, so
|
|
87
|
+
it's trivial to also ship as a single-file GitHub Action.
|
|
88
|
+
|
|
89
|
+
## Status
|
|
90
|
+
|
|
91
|
+
v0.1 — functional CLI, tested against live sites (see commit history / this
|
|
92
|
+
was built and smoke-tested against example.com and nytimes.com, correctly
|
|
93
|
+
detecting NYT's well-documented blanket block of AI crawlers). Not yet
|
|
94
|
+
published to PyPI or packaged as a GitHub Action — see BUSINESS_PLAN.md for
|
|
95
|
+
what's next and what needs a human to do it (PyPI account, GitHub App
|
|
96
|
+
registration, a payment processor for the paid tier).
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
aivisible_fix/__init__.py
|
|
5
|
+
aivisible_fix/audit.py
|
|
6
|
+
aivisible_fix/badge.py
|
|
7
|
+
aivisible_fix/cli.py
|
|
8
|
+
aivisible_fix/crawlers.py
|
|
9
|
+
aivisible_fix/fetch.py
|
|
10
|
+
aivisible_fix/fix.py
|
|
11
|
+
aivisible_fix/robots.py
|
|
12
|
+
aivisible_fix.egg-info/PKG-INFO
|
|
13
|
+
aivisible_fix.egg-info/SOURCES.txt
|
|
14
|
+
aivisible_fix.egg-info/dependency_links.txt
|
|
15
|
+
aivisible_fix.egg-info/entry_points.txt
|
|
16
|
+
aivisible_fix.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
aivisible_fix
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "aivisible-fix"
|
|
3
|
+
version = "0.2.0"
|
|
4
|
+
description = "Audit and auto-fix a website's visibility to AI crawlers and answer engines (GPTBot, ClaudeBot, PerplexityBot, and more)."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
license = { text = "MIT" }
|
|
8
|
+
dependencies = []
|
|
9
|
+
|
|
10
|
+
[project.scripts]
|
|
11
|
+
aivisible-fix = "aivisible_fix.cli:main"
|
|
12
|
+
|
|
13
|
+
[build-system]
|
|
14
|
+
requires = ["setuptools>=68"]
|
|
15
|
+
build-backend = "setuptools.build_meta"
|