// Build the /press/images gallery from the actual files on disk. // // Source of truth: public/landing/press/images/ // *.jpg|png|webp — photography (the downloadable masters) // renders/*.png|webp — product renders (downloadable masters) // previews/ — GENERATED 960px webp previews (this script) // // For every master it (re)generates a preview when missing or stale, then // returns the gallery
markup + counts for build-pages.mjs to splice // into press-images.html ({{PRESS_GALLERY_RENDERS}} / {{PRESS_GALLERY_PHOTOS}} // / {{PRESS_GALLERY_COUNT}}). Captions and the mono meta line (FORMAT · WxH · // size) are derived from the file itself, so they stay truthful. // // Adding an image to the press kit = drop the master in the right folder with // a kebab-case name and run `node scripts/build-pages.mjs`. Masters must stay // under ~25 MB (the Cloudflare Pages per-file limit) — anything bigger goes to // R2 and gets hand-linked instead. import fs from "node:fs"; import path from "node:path"; import sharp from "sharp"; const ROOT = "public/landing/press/images"; const RENDERS = path.join(ROOT, "renders"); const PREVIEWS = path.join(ROOT, "previews"); const EXT = /\.(jpe?g|png|webp)$/i; // kebab-case file name -> display caption const TOKENS = { ipd: "IPD", ceo: "CEO", vr: "VR" }; const SMALL = new Set(["in", "with", "on", "the", "and", "of", "a", "at", "to"]); function caption(base) { return base .split("-") .map((w, i) => { if (TOKENS[w]) return TOKENS[w]; if (/^\d/.test(w)) return w; if (i > 0 && SMALL.has(w)) return w; return w[0].toUpperCase() + w.slice(1); }) .join(" "); } function fmtSize(bytes) { const kb = bytes / 1024; return kb >= 1024 ? (kb / 1024).toFixed(1) + " MB" : Math.round(kb) + " KB"; } const DL_ICON = ''; async function buildItem(absFile, urlDir, { fit }) { const file = path.basename(absFile); const base = file.replace(EXT, ""); const meta = await sharp(absFile).metadata(); const stat = fs.statSync(absFile); // preview: 960px webp, regenerated only when missing or older than the master const prevFile = path.join(PREVIEWS, base + ".webp"); if (!fs.existsSync(prevFile) || fs.statSync(prevFile).mtimeMs < stat.mtimeMs) { await sharp(absFile) .resize({ width: 960, withoutEnlargement: true }) .webp({ quality: 72, effort: 6 }) .toFile(prevFile); } const name = caption(base); const fmt = path.extname(file).slice(1).toUpperCase().replace("JPG", "JPEG"); const info = `${fmt} · ${meta.width}×${meta.height} · ${fmtSize(stat.size)}`; const href = `${urlDir}/${file}`; const prev = `/landing/press/images/previews/${base}.webp`; const view = fit ? "gal__view gal__view--fit" : "gal__view"; return `
${name}
${name} ${info} ${DL_ICON}
`; } export async function buildPressGallery() { fs.mkdirSync(PREVIEWS, { recursive: true }); const photos = fs.readdirSync(ROOT).filter((f) => EXT.test(f)).sort(); const renders = fs.readdirSync(RENDERS).filter((f) => EXT.test(f)).sort(); // prune previews whose master is gone (renamed/removed files) const live = new Set([...photos, ...renders].map((f) => f.replace(EXT, "") + ".webp")); for (const p of fs.readdirSync(PREVIEWS)) if (!live.has(p)) fs.unlinkSync(path.join(PREVIEWS, p)); const photoHtml = []; for (const f of photos) photoHtml.push(await buildItem(path.join(ROOT, f), "/landing/press/images", { fit: false })); const renderHtml = []; for (const f of renders) renderHtml.push(await buildItem(path.join(RENDERS, f), "/landing/press/images/renders", { fit: true })); console.log(`press gallery: ${renders.length} renders + ${photos.length} photos (previews in ${PREVIEWS})`); return { renders: renderHtml.join("\n\n"), photos: photoHtml.join("\n\n"), }; }