Build & Deploy / deploy (push) Successful in 17s
Claude-Session: https://claude.ai/code/session_01GGe7kUhooMFuzY6GaDb2w9
161 lines
5.5 KiB
Python
Executable File
161 lines
5.5 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Efterbehandling af Zolas output i public/:
|
|
|
|
- overskrift-id'er som på den gamle side (bevarer æøå, så gamle #ankre stadig virker)
|
|
- kodeblokke og tabeller ruller inden i sig selv og kan nås med tastatur
|
|
- sitemap-index.xml + sitemap-0.xml med lastmod fra git og frontmatter
|
|
- sw.js med precache-liste
|
|
- PW_PREVIEW=1: hele sitet flyttes ind under /udkast, uden sitemap og service worker
|
|
"""
|
|
import hashlib
|
|
import html
|
|
import json
|
|
import os
|
|
import pathlib
|
|
import re
|
|
import subprocess
|
|
|
|
OUT = pathlib.Path("public")
|
|
SITE = "https://asger.weirsoe.dk"
|
|
PREVIEW = os.environ.get("PW_PREVIEW") == "1"
|
|
BASE = "/udkast"
|
|
|
|
|
|
def pages():
|
|
return sorted(OUT.rglob("*.html"))
|
|
|
|
|
|
def slug(text):
|
|
"""Samme regler som github-slugger, som den gamle build brugte, og som Astro uden afsluttende -."""
|
|
s = html.unescape(re.sub(r"<[^>]+>", "", text)).strip().lower()
|
|
s = re.sub(r"[^\w\- ]", "", s)
|
|
return s.replace(" ", "-").removesuffix("-")
|
|
|
|
|
|
def prose():
|
|
for f in pages():
|
|
src = f.read_text()
|
|
if 'class="prose' not in src:
|
|
continue
|
|
seen = {}
|
|
|
|
def fix(m):
|
|
s = slug(m[3])
|
|
n = seen.get(s, 0)
|
|
seen[s] = n + 1
|
|
return f'<h{m[1]} id="{s}-{n}"{m[2]}>' if n else f'<h{m[1]} id="{s}"{m[2]}>'
|
|
|
|
src = re.sub(r'<h([1-6]) id="[^"]*"([^>]*)>(.*?)(?=</h\1>)', lambda m: fix(m) + m[3], src, flags=re.S)
|
|
src = src.replace('<pre class="giallo"', '<pre tabindex="0" class="giallo"')
|
|
src = src.replace("<table>", '<div class="tabel" tabindex="0"><table>').replace("</table>", "</table></div>")
|
|
f.write_text(src)
|
|
|
|
|
|
def git_date(*files):
|
|
if int(subprocess.run(["git", "rev-list", "--count", "HEAD"], capture_output=True, text=True).stdout or 0) < 2:
|
|
return None
|
|
out = subprocess.run(["git", "log", "-1", "--format=%cI", "--", *files], capture_output=True, text=True).stdout
|
|
return out.strip() or None
|
|
|
|
|
|
def frontmatter(path):
|
|
text = path.read_text()
|
|
head = text.split("---\n", 2)[1]
|
|
return {k: v.strip().strip("'\"") for k, v in re.findall(r"^\s*(\w+):(.*)$", head, re.M)}
|
|
|
|
|
|
def iso(date):
|
|
return f"{date}T00:00:00.000Z"
|
|
|
|
|
|
def lastmods():
|
|
"""Rute -> lastmod: sidste commit for statiske sider, frontmatter for blogindlæg."""
|
|
routes = {}
|
|
for md in pathlib.Path("content").rglob("*.md"):
|
|
fm = frontmatter(md)
|
|
if "blog" in md.parts or md.parts[1] == "projekter" or "projects" in md.parts:
|
|
continue
|
|
route = "/" + str(md.relative_to("content").with_suffix("")).removesuffix("_index")
|
|
route = route.rstrip("/") or "/"
|
|
tpl = fm.get("template")
|
|
d = git_date(str(md), f"templates/{tpl}") if tpl else git_date(str(md))
|
|
if d:
|
|
routes[route] = d
|
|
posts = []
|
|
for md in pathlib.Path("content/blog").glob("*/index.md"):
|
|
fm = frontmatter(md)
|
|
d = iso(fm.get("updated") or fm["date"])
|
|
routes[f"/blog/{md.parent.name}"] = d
|
|
posts.append(d)
|
|
if posts:
|
|
routes["/blog"] = max(posts)
|
|
return routes
|
|
|
|
|
|
def sitemap():
|
|
skip = ("/tags",)
|
|
skip_exact = ("/cookies", "/status", "/chat")
|
|
routes = []
|
|
for f in pages():
|
|
rel = f.relative_to(OUT)
|
|
if rel.name != "index.html":
|
|
continue
|
|
r = "/" + str(rel.parent).replace(".", "", 1) if str(rel.parent) == "." else f"/{rel.parent}"
|
|
if r.startswith(skip) or r in skip_exact:
|
|
continue
|
|
routes.append(r)
|
|
routes.append("/tarballz/")
|
|
mods = lastmods()
|
|
urls = []
|
|
for r in sorted(routes):
|
|
mod = mods.get(r)
|
|
urls.append(f"<url><loc>{SITE}{r}</loc>" + (f"<lastmod>{mod}</lastmod>" if mod else "")
|
|
+ f"<changefreq>weekly</changefreq><priority>{'1.0' if r == '/' else '0.7'}</priority></url>")
|
|
ns = 'xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"'
|
|
(OUT / "sitemap-0.xml").write_text(f'<?xml version="1.0" encoding="UTF-8"?><urlset {ns}>{"".join(urls)}</urlset>')
|
|
newest = max(mods.values(), default=None)
|
|
(OUT / "sitemap-index.xml").write_text(
|
|
f'<?xml version="1.0" encoding="UTF-8"?><sitemapindex {ns}><sitemap><loc>{SITE}/sitemap-0.xml</loc>'
|
|
+ (f"<lastmod>{newest}</lastmod>" if newest else "") + "</sitemap></sitemapindex>")
|
|
|
|
|
|
def service_worker():
|
|
"""Precache-listen afhænger af buildet, resten af sw.js ligger i scripts/sw.js."""
|
|
exts = {".html", ".css", ".js", ".svg", ".png", ".woff2", ".xml", ".txt", ".webmanifest"}
|
|
files = sorted(p for p in OUT.rglob("*") if p.is_file() and p.suffix in exts
|
|
and p.name != "sw.js")
|
|
h = hashlib.sha256()
|
|
for p in files:
|
|
h.update(p.read_bytes())
|
|
urls = ["/" + str(p.relative_to(OUT)) for p in files]
|
|
src = pathlib.Path("scripts/sw.js").read_text()
|
|
src = src.replace("__VERSION__", h.hexdigest()[:12]).replace("__URLS__", json.dumps(urls, ensure_ascii=False))
|
|
(OUT / "sw.js").write_text(src)
|
|
|
|
|
|
def rebase():
|
|
"""Rod-relative links og billeder i HTML peger ind under /udkast."""
|
|
attr = re.compile(r'\b(href|src|action)="/(?!/|udkast)')
|
|
srcset = re.compile(r'\bsrcset="([^"]*)"')
|
|
|
|
def fix_set(m):
|
|
return 'srcset="' + re.sub(r"(^|, )/(?!/)", rf"\1{BASE}/", m[1]) + '"'
|
|
|
|
for f in pages():
|
|
src = f.read_text()
|
|
f.write_text(srcset.sub(fix_set, attr.sub(rf'\1="{BASE}/', src)))
|
|
|
|
|
|
def main():
|
|
(OUT / "sitemap.xml").unlink(missing_ok=True)
|
|
prose()
|
|
if PREVIEW:
|
|
rebase()
|
|
else:
|
|
sitemap()
|
|
service_worker()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|