- Server: resize uploads to max 1200px + generate 600px _thumb.jpg - Server: cache /uploads with max-age=1y immutable headers - Client: CatCard uses _thumb.jpg in feed (fallback to full on error) - Client: first 3 feed cards load eagerly (LCP), rest lazy + decoding=async - Client: loading placeholder bg while image fetches - Migration: server/src/scripts/gen-thumbs.ts for existing images Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
44 lines
1.2 KiB
TypeScript
44 lines
1.2 KiB
TypeScript
import fs from 'fs';
|
|
import path from 'path';
|
|
import { fileURLToPath } from 'url';
|
|
import sharp from 'sharp';
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const uploadsDir = path.join(__dirname, '..', '..', 'uploads');
|
|
|
|
async function main() {
|
|
const files = fs.readdirSync(uploadsDir);
|
|
const candidates = files.filter(
|
|
f => f.endsWith('.jpg') && !f.endsWith('_thumb.jpg')
|
|
);
|
|
|
|
console.log(`Found ${candidates.length} images to process`);
|
|
let created = 0;
|
|
let skipped = 0;
|
|
|
|
for (const filename of candidates) {
|
|
const thumbFilename = filename.replace(/\.jpg$/, '_thumb.jpg');
|
|
const thumbPath = path.join(uploadsDir, thumbFilename);
|
|
|
|
if (fs.existsSync(thumbPath)) {
|
|
skipped++;
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
await sharp(path.join(uploadsDir, filename))
|
|
.resize({ width: 600, withoutEnlargement: true })
|
|
.jpeg({ quality: 80 })
|
|
.toFile(thumbPath);
|
|
created++;
|
|
console.log(` ✓ ${thumbFilename}`);
|
|
} catch (err) {
|
|
console.error(` ✗ ${filename}: ${err}`);
|
|
}
|
|
}
|
|
|
|
console.log(`\nDone: ${created} created, ${skipped} already existed`);
|
|
}
|
|
|
|
main().catch(console.error);
|