MAYBE.
This commit is contained in:
1 parent
866b5f7245
commit
14147eb41b
3 files changed
+131
-100
No files matched your search
@@ -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:
|
||||
|
||||
+128
-29
@@ -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 `<base href="<abs>">` so relative URL resolution works even when
|
||||
the URL bar lacks a trailing slash
|
||||
- rewrite the sidebar `<a class="page-title">` 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
|
||||
`<vault>/publish/quartz-patches/apply.py` and writes to
|
||||
`<vault>/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"(<head[^>]*>)", re.IGNORECASE)
|
||||
BASE_TAG = f'<base href="{ABS_BASE}">\n'
|
||||
TITLE_LINK = re.compile(
|
||||
r'(<h2[^>]*class="page-title"><a\s+href=")[^"]*(">)'
|
||||
)
|
||||
|
||||
|
||||
# --- 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__":
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
"""Patch all built HTML files so the sidebar title link goes to /Threadwell/.
|
||||
|
||||
The `<h2 class="page-title">` 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'(<h2\s+class="page-title"><a\s+href=")[^"]*(">)'
|
||||
)
|
||||
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())
|
||||
Reference in new issue
Block a user