From ee08d3491b487dea16855eec17bb5e0a2dfecd25 Mon Sep 17 00:00:00 2001 From: Yasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com> Date: Tue, 6 Oct 2026 13:40:04 +0900 Subject: Astro 7へ更新し、画像をsrc/へ移行・重複を整理 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - astro 6→7 / starlight 0.40→0.42 / sharp更新、markdown-remark導入 - markdownをunified()へ移行、compressHTML:trueで従来の空白を維持 - starlight日本語訳パッチを0.42.5向けに再生成 - 有益な画像をsrc/へ移行しpublic重複を削除、孤立画像を整理 - scripts/optimize-images.mjs を追加(prebuildで自動実行) --- scripts/optimize-images.mjs | 100 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 scripts/optimize-images.mjs (limited to 'scripts') diff --git a/scripts/optimize-images.mjs b/scripts/optimize-images.mjs new file mode 100644 index 0000000..8e9194a --- /dev/null +++ b/scripts/optimize-images.mjs @@ -0,0 +1,100 @@ +// public/ の画像を安全に最適化するスクリプト。 +// +// 方針(背景): +// Astro(sharo/libwebp) の既定は「写真向けの非可逆WebP」なので、既に最適化済みの +// パレットPNG(ディザあり)やロスレスWebPをWebP化すると、かえって肥大化する。 +// そのため public/ では次を行う: +// - JPEG … 同一拡張子のまま再圧縮し、15%以上小さくなる場合だけ置換する(安全・冪等) +// - PNG/WebP … 「WebP化でどれだけ小さくなるか」をレポートするだけ +// (拡張子が変わると参照URLが壊れるため、自動では変換しない。Astro移行で対応する) +// +// 使い方: +// node scripts/optimize-images.mjs # 判定のみ(書き換えない) +// node scripts/optimize-images.mjs --apply # JPEGを実際に最適化 +// +// `npm run build` の prebuild で --apply が自動実行される。 +import { readdirSync, statSync } from "node:fs"; +import { readFile, writeFile } from "node:fs/promises"; +import path from "node:path"; +import sharp from "sharp"; + +const APPLY = process.argv.includes("--apply"); +const ROOTS = ["public"]; +const EXCLUDE_DIRS = [path.join("public", "bluebey-studio")]; +const EXTS = new Set([".jpg", ".jpeg", ".png", ".webp"]); +const MIN_SIZE = 15000; // 15KB未満は対象外 +const MIN_GAIN = 0.15; // 15%以上小さくなる場合だけ置換 +const WEBP_SUGGEST_GAIN = 0.2; // WebP移行候補として報告する閾値 + +function* walk(dir) { + for (const e of readdirSync(dir, { withFileTypes: true })) { + const p = path.join(dir, e.name); + if (e.isDirectory()) { + if (EXCLUDE_DIRS.includes(p)) continue; + yield* walk(p); + } else { + yield p; + } + } +} + +// Windows では sharp が入力ファイルのハンドルを保持すると、同一パスへの書き込みが +// ロックされる。そのため、先にバッファへ読み込んでから変換する。 +async function encodeJpeg(buf) { + return sharp(buf, { failOn: "none" }).jpeg({ quality: 82, mozjpeg: true }).toBuffer(); +} +async function encodeWebp(buf) { + return sharp(buf, { failOn: "none" }).webp({ quality: 80 }).toBuffer(); +} + +let checked = 0; +let changed = 0; +let saved = 0; +const suggestions = []; + +for (const root of ROOTS) { + for (const file of walk(root)) { + const ext = path.extname(file).toLowerCase(); + if (!EXTS.has(ext)) continue; + const size = statSync(file).size; + if (size < MIN_SIZE) continue; + checked++; + try { + const input = await readFile(file); + // 形式変換(WebP)の効果を測る(自動適用はしない) + if (ext !== ".webp") { + const w = await encodeWebp(input); + if (w.length < size * (1 - WEBP_SUGGEST_GAIN)) { + suggestions.push( + `${file} (${(size / 1024).toFixed(0)}K -> webp ${(w.length / 1024).toFixed(0)}K)`, + ); + } + } + // JPEGは同一拡張子で安全に再圧縮 + if (ext === ".jpg" || ext === ".jpeg") { + const j = await encodeJpeg(input); + const gain = 1 - j.length / size; + if (gain >= MIN_GAIN) { + if (APPLY) await writeFile(file, j); + changed++; + saved += size - j.length; + console.log( + `${APPLY ? "OPT" : "would"} ${(size / 1024).toFixed(0)}K -> ${(j.length / 1024).toFixed(0)}K ${file}`, + ); + } + } + } catch (e) { + console.log("ERR", file, e.message); + } + } +} + +console.log( + `\nchecked=${checked} jpegOptimized=${changed} saved=${(saved / 1024).toFixed(0)}KB${APPLY ? "" : " (dry-run)"}`, +); +if (suggestions.length) { + console.log( + `\n[WebP移行候補 ${suggestions.length}件](拡張子が変わるため自動変換しません。Astro移行を検討)`, + ); + for (const s of suggestions) console.log(" " + s); +} -- cgit v1.3.1