build: replace ad-hoc run with a full make workflow + helper scripts
The old Makefile was a single 'run' target (hugo server + pagefind --serve). Replace it with documented targets: build hugo --gc -> public/ index build + pagefind --site public -> public/pagefind/ serve / run index + pagefind's own server on 127.0.0.1:1414 (local) serve-lan static-serve public/ on 0.0.0.0:8090 for other machines link-check offline internal-link scan (lychee --scheme file) -> JSON link-fix comment out confirmed-dead internal links, then rebuild external-check network scan of external links (report-only) clean / all Add scripts/: serve_lan.py stdlib static server rooted at public/ (prints LAN IP) fix_dead_links.py idempotent internal dead-link comment-out check_external_links.py external link report (drives the daily cron job) .gitignore: add linkcheck/, resources/_gen/ (regenerable Hugo asset cache), __pycache__/, and .hermes/.
This commit is contained in:
Executable
+100
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Comment out confirmed dead INTERNAL page links, report external/bare-domain failures.
|
||||
|
||||
Reads lychee's JSON report (linkcheck/internal.json). A link is auto-commented ONLY
|
||||
if lychee flagged its file:// path as a missing file under the built site (public/).
|
||||
External https:// failures and bare-domain failures are written to a manual-review
|
||||
report and left untouched. Idempotent: already-commented links are not detected by
|
||||
lychee, so re-runs are safe.
|
||||
|
||||
IMPORTANT: run lychee with the SOURCE markdown as input (e.g. `lychee --root-dir public
|
||||
--default-extension html content/*.md`) so the error_map keys are the markdown files
|
||||
we actually edit. They are relative to the repo root.
|
||||
|
||||
Usage:
|
||||
python3 scripts/fix_dead_links.py --report linkcheck/internal.json # dry-run
|
||||
python3 scripts/fix_dead_links.py --report linkcheck/internal.json --apply # edit
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--report", required=True)
|
||||
ap.add_argument("--apply", action="store_true")
|
||||
ap.add_argument("--public-dir", default="public",
|
||||
help="built site dir that lychee's --root-dir pointed at")
|
||||
args = ap.parse_args()
|
||||
|
||||
data = json.loads(Path(args.report).read_text())
|
||||
err_map = data.get("error_map", {})
|
||||
|
||||
to_comment = [] # (md_file_relative, line, dest)
|
||||
manual = [] # (md_file_relative, line, url, status_text)
|
||||
for md_file, items in err_map.items():
|
||||
for it in items:
|
||||
url = it.get("url", "")
|
||||
status = it.get("status", {}).get("text", "")
|
||||
span = it.get("span", {})
|
||||
if url.startswith("file://"):
|
||||
after = url[len("file://"):]
|
||||
m = re.search(r"/" + re.escape(args.public_dir) + r"/(.*)$", after)
|
||||
if m:
|
||||
to_comment.append((md_file, span.get("line"), "/" + m.group(1)))
|
||||
continue
|
||||
manual.append((md_file, span.get("line"), url, status))
|
||||
|
||||
# Group by source file (error_map keys are relative to repo root).
|
||||
by_file = {}
|
||||
for md_file, line, dest in to_comment:
|
||||
by_file.setdefault(md_file, []).append((line, dest))
|
||||
for md_file in by_file:
|
||||
by_file[md_file].sort(reverse=True) # edit bottom-up; keep line count stable
|
||||
|
||||
plan = 0
|
||||
for md_file, edits in sorted(by_file.items()):
|
||||
p = Path(md_file)
|
||||
if not p.exists():
|
||||
print(f" ! missing source, skipping: {p}")
|
||||
continue
|
||||
lines = p.read_text().splitlines(keepends=True)
|
||||
changed = False
|
||||
for line_no, dest in edits:
|
||||
if not line_no:
|
||||
continue
|
||||
idx = line_no - 1
|
||||
if idx < 0 or idx >= len(lines):
|
||||
continue
|
||||
cur = lines[idx]
|
||||
# lychee reports the resolved file path (trailing slash stripped); the
|
||||
# markdown link may keep a trailing slash, so allow an optional "/".
|
||||
m = re.search(r"\[([^\]]*)\]\(" + re.escape(dest) + r"/?\s*\)", cur)
|
||||
if not m:
|
||||
print(f" ? could not locate link on {md_file}:{line_no} for {dest}")
|
||||
continue
|
||||
link_txt = m.group(0)
|
||||
if link_txt.count("<!--") or (cur.count("<!--") > cur.count("-->")):
|
||||
continue # already commented
|
||||
lines[idx] = cur[:m.start()] + "<!-- " + link_txt + " -->" + cur[m.end():]
|
||||
changed = True
|
||||
plan += 1
|
||||
print(f" {'APPLY ' if args.apply else 'DRY '} {md_file}:{line_no} {link_txt}")
|
||||
if changed and args.apply:
|
||||
p.write_text("".join(lines))
|
||||
|
||||
outdir = Path("linkcheck")
|
||||
outdir.mkdir(parents=True, exist_ok=True)
|
||||
with (outdir / "manual_review.txt").open("w") as f:
|
||||
f.write("Manual review (NOT auto-commented). External/bare-domain failures.\n\n")
|
||||
for md_file, line, url, status in sorted(manual):
|
||||
f.write(f"{md_file}:{line}\t{url}\t[{status}]\n")
|
||||
|
||||
print(f"\nAuto-comment planned: {plan} (apply={args.apply})")
|
||||
print(f"Manual review entries: {len(manual)} -> {outdir/'manual_review.txt'}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user