feat: Archiv-Backfill, responsive Bilder und Site-Meta

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>
This commit is contained in:
Frank Schwenk
2026-06-12 18:14:40 +02:00
parent f6e901bad8
commit 8abd7ddb59
15 changed files with 668 additions and 10 deletions
+149
View File
@@ -0,0 +1,149 @@
#!/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())