Claude Codeでブログに上げる画像を一括リサイズする方法|SEOを保つNode.jsツール自作

概要

今回はClaude Codeに「画像縮小して」と頼んだら、ブログ用画像が自動でSEOを保ったままリサイズされるようにするための方法について紹介していきます。

結論から書くと、ブログ画像28ファイルが 14.6MB→10.0MB(-32%) まで縮みました。

ブログを長く運用していると、いつのまにかフォルダに1.5MBを超える写真がゴロゴロ転がっていて、cloneも遅いしGitHubにpushするときも気が重い…ということが起きますよね。

かといって何も考えずにsharpで再エンコードすると、PNGが3倍に膨らんだり、低品質JPGが逆に大きくなる事故が起きます(実際に私もやらかしました(- -;)。

そこでClaude CodeのSkill機能を使って、「画像縮小して」と一声かけたら、安全装置付きの自作ツールが自動で走る仕組みを作りました。

それではやっていきましょう(^^!

目次

なぜClaude Code経由にしたいのか

画像リサイズは「やった方がいい」と分かっていても、毎回コマンドを思い出して叩くのが面倒で、結局放置されがちですよね。

自分の場合はVsCodeでブログを書いているので、コンソールで「画像縮小して」と頼めば、それだけで作業してくれる方が圧倒的に便利と感じました。

要はフォルダやファイルを指定して、コマンド実行するのが面倒だったんですよね(- -

Skill化しておけば、新しいPCでもリポジトリをcloneし.claude/skills/が同期されればすぐ使えるのも便利です。

SEOを守る縮小ルールの決め方

ここが今回の一番大事なところです。

闇雲にサイズを落とすと、Google画像検索の対象から外れたり、画質劣化で滞在時間が悪化したりするので、「これは絶対に守る」ラインを先に決めてから縮小幅を決めました。

最大幅 1600px の根拠

Google画像検索のガイドラインは幅1200px以上を推奨しています。

今回は余裕をもって1600pxに設定しました。
推奨を33%上回り、かつ以下も同時に満たします。

  • ブログのコンテンツ幅800px × Retina(2x) = 1600pxに一致
  • 4Kディスプレイで全画面表示しても破綻しない
  • Twitterカード/Facebook OG画像の推奨上限(1200×630 / 1200×1200)もカバー

これ以上大きくしても表示側で活かされないことが多いので、1600pxが「ちょうどよいライン」だと思ってます。

JPG品質 80 の根拠

mozjpeg(Mozillaが開発した高効率JPEGエンコーダ。同じ品質なら標準JPEGより1〜2割小さく書ける)で品質80は、写真画像で人間の目に劣化判別が困難とされる「実用的最高効率」帯です。

CloudinaryなどのCDN系も品質75〜82をデフォルトにしているので、業界的にも妥当なラインです。

実は品質70まで下げると風景写真でブロックノイズが見えてくることがあるので、品質80を「劣化リスクとファイルサイズ削減の程よいポイント」と判断しました。

PNG をどう扱うか

PNGは少し厄介で、sharpで何も考えずに再エンコードするとpalette PNG(色を256色までに絞った軽量PNG)をtruecolor RGBA(フルカラーPNG)として書き出して2〜3倍に膨らむ事故が起きます。

実際にこれをやらかしてしまい577KBが1442KBになりました…(- -;

なので結論として、PNGは「幅が1600pxを超えるとき」だけ処理する方針にしました。

容量だけが大きいPNGは触らない、という割り切りです。

容量が気になるPNGはpngquant等の専用ツール領域なので、そちらにお任せします。

ツールの実装(Node.js + sharp)

実装方針が決まったのでNode.js+sharpで書いていきます。

sharpを入れるだけで動くので、追加依存はほぼ気にしなくてOKです。

基本実装

ポイントだけ抜粋します。

resize_image.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import sharp from "sharp";
import fs from "fs";

async function resizeOne(file, opts) {
const beforeBytes = fs.statSync(file).size;
const meta = await sharp(file).metadata();
const beforeWidth = meta.width || 0;

const isJpeg = /\.(jpe?g)$/i.test(file);
const thresholdBytes = opts.thresholdKb * 1024;

// PNG は palette 事故を避けるため幅超過時のみ処理
const needsResize = beforeWidth > opts.maxWidth;
const needsRecompress = isJpeg && beforeBytes > thresholdBytes;
if (!needsResize && !needsRecompress) return { status: "skipped" };

const tmp = file + ".resize-tmp";
let pipeline = sharp(file)
.rotate() // EXIF回転を物理ピクセルに焼き込み
.resize({ width: opts.maxWidth, withoutEnlargement: true });

if (isJpeg) {
pipeline = pipeline.jpeg({ quality: opts.quality, mozjpeg: true });
} else {
pipeline = pipeline.png({ compressionLevel: 9, adaptiveFiltering: true });
}

await pipeline.toFile(tmp);
// ...セーフガードは後述
}

rotate()resize()の前に挟んでおくのは重要で、EXIF回転情報を物理ピクセルに焼き込まないと、リサイズ後に向きが反転する事故が起きます。

withoutEnlargement: trueも忘れずに(既に小さい画像を逆に拡大してしまうのを防ぎます)。

失敗談:最初の実行で容量が逆に+7%増えた話

ここからは順番が前後しますが、先に「セーフガードを入れる前にやらかした失敗」の話をさせてください。これを見てもらうと、次のセーフガードが何の対策なのか一目で分かるはずです。

セーフガードを入れる前の最初の実行で、73ファイルを一気に書き換えて、トータル容量が逆に+7%増えたという謎な失敗をしました。

1回目の悲惨な結果

1
[summary] resized=73 skipped=294 failed=0 total 27563KB -> 29385KB (+7%)

特に酷かったのは以下のようなPNGたちです(数値はサイズ増加率)。

  • とあるスクショ画像: 577KB → 1442KB(+150%
  • とあるスクショ画像: 385KB → 1212KB(+215%
  • とあるスクショ画像: 120KB → 244KB(+102%

sharpの標準PNGエンコードがtruecolor RGBAで書くため、元のpalette/indexed PNGを展開してしまったのが原因です。

上書き式・バックアップなしで設計していたため、原本は失われました。

git管理下だったのでgit checkout --で復旧できましたが、git管理外だったらアウトでした…。

リサイズ系ツールは絶対にgitで履歴を残してから実行してください。

セーフガード(事故防止)

この事故を防ぐために、「再エンコード結果が元より大きいか、改善が5%未満なら破棄して元を維持する」という1つのチェックを入れました。PNG paletteをtruecolorで書き戻す事故も、低品質JPGの再エンコードで微増する問題も、これだけで両方ブロックできます。

resize_image.js

1
2
3
4
5
6
7
8
// セーフガード: 悪化 or 微小改善(< 5%)なら破棄して元維持
const tmpBytes = fs.statSync(tmp).size;
const gainRatio = (beforeBytes - tmpBytes) / beforeBytes;
if (gainRatio < 0.05) {
fs.unlinkSync(tmp);
return { status: "no-gain", beforeBytes, tmpBytes };
}
fs.renameSync(tmp, file);

なぜ「5%未満も維持」かというと、毎回1KBずつ縮んで止まらない摩耗パターンを防ぐためです。

ファイルが書き換わるたびにmtimeが更新され、CIや派生ファイル生成も毎回走ってしまうので、ほぼ効果がない再エンコードは捨てるのが正解になります。

これで何度実行しても2回目以降はresized=0で完全に冪等(何回叩いても結果が変わらない状態)になります。

Claude CodeでSkill化する

ここからが本題です。

.claude/skills/resize-images/SKILL.md を作って、Claude Codeに「いつ・どのように」発動するかを教え込みます。

Skill定義の最小例

.claude/skills/resize-images/SKILL.md

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
---
name: resize-images
description: ブログ記事に追加した画像をSEOに支障のない範囲で自動縮小する(最大幅1600px / JPG品質80)。新規画像を追加したとき、またはユーザーが「画像縮小して」「画像リサイズ」「画像軽くして」「サイズ落として」等と依頼したときに発動する。
---

# resize-images スキル

## 発動条件
- 新規記事用に画像フォルダへ画像を追加した直後
- ユーザーが「画像縮小して」「リサイズして」等と指示したとき

## 実行手順
1. 対象画像フォルダを Glob で列挙
2. node "<path>/resize_image.js" "<dir>" を実行
3. ログを Read で確認し、削減率と失敗件数を報告

実際の発動の様子

私が普段ブログ作業中に使っているプロンプトはこんな感じです。

発動例

1
2
ユーザー: 記事に画像追加したから縮小して
Claude: resize-images skillを発動 → ツール実行 → 結果を報告
Skillは説明文(description)のキーワードマッチで発動するので、「縮小」「リサイズ」「軽くして」「サイズ落として」など、自分が普段使う言い回しを全部入れておくと発動率が上がります。

結果と検証

セーフガード入りで再実行した結果です。

正常な結果

1
[summary] resized=28 no-gain=37 skipped=302 failed=0 total 14982KB -> 10249KB (-32%)
  • resized=28: 実際に小さくなった画像
  • no-gain=37: 再エンコードで悪化 or 改善5%未満だったため元維持([keep]ログで個別表示)
  • skipped=302: しきい値以下で対象外
  • failed=0: 失敗ゼロ

[keep]でログに残るので、「ここは縮められなかった」が明示的に分かるのも安心材料です。

冪等性の確認

2回目を続けて実行した結果が以下です。

2回目

1
[summary] resized=0 no-gain=50 skipped=317 failed=0 total 0KB -> 0KB (-0%)

resized=0で完全に冪等

mtimeも変わらないので、CI/ビルドへの余計な負荷もありません。

まとめ

いかがでしたでしょうか?

今回のポイントをまとめると下記のような感じになります。

  • 最大幅1600px・JPG品質80 はGoogle画像検索とCore Web Vitalsの両方を満たすバランス点
  • PNGは幅超過時のみ処理 することでpalette事故を回避
  • 改善5%未満なら元維持 で冪等性と摩耗ゼロを両立
  • Claude CodeのSkillに登録 することで、普段の作業フローに自然に組み込める

画像最適化は地味だけど効果が確実にある作業なので、ブログを長く運用するなら一度きちんと向き合っておくと後がラクになると思います。

特にClaude Codeを使っているなら、「面倒な作業をSkill化して呼び出すだけにする」パターンはかなり応用が利くので、リサイズ以外の自動化にも広げてみるのがお勧めです(^^b

以上となります!
ブログを書く以外の時間はできるだけ圧縮していきたいです!!
それではお疲れさまでした!