WordPressで画像を差し替えても、ブラウザキャッシュのせいで古い画像が表示され続ける ── これを防ぐ定番テクニックが、画像URLへの ?ver=日時 の付与(キャッシュバスター)です。
以前、同じテーマで「the_content フィルターでレンダリング時に付与する」方法を紹介しました。しかしこの方式は ページキャッシュが有効な環境では機能しない という構造的な問題があり、実運用ではサーバーキャッシュに負けてしまうケースが頻発していました。
WordPressで画像のURLに最終更新日時をクエリパラメータとして追加する方法
そこで今回は、投稿保存時にデータベースへ直接 ?ver= を書き込む 方式に移行しました。この方式なら、ページキャッシュが何段積まれていても確実に ?ver= を届けられ、しかも Better Search Replace で複数ページを一括更新できるおまけ付きです。
以前の方式(the_content フィルター)の限界
まず、元の実装を振り返ります。
/* content内の画像URLに、最終更新日時をクエリ文字として追加 */
function add_version_query_to_images($content) {
global $post;
$version = (new DateTime($post->post_modified))->format('YmdHis');
// ... srcやsrcsetに ?ver= を付与 ...
return $content;
}
add_filter('the_content', 'add_version_query_to_images');
このコードは「投稿本文をブラウザに出す直前に、?ver= を差し込む」というアプローチでした。一見シンプルですが、運用すると以下の問題が表面化します。
問題1:ページキャッシュと相性が悪い
the_content フィルターは PHP が実行されて初めて動く処理です。ところが多くのレンタルサーバーでは、一度生成されたHTMLをキャッシュして、以後はPHPを回さずそのまま返します。
初回アクセス → PHP実行 → ?ver= 付与 → HTMLキャッシュ保存
2回目以降 → キャッシュHTML返却(PHPは動かない)
つまりキャッシュが生きている間は、投稿を更新しても 古い ?ver= のHTMLがずっと返り続けるのです。画像を差し替えても、DevToolsで見てもキャッシュが更新されない ── その正体はこれです。
問題2:レンダリング時の負荷
アクセスのたびに正規表現で本文を走査するため、投稿文が長いほど負荷になります。
問題3:Better Search Replace での一括更新が効かない
?ver= がデータベースに存在しない(出力時に動的に付くだけ)ので、データベースに対して置換処理をかけても何も起きません。複数ページの画像を差し替えたいときに、1ページずつ「更新」ボタンを押す必要があります。
新方式:content_save_pre で投稿保存時に書き込む
これら全てを解決するのが、投稿保存時にデータベースへ ?ver= を書き込んでしまう 方式です。
投稿保存 → content_save_pre で ?ver= 付与 → データベースに保存
表示時 → データベースから読み出してそのまま出力(フィルター不要)
メリットは3つ。
| 観点 | the_content 方式 | content_save_pre 方式 |
|---|---|---|
| ページキャッシュ | 効かない | 効く(HTMLにすでに?ver=が含まれる) |
| レンダリング負荷 | 毎回正規表現処理 | ゼロ |
| 一括更新 | 不可 | Better Search Replaceで可能 |
content_save_pre は、WordPressが post_content を保存する直前に適用されるフィルターです。wp_insert_post_data でも同等のことができますが、$data 配列全体を受け取るため除外条件が煩雑になりがちで、content_save_pre は $content だけを直接受け取れるためシンプルに書けます。
実装コード
以下を functions.php に追加してください。
/* ============================================================
* 投稿保存時に content 内の画像URLへ ?ver=YmdHis を付与/更新
* ------------------------------------------------------------
* - フック: content_save_pre(post_content を直接フィルター)
* - 対象タグ: <img>, <source>
* - 対象属性: src, data-src0, data-src744, srcset
* - 内部URL(自サイト or 相対パス)のみ処理
* - data: URI、外部URLは除外
* - 既存の ?ver= があれば最新値で置き換え
* ============================================================ */
add_filter('content_save_pre', 'iv_add_ver_on_save', 10, 1);
function iv_add_ver_on_save($content) {
// 自動保存・空コンテンツはスキップ
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return $content;
if (empty($content)) return $content;
// content_save_pre は slash-escape 済みで渡されるので一旦解除
$content = wp_unslash($content);
$version = current_time('YmdHis');
$site_host = parse_url(get_site_url(), PHP_URL_HOST);
// <img> タグを処理
$content = preg_replace_callback('/<imgb[^>]*>/i', function($m) use ($version, $site_host) {
return iv_process_tag($m[0], $version, $site_host);
}, $content);
// <picture> 内の <source> タグも処理
$content = preg_replace_callback('/<sourceb[^>]*>/i', function($m) use ($version, $site_host) {
return iv_process_tag($m[0], $version, $site_host);
}, $content);
// 戻すときは再度 slash-escape
return wp_slash($content);
}
/**
* 1つのタグ内の対象属性を順に処理
*/
function iv_process_tag($tag, $version, $site_host) {
// 単一URL系の属性
foreach (['src', 'data-src0', 'data-src744'] as $attr) {
$pattern = '/(b' . preg_quote($attr, '/') . 's*=s*)(["'])([^"']*)2/i';
$tag = preg_replace_callback($pattern, function($m) use ($version, $site_host) {
$new_url = iv_apply_ver($m[3], $version, $site_host);
return $m[1] . $m[2] . $new_url . $m[2];
}, $tag);
}
// srcset(カンマ区切り複数URL)
$tag = preg_replace_callback('/(bsrcsets*=s*)(["'])([^"']*)2/i', function($m) use ($version, $site_host) {
$new_set = iv_apply_ver_to_srcset($m[3], $version, $site_host);
return $m[1] . $m[2] . $new_set . $m[2];
}, $tag);
return $tag;
}
/**
* 単一URLに ?ver= を付与(既存があれば除去してから付与)
*/
function iv_apply_ver($url, $version, $site_host) {
if ($url === '' || strpos($url, 'data:') === 0) return $url;
// 絶対URL or プロトコル相対URL の場合、自サイト以外はスキップ
if (preg_match('/^(?:https?:)?///i', $url) && strpos($url, $site_host) === false) {
return $url;
}
// 既存の ver= を除去(3パターン対応)
$url = preg_replace('/?ver=[^&]*&/', '?', $url); // ?ver=xxx&yyy → ?yyy
$url = preg_replace('/&ver=[^&]*/', '', $url); // &ver=xxx → 除去
$url = preg_replace('/?ver=[^&]*$/', '', $url); // ?ver=xxx → 除去
// 新しい ver= を付与
$separator = (strpos($url, '?') !== false) ? '&' : '?';
return $url . $separator . 'ver=' . $version;
}
/**
* srcset 文字列内の各URLに ?ver= を付与
*/
function iv_apply_ver_to_srcset($srcset, $version, $site_host) {
$items = explode(',', $srcset);
foreach ($items as $i => $item) {
$item = trim($item);
if ($item === '') continue;
// URL とサイズ指定(1x, 744w 等)を分離
$parts = preg_split('/s+/', $item, 2);
$url = $parts[0];
$desc = isset($parts[1]) ? ' ' . $parts[1] : '';
$items[$i] = iv_apply_ver($url, $version, $site_host) . $desc;
}
return implode(', ', $items);
}
data-src0 / data-src744 は独自の遅延読み込み属性なので、他のプロジェクトで使っていない場合は配列から外してください。逆に独自属性を追加したい場合はこの配列に足すだけです。
実装のポイント
一見すると単純な文字列置換に見えますが、実際に書いてみると落とし穴がいくつかあります。
ポイント1:wp_unslash / wp_slash の往復は必須
content_save_pre に渡される $content は slash-escape された状態(" が " に)で来ます。これを知らずに ["'] でマッチを取ろうとすると、キャプチャグループに </code> が混入してURLが壊れ、書き換えが効きません。
処理の前に wp_unslash() で解除 → 処理 → wp_slash() で戻す、というサンドイッチ構造が正解です。
ポイント2:タグ単位で切り出してから処理する
最初は <img> タグを特定せず、src= を直接正規表現で探していましたが、[^>]* の貪欲マッチで別のタグまで巻き込む事故が起きます。
❌ /(img[^>]src=["'])([^"'])(["'])/
✅ 1. /<imgb[^>]*>/i でタグを切り出し
2. その中で /(bsrcs*=s*)(["'])([^"']*)2/i
2段階に分けることで、タグ境界を越えたマッチを防げます。
ポイント3:preg_quote で属性名をエスケープ
data-src0 のようなハイフンを含む属性名を正規表現に組み込むときは、preg_quote() でエスケープしておくと将来トラブルを回避できます。
ポイント4:既存 ?ver= の除去は3パターンに分ける
再保存のたびに ?ver= が増殖しないよう、既存の値を除去してから新しい値を付与します。3つのパターンを順番に処理すれば漏れがありません。
$url = preg_replace('/?ver=[^&]*&/', '?', $url); // ?ver=xxx&yyy → ?yyy
$url = preg_replace('/&ver=[^&]*/', '', $url); // &ver=xxx → 除去
$url = preg_replace('/?ver=[^&]*$/', '', $url); // ?ver=xxx → 除去
既存コードからの置き換え手順
the_content フィルター版を使っていた場合、旧コードは 必ず削除してください。両方有効にすると二重に ?ver= が付いてしまいます。
- add_filter('the_content', 'add_version_query_to_images');
- function add_version_query_to_images($content) { ... }
+ add_filter('content_save_pre', 'iv_add_ver_on_save', 10, 1);
+ function iv_add_ver_on_save($content) { ... }
切り替え後、既存の投稿を順次「更新」ボタンで再保存していくと、保存したページから順に ?ver= がデータベース側に書き込まれていきます。
画像差し替え時の運用フロー
この方式の最大のメリットが、画像だけ差し替えた場合でも一括対応できることです。
Better Search Replace で一括更新
画像を数枚差し替えたけど投稿を全部開いて保存し直すのは面倒 ── そんなときは Better Search Replace プラグインで ?ver= の値を一括置換します。
ツール → Better Search Replaceを開く- 「検索」に既存の
?ver=20260101120000を指定(現在データベースに入っている値を1件確認してコピーするのが確実) - 「置換」に新しい
?ver=20260424153000を指定 - テーブルで
wp_postsを選択 - 「ドライラン」で件数確認 → 実行
これで全投稿の画像URLのクエリが一括で更新され、ブラウザキャッシュが強制的に無効化されます。
投稿単位で更新したい場合
特定の投稿だけなら、管理画面で「更新」ボタンを押すだけで自動的に最新の ?ver= に書き換わります。
まとめ
?ver= 付与の実装は、「レンダリング時」ではなく「保存時」に処理する という発想の転換がポイントでした。
| 項目 | 旧方式(the_content) | 新方式(content_save_pre) |
|---|---|---|
| ページキャッシュ耐性 | ✗ | ✓ |
| レンダリング負荷 | 毎回発生 | ゼロ |
| Better Search Replaceで一括更新 | ✗ | ✓ |
| 画像差し替えのみへの対応 | △ | ✓(一括置換で対応) |
特にLiteSpeed CacheやWP Super Cacheなど、サーバー側のページキャッシュを使っている環境では、新方式に切り替えないと ?ver= が期待通りに届きません。「投稿は更新したのに画像が変わらない」と悩んでいる方は、ぜひこのアプローチを試してみてください。
関連記事
キャッシュとストレージとセッションの違い、JavaScriptで クリアする方法
画像のアスペクト比を自動計算・付与する | aspect-ratioが効かない原因とCLS対策