larpaso-website/scripts/fonts-holen.py
Lara 2fa8dc6615 Serve fonts ourselves and write the privacy policy
Fonts: every Google font the editor offers (13 families, latin and
latin-ext, 1.2 MB) now lives in public/fonts; the layout inlines
@font-face rules only for the fonts in use, built from
src/lib/font-faces.json. scripts/fonts-holen.py re-fetches them.
The admin font preview loads the same files via /admin/fonts and
/admin/api/font-faces. Checked: no request leaves larpaso.de on the
start page, Impressum or Datenschutz, nor in the admin editor.

nginx: access_log off, so visitor IPs are not stored. Caddy has no
access log configured either.

Privacy policy replaces the placeholder and describes only what the
site actually does: controller (from the Impressum), hosting at netcup
in Nuremberg (from the server IP), no access logs, HTTPS, mailto
contact form and e-mail, self-hosted fonts and media, no cookies,
data subject rights and the Lower Saxony supervisory authority.
The closing note is no longer styled as a warning.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 10:54:24 +02:00

30 lines
2.1 KiB
Python

import re, json, urllib.request, urllib.error, pathlib
UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0 Safari/537.36"
FAMS = ['Playfair Display','Lora','Merriweather','Libre Baskerville','DM Serif Display','Inter','Roboto','Open Sans','Poppins','Montserrat','Nunito','Work Sans','Source Sans 3']
out_dir = pathlib.Path('public/fonts'); out_dir.mkdir(parents=True, exist_ok=True)
manifest = {}
def get(url):
return urllib.request.urlopen(urllib.request.Request(url, headers={'User-Agent': UA})).read()
for fam in FAMS:
css = None
for w in ['400;500;600;700', '400;700', '400']:
try:
css = get(f"https://fonts.googleapis.com/css2?family={fam.replace(' ','+')}:wght@{w}&display=swap").decode(); break
except urllib.error.HTTPError: pass
slug = fam.lower().replace(' ', '-'); d = out_dir / slug; d.mkdir(exist_ok=True)
faces = []; seen = {}
for subset, body in re.findall(r'/\* ([\w-]+) \*/\s*@font-face \{(.*?)\}', css, re.S):
if subset not in ('latin', 'latin-ext'): continue
weight = re.search(r'font-weight: (\d+)', body).group(1)
url = re.search(r'url\((.*?)\)', body).group(1)
rng = re.search(r'unicode-range: (.*?);', body).group(1)
if url not in seen:
fn = f"{slug}-{subset}-{len(seen)}.woff2"; (d / fn).write_bytes(get(url)); seen[url] = fn
faces.append({'weight': int(weight), 'subset': subset, 'file': f"/fonts/{slug}/{seen[url]}", 'unicodeRange': rng})
# gleiche Datei fuer mehrere Gewichte (variable Schrift) -> eine Regel mit Gewichtsbereich
merged = {}
for f in faces:
k = (f['file'], f['subset']); merged.setdefault(k, dict(f, weights=[]))['weights'].append(f['weight'])
manifest[fam] = [{'file': v['file'], 'weight': f"{min(v['weights'])} {max(v['weights'])}" if len(v['weights']) > 1 else str(v['weights'][0]), 'unicodeRange': v['unicodeRange']} for v in merged.values()]
print(fam, len(manifest[fam]), 'Regeln', len(seen), 'Dateien')
json.dump(manifest, open('src/lib/font-faces.json', 'w', encoding='utf8'), indent=2)