#!/usr/bin/env python3
"""Turn an ai_audit.py JSON result into a printable HTML report (then print to PDF with any browser:
google-chrome --headless --print-to-pdf=report.pdf report.html). Standard library only."""
import html, json, sys
e = lambda x: html.escape(str(x if x is not None else ""))
R = json.load(open(sys.argv[1])); out = sys.argv[2]
s = R["score"]
names = {"robots_access": "AI crawler access (robots.txt)", "live_fetch": "Live fetch as each bot",
"no_js_content": "Content visible without JavaScript", "structured_data": "Structured data (schema)",
"meta_basics": "Title, description, canonical, H1, indexable", "sitemap": "Sitemap", "citation_readiness": "Citation readiness"}
col = {"high": "#b42318", "medium": "#b54708", "low": "#475467", "info": "#175cd3", "good": "#067647"}
lab = {"high": "HIGH", "medium": "MEDIUM", "low": "LOW", "info": "INFO", "good": "OK"}
parts = "".join(f"
| {e(names[k])} | {v:g} / {s['part_max'][k]} |
" for k, v in s["parts"].items())
finds = "".join(f"{lab[f['severity']]} {e(f['area'])}: {e(f['text'])}" for f in R["findings"])
rows = ""
for a in R["agents"]:
rob = "allowed" if a["robots_allowed"] else "BLOCKED " + e(a["robots_rule"])
live = "token only" if a["fetch_ok"] is None else f"{e(a['http_status'] or a.get('error'))} {'OK' if a['fetch_ok'] else 'PROBLEM'}"
rows += f"| {e(a['agent'])} | {e(a['role'])} | {rob} | {live} | {e(a['bytes'] or '')} |
"
c, m, sc = R["content"], R["meta"], R["schema"]
det = [("Words in raw HTML (no JavaScript)", f"{c['raw_html_words']:,}"), ("Words inside <main>/<article>", f"{c['main_words']:,}"),
("Title", m["title"]), ("Meta description", f"{m['description']} ({m['description_len']} chars)"), ("Canonical", m["canonical"]),
("H1", " | ".join(m["h1"])), ("Question-style headings", m["question_heading_count"]),
("JSON-LD types", ", ".join(sc["types"]) or "none"), ("Sitemap", f"{R['sitemap']['url']} (HTTP {R['sitemap']['status']}, {R['sitemap']['locs']} URLs)"),
("llms.txt (not scored)", f"present, {R['llms_txt']['bytes']:,} bytes" if R["llms_txt"]["present"] else "not present")]
dets = "".join(f"| {k} | {e(v) if not str(v).startswith(('Words',)) else v} |
" for k, v in det)
doc = f"""AI visibility audit: {e(R['url'])}
Sample report · AI visibility audit · astorventuresllc.com/ai-visibility-audit
Can ChatGPT, Claude and Perplexity read {e(R['url'])}?
Checked {e(R['checked_at_utc'])} (UTC) with {e(R['tool'])}
{s['total']}/100
Grade {s['grade']}
Technical readability for AI crawlers. This is a diagnosis, not a promise that any assistant will cite or recommend the site.
Findings, most important first
Crawler by crawler
| Crawler | What it does | robots.txt | Live fetch | Bytes |
{rows}
Page details
Limits
- Requests came from one IP with copied user-agent strings. Real crawlers use their own IP ranges; a CDN or firewall can treat them differently.
- Only this one URL was analysed for content. Other pages can differ.
- Google-Extended and Applebot-Extended are robots.txt tokens only; they never fetch pages.
- llms.txt is reported but not scored: no major AI crawler has documented that it reads it.
"""
open(out, "w").write(doc)