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,90 @@
|
||||
"""Responsive JPEG variants for hero images."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image
|
||||
|
||||
VARIANT_WIDTHS = (360, 560, 800)
|
||||
JPEG_QUALITY = 82
|
||||
DATE_IMAGE_RE = re.compile(r"^(\d{4}-\d{2}-\d{2})\.jpg$")
|
||||
|
||||
|
||||
def image_public_path(date_iso: str, width: int | None = None) -> str:
|
||||
if width is None:
|
||||
return f"/img/{date_iso}.jpg"
|
||||
return f"/img/{date_iso}-{width}w.jpg"
|
||||
|
||||
|
||||
def build_srcset(date_iso: str, widths: list[int], full_width: int) -> str:
|
||||
parts = [f"{image_public_path(date_iso, w)} {w}w" for w in widths]
|
||||
parts.append(f"{image_public_path(date_iso)} {full_width}w")
|
||||
return ", ".join(parts)
|
||||
|
||||
|
||||
def create_responsive_variants(img_dir: Path, date_iso: str) -> dict[str, str] | None:
|
||||
source = img_dir / f"{date_iso}.jpg"
|
||||
if not source.exists():
|
||||
return None
|
||||
|
||||
with Image.open(source) as im:
|
||||
im = im.convert("RGB")
|
||||
full_width, full_height = im.size
|
||||
generated: list[int] = []
|
||||
|
||||
for width in VARIANT_WIDTHS:
|
||||
if width >= full_width:
|
||||
continue
|
||||
height = round(full_height * width / full_width)
|
||||
resized = im.resize((width, height), Image.Resampling.LANCZOS)
|
||||
out = img_dir / f"{date_iso}-{width}w.jpg"
|
||||
resized.save(out, "JPEG", quality=JPEG_QUALITY, optimize=True)
|
||||
generated.append(width)
|
||||
|
||||
im.save(source, "JPEG", quality=JPEG_QUALITY, optimize=True)
|
||||
|
||||
srcset = build_srcset(date_iso, generated, full_width)
|
||||
default_src = image_public_path(date_iso, 560) if 560 in generated else image_public_path(date_iso)
|
||||
thumb_src = image_public_path(date_iso, 360) if 360 in generated else image_public_path(date_iso)
|
||||
|
||||
return {
|
||||
"src": default_src,
|
||||
"srcset": srcset,
|
||||
"sizes": "(max-width: 768px) 100vw, 560px",
|
||||
"og": image_public_path(date_iso),
|
||||
"thumb_src": thumb_src,
|
||||
"thumb_srcset": srcset,
|
||||
"thumb_sizes": "(max-width: 640px) 50vw, 360px",
|
||||
}
|
||||
|
||||
|
||||
def load_image_descriptor(img_dir: Path, date_iso: str) -> dict[str, str] | None:
|
||||
if not (img_dir / f"{date_iso}.jpg").exists():
|
||||
return None
|
||||
if not (img_dir / f"{date_iso}-360w.jpg").exists():
|
||||
return create_responsive_variants(img_dir, date_iso)
|
||||
widths = [w for w in VARIANT_WIDTHS if (img_dir / f"{date_iso}-{w}w.jpg").exists()]
|
||||
with Image.open(img_dir / f"{date_iso}.jpg") as im:
|
||||
full_width = im.size[0]
|
||||
srcset = build_srcset(date_iso, widths, full_width)
|
||||
return {
|
||||
"src": image_public_path(date_iso, 560) if 560 in widths else image_public_path(date_iso),
|
||||
"srcset": srcset,
|
||||
"sizes": "(max-width: 768px) 100vw, 560px",
|
||||
"og": image_public_path(date_iso),
|
||||
"thumb_src": image_public_path(date_iso, 360) if 360 in widths else image_public_path(date_iso),
|
||||
"thumb_srcset": srcset,
|
||||
"thumb_sizes": "(max-width: 640px) 50vw, 360px",
|
||||
}
|
||||
|
||||
|
||||
def iter_source_images(img_dir: Path) -> list[Path]:
|
||||
if not img_dir.exists():
|
||||
return []
|
||||
sources = []
|
||||
for path in sorted(img_dir.glob("*.jpg")):
|
||||
if DATE_IMAGE_RE.match(path.name):
|
||||
sources.append(path)
|
||||
return sources
|
||||
Reference in New Issue
Block a user