From 14147eb41bd2fdd98b951be7f1a7865c0c49c72c Mon Sep 17 00:00:00 2001 From: Ed_ Date: Mon, 24 Aug 2026 10:07:43 -0400 Subject: [PATCH] MAYBE. --- .github/workflows/deploy.yml | 9 +- publish/quartz-patches/apply.py | 157 +++++++++++++++++---- publish/quartz-patches/patch-title-link.py | 65 --------- 3 files changed, 131 insertions(+), 100 deletions(-) delete mode 100644 publish/quartz-patches/patch-title-link.py diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index d14f027..9701933 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -31,7 +31,7 @@ jobs: path: site - name: Apply SPA basepath patch working-directory: site - run: python ../publish/quartz-patches/apply.py . + run: python ../publish/quartz-patches/apply.py spa - name: Apply publish overlay run: cp publish/quartz.config.yaml site/quartz.config.yaml - uses: actions/setup-node@v6 @@ -49,8 +49,6 @@ jobs: with: path: site/.quartz/plugins key: ${{ runner.os }}-plugins-${{ hashFiles('site/package-lock.json') }} - restore-keys: | - ${{ runner.os }}-plugins- - name: Install Dependencies working-directory: site run: npm ci @@ -60,9 +58,8 @@ jobs: - name: Build Quartz working-directory: site run: npx quartz build -d .. - - name: Patch sidebar title link - working-directory: site - run: python ../publish/quartz-patches/patch-title-link.py + - name: Rewrite built HTML + run: python ../publish/quartz-patches/apply.py built - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: diff --git a/publish/quartz-patches/apply.py b/publish/quartz-patches/apply.py index 4c8ab11..89678a5 100644 --- a/publish/quartz-patches/apply.py +++ b/publish/quartz-patches/apply.py @@ -1,35 +1,78 @@ -"""Patch quartz spa.inline.ts so relative URL clicks resolve from a directory base. +"""Single source of truth for the Threadwell site base. -Quartz's SPA click handler calls `new URL(href)`. When the URL bar is missing -a trailing slash, the browser treats the last path segment as a file, so -`../../../../tags/archive` from `/Threadwell/x/y/` (no slash) resolves to -`/Threadwell/x/tags/archive` instead of `/Threadwell/tags/archive`. SPA routing -loses the `/Threadwell/` base path on GitHub Pages project deployments. +Two stages: -Fix: compute `SPA_BASE_HREF` from `window.location`, always ending with `/`, -and resolve relative URLs against it inside the SPA click handler. +1. Apply the SPA basepath patch to quartz/components/scripts/spa.inline.ts so + relative URLs in the SPA click handler resolve from a directory base. + +2. After `npx quartz build`, rewrite every built HTML file: + - add `` so relative URL resolution works even when + the URL bar lacks a trailing slash + - rewrite the sidebar `` href to the absolute site + base, not the directory the page is in + +The base URL is read from `publish/quartz.config.yaml`'s `baseUrl`. Change +that line; the next deploy uses the new value everywhere. Idempotent. Safe to re-run. - -Path resolution is absolute: the script lives at -`/publish/quartz-patches/apply.py` and writes to -`/site/quartz/components/scripts/spa.inline.ts`. """ from __future__ import annotations +import re +import sys from pathlib import Path SCRIPT = Path(__file__).resolve() VAULT = SCRIPT.parents[2] -SPA_FILE = ( - VAULT - / "site" - / "quartz" - / "components" - / "scripts" - / "spa.inline.ts" +QUARTZ_REPO = VAULT / "site" / "quartz" +SPA_FILE = QUARTZ_REPO / "components" / "scripts" / "spa.inline.ts" +PUBLIC = VAULT / "site" / "public" +OVERLAY = VAULT / "publish" / "quartz.config.yaml" + + +def read_base_url() -> tuple[str, str]: + """Read `baseUrl` from the overlay (YAML) as the single source of truth. + + Falls back to lenient line scan if YAML parsing isn't available. + """ + text = OVERLAY.read_text(encoding="utf-8") + host = "" + for line in text.splitlines(): + stripped = line.strip() + if stripped.startswith("#") or not stripped: + continue + if stripped.startswith("baseUrl:"): + host = stripped.split(":", 1)[1].strip() + break + if not host or "/" not in host: + raise SystemExit(f"missing baseUrl in {OVERLAY}") + if host.startswith("http://"): + scheme = "http://" + host = host[len("http://") :] + elif host.startswith("https://"): + scheme = "https://" + host = host[len("https://") :] + else: + scheme = "https://" + domain, _, path = host.partition("/") + abs_base = ( + f"{scheme}{domain}/{path.rstrip('/')}/" if path else f"{scheme}{domain}/" + ) + rel_base = "/" + path.rstrip("/") + "/" if path else "/" + return abs_base, rel_base + + +ABS_BASE, REL_BASE = read_base_url() +HEAD_RE = re.compile(r"(]*>)", re.IGNORECASE) +BASE_TAG = f'\n' +TITLE_LINK = re.compile( + r'(]*class="page-title">)' ) + +# --- SPA click-handler patch ---------------------------------------------- + + INSERT_AFTER = ( "const isElement = (target: EventTarget | null): target is Element =>\n" " (target as Node)?.nodeType === NODE_TYPE_ELEMENT" @@ -64,29 +107,85 @@ REPLACE_WITH = ( ) -def patch(path: Path) -> bool: - if not path.exists(): - print(f"missing {path}") +def patch_spa() -> bool: + if not SPA_FILE.exists(): + print(f"missing {SPA_FILE}") return False - text = path.read_text(encoding="utf-8") + text = SPA_FILE.read_text(encoding="utf-8") if "SPA_BASE_HREF" in text: - print(f"already patched {path}") + print(f"SPA already patched") return True if INSERT_AFTER not in text: - print(f"anchor missing in {path}") + print("SPA anchor missing; Quartz upstream changed?") return False if REPLACE_GETOPTS not in text: - print(f"getOpts block missing in {path}") + print("SPA getOpts missing") return False text = text.replace(INSERT_AFTER, INSERT_BLOCK + INSERT_AFTER, 1) text = text.replace(REPLACE_GETOPTS, REPLACE_WITH, 1) - path.write_text(text, encoding="utf-8") - print(f"patched {path}") + SPA_FILE.write_text(text, encoding="utf-8") + print("SPA patched") return True +# --- post-build HTML rewrites ---------------------------------------------- + + +def rewrite_html(path: Path) -> str | None: + """Return 'base' / 'title' / None for what changed.""" + text = path.read_text(encoding="utf-8", errors="ignore") + out = text + actions = [] + + m = HEAD_RE.search(out) + if m and "quartz-base" not in out: + out = out[: m.end()] + "\n" + BASE_TAG + out[m.end():] + actions.append("base") + if 'class="page-title"' in out: + out2 = TITLE_LINK.sub(rf"\g<1>{ABS_BASE}\g<2>", out) + if out2 != out: + out = out2 + actions.append("title") + + if out != text: + path.write_text(out, encoding="utf-8") + return ",".join(actions) if actions else None + + +def rewrite_built_htmls() -> tuple[int, int, int]: + if not PUBLIC.is_dir(): + print(f"missing {PUBLIC} (run after npx quartz build)") + return 0, 0, 0 + base_count = title_count = total = 0 + for path in PUBLIC.rglob("*.html"): + total += 1 + result = rewrite_html(path) + if result: + if "base" in result: + base_count += 1 + if "title" in result: + title_count += 1 + return base_count, title_count, total + + +# --- entry point ---------------------------------------------------------- + + def main() -> int: - return 0 if patch(SPA_FILE) else 1 + mode = sys.argv[1] if len(sys.argv) > 1 else "all" + + if mode in ("spa", "all"): + ok = patch_spa() + if not ok and mode == "spa": + return 1 + + if mode in ("built", "all"): + b, t, total = rewrite_built_htmls() + print(f"base tag: {b}, title link: {t}, total {total}") + print(f"absolute base: {ABS_BASE}") + print(f"relative base: {REL_BASE}") + + return 0 if __name__ == "__main__": diff --git a/publish/quartz-patches/patch-title-link.py b/publish/quartz-patches/patch-title-link.py deleted file mode 100644 index cef9e25..0000000 --- a/publish/quartz-patches/patch-title-link.py +++ /dev/null @@ -1,65 +0,0 @@ -"""Patch all built HTML files so the sidebar title link goes to /Threadwell/. - -The `

