はじめに:画像管理の悩み
WordPressで記事を更新するとき、こんな経験はありませんか?
記事を編集
↓
画像を差し替え
↓
新しい画像をアップロード
↓
古い画像は放置...気づけば、WordPress Media Libraryに同じような画像が大量に溜まっている。

screenshot.png
screenshot-2.png
screenshot-3.png
screenshot-4.png
...どれが最新の画像なのか分からない!
この記事では、私がWordPress自動投稿システムを作る中で遭遇した画像管理の問題と、それを解決したハッシュベースの仕組みについて詳しく解説します。
問題の発生:画像が増え続ける
最初のシステム
私は最初、シンプルなシステムを作りました:
// 画像をアップロード
async function uploadImage(imagePath, fileName) {
const form = new FormData();
form.append('file', fs.createReadStream(imagePath));
const response = await axios.post(
`${WP_URL}/wp-json/wp/v2/media`,
form,
{ auth: { username: WP_USER, password: WP_APP_PASSWORD } }
);
return response.data.source_url;
}
// 使用
for (const imageFile of imageFiles) {
const url = await uploadImage(imagePath, imageFile);
console.log(`✅ ${imageFile} → ${url}`);
}動作確認:
npm run publish✅ screenshot.png → http://example.com/wp-content/uploads/2026/01/screenshot.png素晴らしい!動いた!
しかし、問題が...
記事を少し修正して、もう一度実行:
npm run publish✅ screenshot.png → http://example.com/wp-content/uploads/2026/01/screenshot-2.pngあれ?screenshot-2.png になってる...
WordPress Media Library:
screenshot.png (古い)
screenshot-2.png (新しい)さらに3回目:
✅ screenshot.png → .../screenshot-3.pngWordPress Media Library:
screenshot.png (古い)
screenshot-2.png (古い)
screenshot-3.png (新しい)画像が増え続ける!😱
なぜこうなるのか?
WordPress REST APIの仕様:
同じファイル名でアップロード
↓
WordPress: 「既にある!」
↓
自動的に連番を付ける
↓
screenshot-2.pngつまり、WordPressには「上書き」という概念がない。
これは、誤って既存の画像を上書きしないための安全機構ですが、私のユースケースでは問題になりました。
解決策の模索
方法1:ファイル名にタイムスタンプを付ける
アイデア:
const timestamp = Date.now();
const newFileName = `screenshot-${timestamp}.png`;結果:
screenshot-1736584800000.png
screenshot-1736584801234.png
screenshot-1736584802567.png問題:
- ❌ 画像は増え続ける(解決していない)
- ❌ ファイル名が分かりにくい
却下。
方法2:既存画像を削除して再アップロード
アイデア:
// 1. 既存画像を検索
const existingImages = await searchImages(slug);
// 2. 削除
for (const image of existingImages) {
await deleteImage(image.id);
}
// 3. 再アップロード
await uploadImage(imagePath, fileName);メリット:
- ✅ 画像は増えない
デメリット:
- ❌ 毎回削除&再アップロード(遅い)
- ❌ 画像が変わっていなくても処理される(無駄)
- ❌ 他の記事で使っている画像を削除してしまうリスク
もっと良い方法はないか?
方法3:既存画像を再利用(改善)
アイデア:
// 1. 既存画像を検索
const existingImage = await searchImage(fileName);
if (existingImage) {
// 2. 見つかったら再利用
console.log(`♻️ ${fileName} (再利用)`);
return existingImage.url;
} else {
// 3. 見つからなければアップロード
console.log(`✅ ${fileName} (新規)`);
return await uploadImage(imagePath, fileName);
}メリット:
- ✅ 画像は増えない
- ✅ 再アップロード不要(高速)
しかし、新たな問題が...
方法3の問題点
シナリオ:画像を更新したい
1. スクリーンショットを撮り直す
2. 同じファイル名(screenshot.png)で保存
3. スクリプト実行期待:
新しい画像がアップロードされる実際:
♻️ screenshot.png (再利用) ← 古い画像を再利用してしまう問題:
- ファイル名が同じ
- でも内容は違う
- これを検出できない
もっと賢い方法が必要だ。
ハッシュベースの画像管理
ハッシュとは?
**ハッシュ(Hash)**とは、データから計算される固定長の文字列です。
データ → ハッシュ関数 → ハッシュ値重要な性質:
- 同じデータ → 同じハッシュ
- 異なるデータ → 異なるハッシュ
- 元のデータを復元できない(一方向)
例:MD5ハッシュ
"Hello, World!" → 65a8e27d8879283831b664bd8b7f0ad4
"Hello, World?" → 1e825dfe1c22c5d6b46c87b61f7a3f2aたった1文字違うだけで、まったく異なるハッシュになります。
画像のハッシュを計算
Node.jsでの実装:
const crypto = require('crypto');
const fs = require('fs');
function calculateFileHash(filePath) {
// 1. ファイルを読み込み
const fileBuffer = fs.readFileSync(filePath);
// 2. MD5ハッシュを計算
const hashSum = crypto.createHash('md5');
hashSum.update(fileBuffer);
// 3. 16進数に変換
const hex = hashSum.digest('hex');
// 4. 最初の6桁を返す
return hex.substring(0, 6);
}使用例:
const hash1 = calculateFileHash('screenshot.png');
console.log(hash1); // a3f2b1
// 画像を編集して保存
const hash2 = calculateFileHash('screenshot.png');
console.log(hash2); // 9e8f7d ← 変わった!完璧!これで画像の内容が変わったかどうかを検出できる。
ハッシュをファイル名に含める
アイデア:
元のファイル名: screenshot.png
↓ ハッシュ計算
ハッシュ: a3f2b1
↓ ファイル名生成
新しいファイル名: article-slug-screenshot-a3f2b1.png実装:
async function uploadImageWithHash(imagePath, fileName, slug) {
// 1. ハッシュ計算
const hash = calculateFileHash(imagePath);
// 2. ファイル名生成
const ext = path.extname(fileName);
const nameWithoutExt = path.basename(fileName, ext);
const newFileName = `${slug}-${nameWithoutExt}-${hash}${ext}`;
// 3. アップロード
const form = new FormData();
form.append('file', fs.createReadStream(imagePath), newFileName);
const response = await axios.post(
`${WP_URL}/wp-json/wp/v2/media`,
form,
{ auth: { username: WP_USER, password: WP_APP_PASSWORD } }
);
console.log(`✅ ${fileName} → ${newFileName}`);
return response.data;
}結果:
✅ screenshot.png → article-name-screenshot-a3f2b1.pngメリット:
- ✅ ファイル名にハッシュが含まれる
- ✅ 内容が変われば、ハッシュも変わる
- ✅ ファイル名から内容の変更が分かる
既存画像の検索
次のステップ:既存画像を探す
async function findExistingImageByPattern(slug, originalFileName) {
// 1. 検索パターン作成
const nameWithoutExt = path.basename(originalFileName, path.extname(originalFileName));
const searchPattern = `${slug}-${nameWithoutExt}`;
// 2. WordPress Media Libraryを検索
const response = await axios.get(
`${WP_URL}/wp-json/wp/v2/media`,
{
auth: { username: WP_USER, password: WP_APP_PASSWORD },
params: {
search: searchPattern, // 例:article-name-screenshot
per_page: 10
}
}
);
// 3. パターンに一致する画像を探す
for (const media of response.data) {
const fileName = path.basename(media.source_url);
if (fileName.startsWith(searchPattern)) {
return media; // 見つかった!
}
}
return null; // 見つからない
}動作例:
const existingImage = await findExistingImageByPattern('article-name', 'screenshot.png');
if (existingImage) {
console.log('既存画像:', existingImage.source_url);
// 例:article-name-screenshot-a3f2b1.png
}
統合:再利用 or 削除&再アップロード
最終的な実装:
async function uploadOrReuseImage(imagePath, fileName, slug) {
// 1. 現在の画像のハッシュを計算
const currentHash = calculateFileHash(imagePath);
// 2. 新しいファイル名を生成
const ext = path.extname(fileName);
const nameWithoutExt = path.basename(fileName, ext);
const newFileName = `${slug}-${nameWithoutExt}-${currentHash}${ext}`;
// 3. 既存画像を検索
const existingImage = await findExistingImageByPattern(slug, fileName);
if (existingImage) {
const existingFileName = path.basename(existingImage.source_url);
// 4. ハッシュを比較
if (existingFileName.includes(`-${currentHash}${ext}`)) {
// ハッシュ一致 → 再利用
console.log(`♻️ ${fileName} (変更なし)`);
return {
id: existingImage.id,
url: existingImage.source_url
};
} else {
// ハッシュ不一致 → 古い画像を削除
console.log(`🗑️ 古い画像を削除: ${existingFileName}`);
await deleteImage(existingImage.id);
}
}
// 5. 新規アップロード
console.log(`✅ ${fileName} → ${newFileName}`);
return await uploadImage(imagePath, newFileName);
}画像削除の実装:
async function deleteImage(imageId) {
try {
await axios.delete(
`${WP_URL}/wp-json/wp/v2/media/${imageId}`,
{
auth: { username: WP_USER, password: WP_APP_PASSWORD },
params: {
force: true // 完全削除(ゴミ箱に入れない)
}
}
);
} catch (error) {
console.error(`画像削除エラー (ID: ${imageId}):`, error.message);
}
}
実際の動作
ケース1:初回アップロード
npm run publish処理:
screenshot.png を読み込み
↓
ハッシュ計算: a3f2b1
↓
既存画像を検索: 見つからない
↓
新規アップロード出力:
✅ screenshot.png → article-name-screenshot-a3f2b1.pngWordPress Media Library:
article-name-screenshot-a3f2b1.png (新規)
ケース2:2回目実行(画像変更なし)
記事を少し編集(画像は触らない):
npm run publish処理:
screenshot.png を読み込み
↓
ハッシュ計算: a3f2b1 (同じ)
↓
既存画像を検索: 見つかった
↓
ハッシュ比較: a3f2b1 == a3f2b1
↓
再利用!出力:
♻️ screenshot.png (変更なし)WordPress Media Library:
article-name-screenshot-a3f2b1.png (既存のまま)再アップロードしない!高速!
ケース3:画像を更新
スクリーンショットを撮り直して、同じファイル名で保存:
npm run publish処理:
screenshot.png を読み込み(内容が変わっている)
↓
ハッシュ計算: 9e8f7d (変わった!)
↓
既存画像を検索: 見つかった
↓
ハッシュ比較: 9e8f7d != a3f2b1
↓
古い画像を削除
↓
新規アップロード出力:
🗑️ 古い画像を削除: article-name-screenshot-a3f2b1.png
✅ screenshot.png → article-name-screenshot-9e8f7d.pngWordPress Media Library:
article-name-screenshot-9e8f7d.png (新しい)古い画像は削除されている!
ケース4:複数画像の同時更新
3つの画像があって、2つを更新:
01-install.png (更新)
02-config.png (更新)
03-run.png (変更なし)npm run publish出力:
🗑️ 古い画像を削除: article-name-01-install-a3f2b1.png
✅ 01-install.png → article-name-01-install-7d9e4c.png
🗑️ 古い画像を削除: article-name-02-config-b5c8f2.png
✅ 02-config.png → article-name-02-config-3a6d91.png
♻️ 03-run.png (変更なし)WordPress Media Library:
article-name-01-install-7d9e4c.png (新しい)
article-name-02-config-3a6d91.png (新しい)
article-name-03-run-c9a1b3.png (既存)変更がない画像は再利用!効率的!
パフォーマンスの比較
従来の方式(毎回アップロード)
記事更新(画像3枚、変更なし)
↓
3枚すべて再アップロード
↓
所要時間: 約5秒
↓
WordPress Media Library: 画像が増えるハッシュベース(賢く判断)
記事更新(画像3枚、変更なし)
↓
3枚すべて再利用
↓
所要時間: 約0.5秒
↓
WordPress Media Library: 変化なし約10倍高速!
ハッシュの衝突について
衝突とは?
異なる画像が同じハッシュになること:
画像A → ハッシュ: a3f2b1
画像B → ハッシュ: a3f2b1 ← 衝突どれくらいの確率?
6桁の16進数(3バイト):
16^6 = 16,777,216 通り現実的には:
- 1つの記事で10枚の画像を使用
- 1,677,721記事まで衝突しない計算
個人ブログでは、まず起こりえない。
もし衝突したら?
桁数を増やせばOK:
return hex.substring(0, 8); // 6 → 8桁8桁の場合:
16^8 = 4,294,967,296 通り42億通り!完璧。
実装時の注意点
1. ファイル名の形式
推奨:
{スラッグ}-{元のファイル名}-{ハッシュ}.{拡張子}例:
article-name-screenshot-a3f2b1.png
article-name-featured-7c4d8e.jpgメリット:
- どの記事の画像か一目瞭然
- 元のファイル名が分かる
- ハッシュで内容が分かる
2. 削除のタイミング
選択肢A:即座に削除(採用)
await deleteImage(existingImage.id);
await uploadImage(imagePath, newFileName);メリット:
- シンプル
- Media Libraryがクリーン
デメリット:
- アップロードが失敗したら、古い画像も削除済み
選択肢B:アップロード後に削除
const newImage = await uploadImage(imagePath, newFileName);
await deleteImage(existingImage.id);メリット:
- より安全
デメリット:
- 一時的に両方の画像が存在
私は選択肢Aを採用しました(シンプルさ優先)。
3. エラーハンドリング
画像削除が失敗しても続行:
async function deleteImage(imageId) {
try {
await axios.delete(...);
} catch (error) {
console.error(`⚠️ 画像削除エラー (ID: ${imageId}):`, error.message);
// エラーを投げない → 続行
}
}理由:
- 削除に失敗しても、新規アップロードは続けたい
- 古い画像が残るだけなので、大きな問題ではない
4. 複数記事で同じ画像を使う場合
例:ロゴ画像
記事A: article-a-logo-a3f2b1.png
記事B: article-b-logo-a3f2b1.pngハッシュは同じだが、スラッグが違うので別ファイル。
メリット:
- 記事ごとに独立して管理
- 一方を削除しても他方に影響なし
もし完全に共有したい場合:
- スラッグなしで
logo-a3f2b1.png - ただし、記事ごとの管理が複雑になる
私は記事ごとの管理を優先しました。
応用:画像の圧縮
ハッシュベースの仕組みは、画像の圧縮にも使えます。
シナリオ
1. TinyPNGで画像を圧縮
2. 同じファイル名で保存
3. スクリプト実行処理:
screenshot.png (圧縮後)
↓
ハッシュ計算: 3f8d2a (変わった)
↓
既存画像を検索: 見つかった
↓
ハッシュ比較: 3f8d2a != a3f2b1
↓
古い画像を削除
↓
新しい画像をアップロード結果:
🗑️ 古い画像を削除: article-name-screenshot-a3f2b1.png (1.2MB)
✅ screenshot.png → article-name-screenshot-3f8d2a.png (300KB)自動的に軽量化された画像に差し替わる!
まとめ
ハッシュベースの画像管理のメリット
1. 画像が増えない
- 古い画像は自動削除
- Media Libraryがクリーン
2. 無駄なアップロードを回避
- 変更がない画像は再利用
- 処理が高速
3. 完全自動
- ファイル名を変える必要なし
- 画像を上書き保存すればOK
4. 賢い判断
- 内容が変わったかを自動検出
- ユーザーは何も考えなくていい
実装のポイント
1. ハッシュ計算
const hash = crypto.createHash('md5').update(fileBuffer).digest('hex');2. ファイル名にハッシュを含める
const newFileName = `${slug}-${nameWithoutExt}-${hash}${ext}`;3. 既存画像を検索
const existingImage = await findExistingImageByPattern(slug, fileName);4. ハッシュを比較
if (existingFileName.includes(`-${hash}`)) {
// 再利用
} else {
// 削除 → 再アップロード
}学んだこと
1. 問題を分解する
- 「画像が増える」という問題
- → 「内容の変更を検出できない」という本質
2. 適切なツールを使う
- ハッシュは内容の変更検出に最適
3. ユーザー体験を優先
- 「ファイル名を変える」という手間を省く
4. 試行錯誤の価値
- 最初からベストな方法は見つからない
- 改善を繰り返すことが重要
おわりに
最初は「画像をアップロードする」だけのシンプルな機能でした。
しかし、実際に使ってみると、画像が増え続けるという問題に直面しました。
この問題を解決する過程で、ハッシュベースの画像管理という、想像以上に完成度の高い仕組みを作ることができました。
もし同じような問題に悩んでいる方がいたら、この記事が参考になれば嬉しいです。
そして、「問題を分解して、本質を見つける」というアプローチは、他の場面でも役立つはずです。
Happy Coding! 🚀
参考リンク
最終更新: 2026-01-11
コメント