// Non-project imagery: team portraits, team group, principal, OG image. // Usage: node tools/images-extra.js (after tools/images.js; before build) const fs = require('fs'); const path = require('path'); const sharp = require('sharp'); const ROOT = path.join(__dirname, '..'); const OUT = path.join(ROOT, 'dist', 'assets', 'img'); const site = JSON.parse(fs.readFileSync(path.join(ROOT, 'src', 'data', 'site.json'), 'utf8')); const home = JSON.parse(fs.readFileSync(path.join(ROOT, 'src', 'data', 'home.json'), 'utf8')); const projects = JSON.parse(fs.readFileSync(path.join(ROOT, 'src', 'data', 'projects.json'), 'utf8')); const slugify = s => s.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, ''); async function variants(srcAbs, destDir, baseName, widths, jpegWidth) { fs.mkdirSync(destDir, { recursive: true }); const base = sharp(srcAbs).rotate(); const meta = await base.metadata(); const srcW = meta.autoOrient ? meta.autoOrient.width : meta.width; const srcH = meta.autoOrient ? meta.autoOrient.height : meta.height; const entry = { variants: [] }; for (const w of widths) { const eff = Math.min(w, srcW); const file = `${baseName}-${w}.webp`; await base.clone().resize({ width: eff, withoutEnlargement: true }).webp({ quality: 78 }).toFile(path.join(destDir, file)); entry.variants.push({ file: path.relative(path.join(ROOT, 'dist'), path.join(destDir, file)).replace(/\\/g, '/'), width: eff, height: Math.round(srcH * eff / srcW), format: 'webp' }); } const jw = Math.min(jpegWidth, srcW); const jfile = `${baseName}-${jpegWidth}.jpg`; await base.clone().resize({ width: jw, withoutEnlargement: true }).jpeg({ quality: 74, mozjpeg: true }).toFile(path.join(destDir, jfile)); entry.variants.push({ file: path.relative(path.join(ROOT, 'dist'), path.join(destDir, jfile)).replace(/\\/g, '/'), width: jw, height: Math.round(srcH * jw / srcW), format: 'jpeg' }); return entry; } (async () => { const out = {}; for (const t of site.team) { const src = path.join(ROOT, t.photo); if (!fs.existsSync(src)) { console.error('MISSING team photo:', t.photo); continue; } out['team:' + t.name] = await variants(src, path.join(OUT, 'team'), slugify(t.name), [480], 480); } out['principal'] = await variants(path.join(ROOT, site.principal.photo), path.join(OUT, 'team'), 'principal', [480, 800], 800); const group = path.join(ROOT, 'img', 'team', 'team group.jpg'); if (fs.existsSync(group)) out['team-group'] = await variants(group, path.join(OUT, 'team'), 'team-group', [800, 1600], 1200); // OG image 1200x630 from the homepage hero pick const heroProj = projects.find(p => p.slug === home.hero.slug); const heroSrc = path.join(ROOT, heroProj.images[home.hero.index]); fs.mkdirSync(OUT, { recursive: true }); await sharp(heroSrc).rotate().resize(1200, 630, { fit: 'cover', position: 'attention' }).jpeg({ quality: 78, mozjpeg: true }).toFile(path.join(OUT, 'og.jpg')); out['og'] = { variants: [{ file: 'assets/img/og.jpg', width: 1200, height: 630, format: 'jpeg' }] }; fs.writeFileSync(path.join(ROOT, 'src', 'data', 'images-extra.json'), JSON.stringify(out, null, 1)); console.log('DONE — extra images:', Object.keys(out).length); })();