Files
pw/scripts/postbuild.py
T
Asger Geel WeirsøeandClaude Opus 5.5 e58a11c2b4 feat: flyt sitet fra Astro til Zola
Samme indhold, URL'er og udseende, bygget af en enkelt zola-binær og Tailwind
standalone-CLI uden Node. Kanonisk domæne er asger.weirsoe.dk.

Blogindlæg ligger som page bundles med frontmatter Zola forstår, så de gamle
stempler er ugyldige, indtil scripts/omstempl.py er kørt med PUBLISH_KEY.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 16:47:08 +01:00

158 lines
5.2 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)
- 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."""
s = html.unescape(re.sub(r"<[^>]+>", "", text)).strip().lower()
s = re.sub(r"[^\w\- ]", "", s)
return s.replace(" ", "-")
def heading_ids():
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]}>'
f.write_text(re.sub(r'<h([1-6]) id="[^"]*"([^>]*)>(.*?)(?=</h\1>)',
lambda m: fix(m) + m[3], src, flags=re.S))
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", "/peppol")
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 not in ("sw.js", "mermaid.min.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)
heading_ids()
if PREVIEW:
rebase()
else:
sitemap()
service_worker()
if __name__ == "__main__":
main()