` link is generated by the -`@quartz-community/page-title` plugin from a relative href that breaks -when the URL bar lacks a trailing slash. We rewrite the href to the -absolute site base path so it never lands on the bare domain. - -Idempotent. Safe to re-run. -""" -from __future__ import annotations - -import re -import sys -from pathlib import Path - -SCRIPT = Path(__file__).resolve() -VAULT = SCRIPT.parents[2] - -# Detect base path from the publish overlay if present, else default to /Threadwell/ -overlay = VAULT / "publish" / "quartz.config.yaml" -site_base = "/Threadwell/" -if overlay.is_file(): - for line in overlay.read_text(encoding="utf-8").splitlines(): - if line.startswith("baseUrl:"): - host = line.split(":", 1)[1].strip() - if "/" in host: - site_base = "/" + host.split("/", 1)[1].rstrip("/") + "/" - else: - site_base = "/" - break - -PUBLIC = VAULT / "site" / "public" - -PATTERN = re.compile( - r'()' -) -REPLACEMENT = rf"\1{site_base}\2" - - -def patch_html(path: Path) -> bool: - text = path.read_text(encoding="utf-8", errors="ignore") - new = PATTERN.sub(REPLACEMENT, text) - if new != text: - path.write_text(new, encoding="utf-8") - return True - return False - - -def main() -> int: - if not PUBLIC.is_dir(): - print(f"missing {PUBLIC}") - return 1 - changed = 0 - total = 0 - for path in PUBLIC.rglob("*.html"): - total += 1 - if patch_html(path): - changed += 1 - print(f"site base: {site_base}") - print(f"total {total}, changed {changed}") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main())