master-zapier-plan-draft/scripts/check-doc-links.py
George Lambert 85b5b53ea9
Some checks are pending
offline / test (push) Waiting to run
Fix catalog book links and rewrite PDF destinations to live HTTPS.
Module and model book cards now point at generated index.html/index.pdf
instead of directory URLs that 404. WeasyPrint file:// annotations are
mapped to https://zapier.georgelambert.org/ paths that exist. Developer
module, architecture, and research snapshot links target published files.
2026-09-11 22:50:00 -04:00

111 lines
3.4 KiB
Python

#!/usr/bin/env python3
"""Check catalog homepage + PDF URI targets after a site build."""
from __future__ import annotations
import sys
from html.parser import HTMLParser
from pathlib import Path
from urllib.parse import unquote, urljoin, urlparse
ROOT = Path(__file__).resolve().parents[1]
SITE = ROOT / "site"
WEB = "https://zapier.georgelambert.org/"
class Anchors(HTMLParser):
def __init__(self) -> None:
super().__init__()
self.hrefs: list[str] = []
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
if tag == "a":
d = dict(attrs)
if d.get("href"):
self.hrefs.append(d["href"] or "")
def local_ok(url: str) -> bool:
if url.startswith(WEB):
rel = url[len(WEB) :].split("#", 1)[0]
dest = SITE / rel
if dest.is_file():
return True
if dest.is_dir() and any(
(dest / n).exists() for n in ("index.html", "index.pdf", "README.html", "README.pdf")
):
return True
# yaml/svg/png on homepage
return False
if url.startswith("file:"):
return False
return True # http(s) off-site; homepage checker uses GET separately
def main() -> int:
idx = SITE / "index.html"
if not idx.exists():
print("no site/index.html — run scripts/build-docs-site.py")
return 1
p = Anchors()
p.feed(idx.read_text(encoding="utf-8", errors="replace"))
broken: list[str] = []
for h in sorted(set(p.hrefs)):
if h.startswith("#") or h.startswith("mailto:"):
continue
if h.startswith("https://git.georgelambert.org"):
continue
if h.startswith("https://") and not h.startswith(WEB):
continue
if h.startswith(WEB):
if not local_ok(h):
broken.append(f"homepage {h}")
continue
rel = h.lstrip("/")
dest = SITE / rel
if not dest.exists():
broken.append(f"homepage missing {h}")
from pypdf import PdfReader
n_file = 0
for pdf in SITE.rglob("*.pdf"):
try:
reader = PdfReader(str(pdf))
except Exception as exc:
broken.append(f"unreadable {pdf.relative_to(SITE)} {exc}")
continue
for page in reader.pages:
annots = page.get("/Annots")
if not annots:
continue
for annot in annots:
obj = annot.get_object()
action = obj.get("/A")
if not action:
continue
uri = action.get("/URI")
if not uri:
continue
raw = str(uri)
if raw.startswith("file:"):
n_file += 1
broken.append(f"file URI {pdf.relative_to(SITE)} -> {raw[:120]}")
continue
if raw.startswith(WEB):
path = raw.split("#", 1)[0]
if "/research/" in path:
continue
if not local_ok(path):
broken.append(f"pdf missing {pdf.relative_to(SITE)} -> {raw}")
print(f"file:// leftover {n_file}")
print(f"broken {len(broken)}")
for b in broken[:80]:
print(" ", b)
if len(broken) > 80:
print(f"{len(broken) - 80} more")
return 1 if broken else 0
if __name__ == "__main__":
sys.exit(main())