diff --git a/next.config.ts b/next.config.ts index b17aaa3..94d6785 100644 --- a/next.config.ts +++ b/next.config.ts @@ -26,18 +26,13 @@ const nextConfig: NextConfig = { return config }, images: { + formats: ['image/avif', 'image/webp'], + deviceSizes: [375, 640, 768, 1024, 1280, 1536, 1920], + imageSizes: [16, 32, 64, 96, 128, 256, 384, 512, 694], + minimumCacheTTL: 31536000, remotePatterns: [ - { - protocol: 'http', - hostname: 'localhost', - port: '3000', - pathname: '/media/**', - }, - { - protocol: 'https', - hostname: 'shumiland.com.ua', - pathname: '/media/**', - }, + { protocol: 'http', hostname: 'localhost', port: '3000', pathname: '/media/**' }, + { protocol: 'https', hostname: 'shumiland.com.ua', pathname: '/media/**' }, ], }, } diff --git a/scripts/optimize-images.manifest.json b/scripts/optimize-images.manifest.json new file mode 100644 index 0000000..e0fc830 --- /dev/null +++ b/scripts/optimize-images.manifest.json @@ -0,0 +1,75 @@ +{ + "deletedDuplicates": [ + "081e52b5-d35a-41d2-b506-a9d751b0b563", + "109f13b4-25f9-4f94-a06d-77421ff2b4fe", + "17d78753-cec3-40ca-a1fc-4125c2b79eff", + "2936ec5e-4f99-441e-9bf2-34f23c283170", + "2c6a3e5e-7346-4c3e-b8a0-fae1facb87ad", + "455670b9-c89a-4bea-81fe-5a4d93b25483", + "5088f41f-b898-4958-b7f6-519496b65382", + "58218f84-457d-4ff5-8d4e-2ace40b45568", + "640999e1-7096-4623-bf96-12bb8ef62ffc", + "7a2627b2-b6ce-4325-a0b1-fbc3393aca4c", + "908a8aab-6129-4d7d-b7ac-c43b6fa60044", + "abacda18-57c7-441d-9313-c70d22c6f0f0", + "ac7f3a5e-0e66-4971-bccc-89901a7c314d", + "b5597790-7b54-4ad7-9977-4cb15633286e", + "c3053789-6cd0-4dda-9774-d4ae4bc400e1", + "de9ad287-5e83-4a0c-ad06-f2420bff4096", + "e42d6611-82e6-47b0-9d84-271f9810ab1a", + "e9a8cee6-6ee5-4c74-b270-1c133a762c0a", + "f4e2bff2-754c-460c-baba-baa95521bfc9", + "f5d32fc0-8ea7-4fd1-b193-819d6aa1a68e" + ], + "encoded": [], + "skipped": [ + "081e52b5-d35a-41d2-b506-a9d751b0b563.png (webp newer)", + "109f13b4-25f9-4f94-a06d-77421ff2b4fe.jpg (webp newer)", + "17d78753-cec3-40ca-a1fc-4125c2b79eff.png (webp newer)", + "2936ec5e-4f99-441e-9bf2-34f23c283170.jpg (webp newer)", + "2c6a3e5e-7346-4c3e-b8a0-fae1facb87ad.jpg (webp newer)", + "455670b9-c89a-4bea-81fe-5a4d93b25483.png (webp newer)", + "5088f41f-b898-4958-b7f6-519496b65382.png", + "58218f84-457d-4ff5-8d4e-2ace40b45568.png (webp newer)", + "640999e1-7096-4623-bf96-12bb8ef62ffc.png (webp newer)", + "7a2627b2-b6ce-4325-a0b1-fbc3393aca4c.png (webp newer)", + "908a8aab-6129-4d7d-b7ac-c43b6fa60044.png (webp newer)", + "abacda18-57c7-441d-9313-c70d22c6f0f0.png (webp newer)", + "ac7f3a5e-0e66-4971-bccc-89901a7c314d.png (webp newer)", + "b5597790-7b54-4ad7-9977-4cb15633286e.png", + "c3053789-6cd0-4dda-9774-d4ae4bc400e1.png (webp newer)", + "check-mark.png", + "de9ad287-5e83-4a0c-ad06-f2420bff4096.png", + "e42d6611-82e6-47b0-9d84-271f9810ab1a.png (webp newer)", + "e9a8cee6-6ee5-4c74-b270-1c133a762c0a.png (webp newer)", + "f4e2bff2-754c-460c-baba-baa95521bfc9.png (webp newer)", + "f5d32fc0-8ea7-4fd1-b193-819d6aa1a68e.png (webp newer)", + "footer-bg.png", + "gallery-1.png", + "gallery-2.png (webp newer)", + "gallery-3.png", + "gallery-4.png (webp newer)", + "gallery-6.png (webp newer)", + "gallery-7.png (webp newer)", + "gallery-8.png (webp newer)", + "hero-bg-family.png (webp newer)", + "hero-bg1.png (webp newer)", + "hero-bg2.png (webp newer)", + "hero-blur-mask.png", + "loc-dinopark.jpg (webp newer)", + "loc-divo-lis.png (webp newer)", + "loc-map.jpg (webp newer)", + "news-bg1.jpg", + "news-bg2.png (webp newer)", + "news-bg3.jpg (webp newer)", + "news-bg4.png (webp newer)", + "news-bg5.png (webp newer)", + "news-bg6.png (webp newer)", + "review-avatar-bg.jpg (webp newer)", + "video-preview.png (webp newer)", + "why-parents-1.png (webp newer)", + "why-parents-2.png (webp newer)", + "why-parents-3.png (webp newer)", + "why-parents-4.png (webp newer)" + ] +} \ No newline at end of file diff --git a/scripts/optimize-images.mjs b/scripts/optimize-images.mjs new file mode 100644 index 0000000..9513e90 --- /dev/null +++ b/scripts/optimize-images.mjs @@ -0,0 +1,49 @@ +#!/usr/bin/env node +import fs from 'node:fs/promises' +import path from 'node:path' +import sharp from 'sharp' + +const ROOT = path.resolve(process.cwd(), 'public/images/figma') +const QUALITY = 82 +const MIN_BYTES = 200_000 + +const manifest = { deletedDuplicates: [], encoded: [], skipped: [] } + +const all = await fs.readdir(ROOT) +const byBase = new Map() +for (const f of all) { + const ext = path.extname(f) + const base = ext ? path.basename(f, ext) : f + if (!byBase.has(base)) byBase.set(base, []) + byBase.get(base).push(f) +} + +// 1) Delete extensionless duplicates that have a webp/png/jpg sibling +for (const [, variants] of byBase) { + const extensionless = variants.find((v) => path.extname(v) === '') + const hasSibling = variants.some((v) => /\.(webp|png|jpg|jpeg)$/i.test(v)) + if (extensionless && hasSibling) { + const p = path.join(ROOT, extensionless) + await fs.unlink(p) + manifest.deletedDuplicates.push(path.basename(p)) + } +} + +// 2) Re-encode large png/jpg to webp (skip if webp already exists and is newer) +for (const [base, variants] of byBase) { + const heavy = variants.find((v) => /\.(png|jpg|jpeg)$/i.test(v)) + if (!heavy) continue + const srcPath = path.join(ROOT, heavy) + const stat = await fs.stat(srcPath).catch(() => null) + if (!stat || stat.size < MIN_BYTES) { manifest.skipped.push(path.basename(srcPath)); continue } + const outPath = path.join(ROOT, `${base}.webp`) + const outStat = await fs.stat(outPath).catch(() => null) + if (outStat && outStat.mtimeMs > stat.mtimeMs) { manifest.skipped.push(path.basename(srcPath) + ' (webp newer)'); continue } + await sharp(srcPath).webp({ quality: QUALITY }).toFile(outPath) + manifest.encoded.push({ src: path.basename(srcPath), out: `${base}.webp`, before: stat.size }) +} + +const manifestPath = path.join(process.cwd(), 'scripts/optimize-images.manifest.json') +await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2)) +console.log(`Deleted ${manifest.deletedDuplicates.length} duplicates, encoded ${manifest.encoded.length} files, skipped ${manifest.skipped.length}.`) +console.log('Manifest:', manifestPath)