feat: photo landing page with favicon set
This commit is contained in:
parent
b533549e45
commit
672faf3af8
19 changed files with 297 additions and 45 deletions
78
frontend/scripts/fetch-photos.py
Normal file
78
frontend/scripts/fetch-photos.py
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
"""Fetch landing-page photos (one-time + re-runnable).
|
||||
|
||||
Downloads royalty-free originals (see SOURCES.md next to the photos),
|
||||
resizes to max 1600px wide and stores them as WebP q80 in
|
||||
frontend/public/photos/ (self-hosted: no hotlinking, no tracking).
|
||||
|
||||
Run in the api container (has network + Pillow):
|
||||
docker compose exec api /opt/venv/bin/python \\
|
||||
/srv/kirche-einfach/frontend/scripts/fetch-photos.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import urllib.request
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from PIL import Image
|
||||
except ImportError:
|
||||
sys.exit("Pillow required (present in the api image)")
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent.parent
|
||||
OUT = ROOT / "frontend" / "public" / "photos"
|
||||
|
||||
|
||||
def pexels(photo_id: int) -> str:
|
||||
return (
|
||||
f"https://images.pexels.com/photos/{photo_id}/"
|
||||
f"pexels-photo-{photo_id}.jpeg?auto=compress&cs=tinysrgb&w=1600"
|
||||
)
|
||||
|
||||
|
||||
PHOTOS = {
|
||||
# target file -> (direct file URL, source page for SOURCES.md)
|
||||
"hero-church.webp": (
|
||||
pexels(31630127),
|
||||
"https://www.pexels.com/photo/historic-church-with-bell-tower-in-germany-31630127/",
|
||||
),
|
||||
"church-pews.webp": (
|
||||
pexels(7566207),
|
||||
"https://www.pexels.com/photo/church-pews-7566207/",
|
||||
),
|
||||
"paperwork-desk.webp": (
|
||||
pexels(18687845),
|
||||
"https://www.pexels.com/photo/desk-with-documents-and-stamps-18687845/",
|
||||
),
|
||||
"team-laptop.webp": (
|
||||
pexels(7793702),
|
||||
"https://www.pexels.com/photo/a-team-looking-at-a-laptop-7793702/",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def fetch(url: str) -> bytes:
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "kirche-einfach/1.0"})
|
||||
with urllib.request.urlopen(req, timeout=120) as response:
|
||||
return response.read()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
for filename, (url, _page) in PHOTOS.items():
|
||||
raw = fetch(url)
|
||||
with Image.open(BytesIO(raw)) as img:
|
||||
img = img.convert("RGB")
|
||||
if img.width > 1600:
|
||||
img = img.resize(
|
||||
(1600, round(img.height * 1600 / img.width)), Image.LANCZOS
|
||||
)
|
||||
target = OUT / Path(filename).with_suffix(".webp").name
|
||||
img.save(target, "WEBP", quality=80, method=6)
|
||||
print(f"wrote {target} ({target.stat().st_size // 1024} KB)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
44
frontend/scripts/icons.mjs
Normal file
44
frontend/scripts/icons.mjs
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
/* Generate favicon PNGs + manifest icons from favicon.svg (re-runnable).
|
||||
*
|
||||
* Usage (frontend container, Chromium already installed for screenshots):
|
||||
* npm run icons
|
||||
*
|
||||
* Renders the SVG at each target size (crisp native rasterization) into
|
||||
* public/. apple-touch-icon keeps the solid kirchenblau background (iOS
|
||||
* fills transparency black). The 512 icon doubles as maskable: the glyph
|
||||
* stays inside the inner 80% safe zone.
|
||||
*/
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { chromium } from "playwright";
|
||||
|
||||
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const svg = readFileSync(join(root, "public", "favicon.svg"), "utf-8");
|
||||
const dataUrl = `data:image/svg+xml,${encodeURIComponent(svg)}`;
|
||||
|
||||
const targets = [
|
||||
["favicon-32x32.png", 32],
|
||||
["apple-touch-icon.png", 180],
|
||||
["icon-192.png", 192],
|
||||
["icon-512.png", 512],
|
||||
];
|
||||
|
||||
const browser = await chromium.launch();
|
||||
try {
|
||||
const page = await browser.newPage();
|
||||
for (const [file, size] of targets) {
|
||||
await page.setViewportSize({ width: size, height: size });
|
||||
await page.setContent(
|
||||
`<body style="margin:0"><img src="${dataUrl}" width="${size}" height="${size}"/></body>`,
|
||||
);
|
||||
await page.locator("img").screenshot({
|
||||
path: join(root, "public", file),
|
||||
omitBackground: true,
|
||||
});
|
||||
console.log(`wrote public/${file}`);
|
||||
}
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue