Make catalog PDF links host-relative and add a hostname rewrite script.
Some checks are pending
offline / test (push) Waiting to run

New builds write path-relative PDF annotations so the tree can be served
from docs.verae-time.net (or any host at the site root) without a rebuild.
scripts/rewrite-docs-host.py retargets an already-built site when absolute
URLs are preferred. Live service doors stay on their own hostnames.
This commit is contained in:
George Lambert 2026-09-11 23:00:12 -04:00
parent 85b5b53ea9
commit 20155de96b
4 changed files with 246 additions and 40 deletions

View file

@ -7,6 +7,7 @@ defaults to PDF links; index-md.html is the Markdown/HTML index.
from __future__ import annotations
import os
import shutil
import subprocess
from concurrent.futures import ThreadPoolExecutor, as_completed
@ -17,6 +18,11 @@ from urllib.parse import unquote
ROOT = Path(__file__).resolve().parents[1]
SITE = ROOT / "site"
CSS = ROOT / "scripts" / "docs-print.css"
# Display label only. Catalog file links are relative so the tree can move hosts.
DOCS_PUBLIC_HOST = os.environ.get("DOCS_PUBLIC_HOST", "zapier.georgelambert.org")
# If set (e.g. https://docs.verae-time.net), PDF annotations become absolute.
# Empty (default) writes path-relative URIs that work on any hostname at site root.
DOCS_PUBLIC_URL = os.environ.get("DOCS_PUBLIC_URL", "").rstrip("/")
SECTIONS = [
(
@ -180,7 +186,7 @@ def render_md(src: Path, html_dest: Path, pdf_dest: Path, title: str) -> str:
header.write_text(f"<style>{css_text}</style>\n", encoding="utf-8")
banner.write_text(
f'<div class="doc-banner">'
f'<nav class="site"><a href="/">zapier.georgelambert.org</a>'
f'<nav class="site"><a href="/">{DOCS_PUBLIC_HOST}</a>'
f' · <a href="/index-md.html">Markdown indexes</a></nav>'
f'<div class="kicker">Verae Time × Zapier</div>'
f"<h1>{title}</h1>"
@ -318,45 +324,68 @@ def _web_target(site: Path, rel: str, index: dict[str, list[str]] | None = None)
return rel
_DOCS_ORIGINS = (
"https://zapier.georgelambert.org/",
"http://zapier.georgelambert.org/",
)
def _href_for(pdf: Path, site: Path, target: str, frag: str = "") -> str:
target = (target or "index.html").lstrip("/") or "index.html"
dest = site / target
if DOCS_PUBLIC_URL:
return DOCS_PUBLIC_URL + "/" + target + frag
rel = os.path.relpath(dest.resolve(), pdf.parent.resolve())
return Path(rel).as_posix() + frag
def rewrite_pdf_uris(site: Path) -> None:
"""Turn WeasyPrint file:///… annotations into https://zapier.georgelambert.org/…"""
"""Turn WeasyPrint file:///… annotations into host-relative catalog paths.
Default is a path relative to the PDF (works on any hostname at the site
root). Set DOCS_PUBLIC_URL to emit absolute URLs instead.
"""
try:
from pypdf import PdfReader, PdfWriter
from pypdf.generic import NameObject, create_string_object
except ImportError:
print("pypdf missing; PDF URI rewrite skipped")
return
web = "https://zapier.georgelambert.org/"
prefix = site.resolve().as_uri().rstrip("/") + "/"
site = site.resolve()
prefix = site.as_uri().rstrip("/") + "/"
origins = list(_DOCS_ORIGINS)
if DOCS_PUBLIC_URL:
origins.append(DOCS_PUBLIC_URL.rstrip("/") + "/")
index = _file_index(site)
def resolve(raw: str) -> str:
def resolve(pdf: Path, raw: str) -> str:
frag = ""
if "#" in raw:
raw, frag = raw.split("#", 1)
frag = "#" + frag
mapped = raw
target = None
if raw.startswith(prefix):
mapped = web + _web_target(site, raw[len(prefix) :], index)
target = _web_target(site, raw[len(prefix) :], index)
elif raw.startswith("file:"):
name = unquote(raw.rsplit("/", 1)[-1] if "/" in raw else raw)
if not name or name in {".", "/", "file:"}:
mapped = web
target = "index.html"
else:
hit = _web_target(site, name, index)
if (site / hit).exists():
mapped = web + hit
target = hit
elif "index-md" in raw:
mapped = web + "index-md.html"
target = "index-md.html"
else:
mapped = web
elif raw.startswith(web):
hit = _web_target(site, raw[len(web) :], index)
if (site / hit).is_file() or (site / hit).is_dir():
mapped = web + hit
if mapped != raw:
return mapped + frag
return raw + frag
target = "index.html"
else:
for origin in origins:
if raw.startswith(origin):
target = _web_target(site, raw[len(origin) :], index)
break
if target is None:
return raw + frag
return _href_for(pdf, site, target, frag)
n_pdf = 0
n_fix = 0
@ -379,7 +408,7 @@ def rewrite_pdf_uris(site: Path) -> None:
if not uri:
continue
raw = str(uri)
new = resolve(raw)
new = resolve(pdf, raw)
if new != raw:
action[NameObject("/URI")] = create_string_object(new)
changed = True
@ -391,7 +420,8 @@ def rewrite_pdf_uris(site: Path) -> None:
writer.write(fh)
tmp.replace(pdf)
n_pdf += 1
print(f"rewrote {n_fix} PDF URIs in {n_pdf} files → {web}")
mode = DOCS_PUBLIC_URL + "/" if DOCS_PUBLIC_URL else "relative paths"
print(f"rewrote {n_fix} PDF URIs in {n_pdf} files → {mode}")
def convert_all_markdown(copied: list[tuple[Path, Path, str]]) -> list[str]:
@ -489,7 +519,7 @@ def page_shell(now: str, body: str, switch_to_md: bool, title: str) -> str:
<a class="skip" href="#main">Skip to content</a>
<header>
{switch}
<div class="kicker">zapier.georgelambert.org</div>
<div class="kicker">{DOCS_PUBLIC_HOST}</div>
<h1>Verae Time × Zapier documentation</h1>
<p>Project reference: setup, architecture, NATS, module APIs, user guide, and operator console.
Catalog defaults to <strong>colored PDFs</strong>. Generated {now}.</p>
@ -849,7 +879,7 @@ def main() -> None:
"Zapier /zapier/v1, jobs, archive, chain (MOCK_VERAE in lab)."),
("fleet.png", "https://fleet.zapier.georgelambert.org/", "Operator console",
"Fleet replicas, Trace, Docs. Pause/stop honored by keep."),
("docs.png", "https://zapier.georgelambert.org/", "This catalog",
("docs.png", "/", "This catalog",
"Colored PDF documentation site."),
("git.png", "https://git.georgelambert.org/", "Forgejo",
"Independent module repos (SSH 2223)."),