/* * Generate pixelated placeholder images for the website. * * Scans website/public/ for all image files (png, jpg, jpeg, webp) and * generates a tiny 64px-wide version into src/assets/placeholders/. * When displayed at full size with CSS `image-rendering: pixelated` * (nearest-neighbor / point sampling), these produce a crisp mosaic effect. * * Output goes to src/assets/placeholders/ (NOT public/) so that Vite's * asset pipeline processes them. Since all placeholders are < 4KB, Vite * automatically inlines them as base64 data URIs via assetsInlineLimit, * eliminating extra HTTP requests. * * Skips files that already have the `placeholder-` prefix. * Skips regeneration if the placeholder is newer than the source image. * * Usage: tsx website/scripts/generate-placeholders.ts */ import sharp from 'sharp' import path from 'node:path' import fs from 'node:fs' const PUBLIC_DIR = path.resolve(import.meta.dirname, '../public') const OUTPUT_DIR = path.resolve(import.meta.dirname, '../src/assets/placeholders') const PLACEHOLDER_PREFIX = 'placeholder-' const PLACEHOLDER_WIDTH = 64 const IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.webp']) async function main() { fs.mkdirSync(OUTPUT_DIR, { recursive: true }) const entries = fs.readdirSync(PUBLIC_DIR) const images = entries.filter((name) => { if (name.startsWith(PLACEHOLDER_PREFIX)) { return false } const ext = path.extname(name).toLowerCase() return IMAGE_EXTENSIONS.has(ext) }) if (images.length === 0) { console.error('No images found in public/') return } for (const name of images) { const inputPath = path.join(PUBLIC_DIR, name) const ext = path.extname(name) const base = path.basename(name, ext) const outputPath = path.join(OUTPUT_DIR, `${PLACEHOLDER_PREFIX}${base}${ext}`) // Skip if placeholder already exists and is newer than source if (fs.existsSync(outputPath)) { const srcMtime = fs.statSync(inputPath).mtimeMs const outMtime = fs.statSync(outputPath).mtimeMs if (outMtime > srcMtime) { continue } } await sharp(inputPath).resize(PLACEHOLDER_WIDTH).png({ compressionLevel: 9 }).toFile(outputPath) const stats = fs.statSync(outputPath) console.error(`Generated ${PLACEHOLDER_PREFIX}${base}${ext} (${PLACEHOLDER_WIDTH}px wide, ${stats.size} bytes)`) } } main()