【試行錯誤の記録】WordPress画像管理を最適化!ハッシュベースの賢い仕組み

Development
スポンサーリンク
スポンサーリンク

はじめに:画像管理の悩み

WordPressで記事を更新するとき、こんな経験はありませんか?

記事を編集
  ↓
画像を差し替え
  ↓
新しい画像をアップロード
  ↓
古い画像は放置...

気づけば、WordPress Media Libraryに同じような画像が大量に溜まっている

WordPressのメディアライブラリ

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.png

WordPress 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)**とは、データから計算される固定長の文字列です。

データ → ハッシュ関数 → ハッシュ値

重要な性質:

  1. 同じデータ → 同じハッシュ
  2. 異なるデータ → 異なるハッシュ
  3. 元のデータを復元できない(一方向)

例: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.png

WordPress 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.png

WordPress 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

コメント