8abd7ddb59
Ergänzt Backfill- und Hilfsskripte, erzeugt nach der Pixazo-Generierung komprimierte srcset-Varianten, validiert leere Bilder und fügt Favicon, Web Manifest sowie Open-Graph-/Twitter-Tags in die Templates ein. Co-authored-by: Cursor <cursoragent@cursor.com>
150 lines
4.8 KiB
Python
150 lines
4.8 KiB
Python
#!/usr/bin/env python3
|
||
"""Inject favicon and social meta tags into existing static HTML pages."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import os
|
||
import re
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
from generate import build_archiv_entries, log, render_template, write_text_atomic
|
||
|
||
|
||
def extract_meta(html: str, prop: str) -> str | None:
|
||
match = re.search(rf'property="{re.escape(prop)}" content="([^"]*)"', html)
|
||
return match.group(1) if match else None
|
||
|
||
|
||
def extract_name_meta(html: str, name: str) -> str | None:
|
||
match = re.search(rf'name="{re.escape(name)}" content="([^"]*)"', html)
|
||
return match.group(1) if match else None
|
||
|
||
|
||
def extract_hero_image(html: str) -> str | None:
|
||
match = re.search(r'<img src="/img/(\d{4}-\d{2}-\d{2})(?:-\d+w)?\.jpg"', html)
|
||
return f"/img/{match.group(1)}.jpg" if match else None
|
||
|
||
|
||
def render_head_block(
|
||
*,
|
||
site_url: str,
|
||
canonical_path: str,
|
||
og_title: str,
|
||
og_description: str,
|
||
og_image: str | None,
|
||
) -> str:
|
||
return render_template(
|
||
"_head_common.html.j2",
|
||
{
|
||
"site_url": site_url,
|
||
"canonical_path": canonical_path,
|
||
"og_title": og_title,
|
||
"og_description": og_description,
|
||
"og_image": og_image,
|
||
},
|
||
).rstrip()
|
||
|
||
|
||
def inject_head_block(html: str, block: str) -> str:
|
||
if 'rel="icon"' in html:
|
||
return html
|
||
marker = '<meta property="og:description"'
|
||
idx = html.find(marker)
|
||
if idx == -1:
|
||
marker = '<meta name="description"'
|
||
idx = html.find(marker)
|
||
if idx == -1:
|
||
raise ValueError("could not find description meta tag")
|
||
line_end = html.find("\n", idx)
|
||
if line_end == -1:
|
||
raise ValueError("unexpected HTML structure")
|
||
return html[: line_end + 1] + block + "\n" + html[line_end + 1 :]
|
||
|
||
|
||
def patch_startup_page(path: Path, canonical_path: str, site_url: str) -> bool:
|
||
html = path.read_text(encoding="utf-8")
|
||
og_title = extract_meta(html, "og:title") or extract_name_meta(html, "twitter:title")
|
||
og_description = extract_meta(html, "og:description") or extract_name_meta(html, "description")
|
||
if not og_title or not og_description:
|
||
log(f"warning: skipping {path}, missing title/description")
|
||
return False
|
||
block = render_head_block(
|
||
site_url=site_url,
|
||
canonical_path=canonical_path,
|
||
og_title=og_title,
|
||
og_description=og_description,
|
||
og_image=extract_hero_image(html),
|
||
)
|
||
updated = inject_head_block(html, block)
|
||
if updated != html:
|
||
write_text_atomic(path, updated)
|
||
log(f"patched {path}")
|
||
return True
|
||
return False
|
||
|
||
|
||
def patch_archiv_index(path: Path, site_url: str, og_image: str) -> bool:
|
||
html = path.read_text(encoding="utf-8")
|
||
og_title = "MOBEA Archiv – Unsere bisherigen Pivots"
|
||
og_description = "Jeden Tag ein neues Startup. Hier ruhen die bisherigen."
|
||
if 'property="og:title"' not in html:
|
||
html = html.replace(
|
||
'<meta name="description" content="Jeden Tag ein neues Startup. Hier ruhen die bisherigen.">',
|
||
'<meta name="description" content="Jeden Tag ein neues Startup. Hier ruhen die bisherigen.">\n'
|
||
f' <meta property="og:title" content="{og_title}">\n'
|
||
f' <meta property="og:description" content="{og_description}">',
|
||
1,
|
||
)
|
||
block = render_head_block(
|
||
site_url=site_url,
|
||
canonical_path="/archiv/",
|
||
og_title=og_title,
|
||
og_description=og_description,
|
||
og_image=og_image,
|
||
)
|
||
updated = inject_head_block(html, block)
|
||
if updated != html:
|
||
write_text_atomic(path, updated)
|
||
log(f"patched {path}")
|
||
return True
|
||
return False
|
||
|
||
|
||
def main() -> int:
|
||
repo_root = Path(__file__).resolve().parent.parent
|
||
site_dir = Path(os.environ.get("SITE_DIR", repo_root / "public"))
|
||
data_dir = Path(os.environ.get("DATA_DIR", repo_root / "data"))
|
||
site_url = os.environ.get("SITE_URL", "https://mobea.de")
|
||
|
||
archive = json.loads((data_dir / "archive.json").read_text(encoding="utf-8"))
|
||
latest_image = next(
|
||
(
|
||
f"/img/{entry['date']}.jpg"
|
||
for entry in sorted(archive, key=lambda e: e["date"], reverse=True)
|
||
if entry.get("has_image")
|
||
),
|
||
"/og-default.jpg",
|
||
)
|
||
|
||
patched = 0
|
||
index_path = site_dir / "index.html"
|
||
if index_path.exists():
|
||
patched += patch_startup_page(index_path, "/", site_url)
|
||
|
||
archiv_dir = site_dir / "archiv"
|
||
for path in sorted(archiv_dir.glob("2026-*.html")):
|
||
patched += patch_startup_page(path, f"/archiv/{path.name}", site_url)
|
||
|
||
archiv_index = archiv_dir / "index.html"
|
||
if archiv_index.exists():
|
||
patched += patch_archiv_index(archiv_index, site_url, latest_image)
|
||
|
||
log(f"done, patched {patched} file(s)")
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|