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:
@@ -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())
|
||||
Reference in New Issue
Block a user