2026年5月22日
2026年5月22日
WordPressのCDNとオリジンサーバーの役割分担でキャッシュ戦略を最適化する
CDNを入れても「静的ファイルしかキャッシュされていない」「更新が反映されない」「ログイン中の画面が他人に見えた」という問題が起きるのは、CDNとオリジン(WordPressが動くサーバー)のどちらが何を、何秒キャッシュするかを決めていないことが原因です。この記事では、コンテンツを種類ごとに分けてキャッシュ期間を決め、オリジンが返す Cache-Control ヘッダーでCDNの挙動をそろえる考え方と、更新時のパージ、オリジンの保護までを説明します。
オリジン側のページキャッシュ(Nginxの fastcgi_cache など)の設定そのものは別記事で扱っています。この記事は「CDNとオリジンの間の取り決め」に焦点を当てます。
コンテンツを4種類に分けて考える
WordPressサイトのレスポンスは、キャッシュの観点で次の4つに分けると整理しやすくなります。
- バージョン付きの静的ファイル:テーマやプラグインのCSS・JS。WordPressは
wp_enqueue_style()などで読み込んだファイルのURLに?ver=を付けるため、ファイルが変わればURLも変わる - メディアファイル:アップロードした画像やPDF。同じURLのまま差し替えられることは少ないが、ゼロではない
- 公開ページのHTML:記事やトップページ。更新されると内容が変わる
- 個人ごとに変わるもの:管理画面、ログイン中のページ、カート・決済、マイページ、プレビュー、REST APIの認証付きリクエスト
基本方針は、1と2はCDNで長く、3はCDNで短く(またはパージ前提で長く)、4はどこにもキャッシュしない、です。事故が起きるのはほぼ4の扱いで、ここを最初に固めます。
Cache-Control の役割を押さえる
CDNとブラウザの両方に指示を出すのが Cache-Control ヘッダーです。よく使う指定は次のとおりです。
max-age:ブラウザ(と、他に指定がなければ共有キャッシュ)がキャッシュしてよい秒数s-maxage:CDNなどの共有キャッシュにだけ適用される秒数。max-ageより優先されるstale-while-revalidate:期限切れ後、裏で取り直している間は古い内容を返してよい秒数stale-if-error:オリジンがエラーを返したとき、古い内容を返してよい秒数private:共有キャッシュには保存しない(ブラウザだけ)no-store:どこにも保存しない
HTMLでは「ブラウザには短く、CDNには長めに」とするのが定番です。ブラウザに長く持たせると、CDNをパージしても読者の手元の古いページは消せないからです。
CDNによっては、CDN-Cache-Control や Surrogate-Control など、CDN向けだけのヘッダーにも対応しています。どのヘッダーをどの優先順位で解釈するか、stale-while-revalidate に対応しているかはCDNごとに違うため、使うサービスのドキュメントで確認してください。各社とも仕様の追加・変更が続いています(2026年10月現在)。
種類ごとの設定例
静的ファイルは、オリジンのWebサーバーで長期キャッシュを指定します。Nginxの例です。
location ~* \.(css|js|woff2?|svg)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
access_log off;
}
location ~* \.(jpe?g|png|gif|webp|avif)$ {
add_header Cache-Control "public, max-age=2592000";
}
CSS・JSに1年と immutable を付けられるのは、更新時にURLが変わる前提があるからです。自作テーマで wp_enqueue_style() の第4引数(バージョン)を固定値のままにしていると、ファイルを更新してもURLが変わらず古いCSSが残ります。開発中はファイルの更新時刻をバージョンに使う方法がよく使われます。
// 子テーマの functions.php:ファイル更新時刻をバージョンにする
add_action( 'wp_enqueue_scripts', function () {
$path = get_stylesheet_directory() . '/style.css';
wp_enqueue_style(
'child-style',
get_stylesheet_uri(),
array(),
file_exists( $path ) ? (string) filemtime( $path ) : null
);
} );
画像は同じファイル名で差し替える運用があるなら、1年ではなく1か月程度にとどめるのが無難です。
公開ページのHTMLは、WordPress側でヘッダーを付けます。次のmu-pluginは、ログアウト中の通常ページにだけCDN向けのキャッシュ期間を指定する例です。
<?php
// wp-content/mu-plugins/cdn-cache-headers.php
add_action( 'template_redirect', function () {
if ( 'GET' !== ( $_SERVER['REQUEST_METHOD'] ?? '' ) ) {
return;
}
if ( is_user_logged_in() || is_preview() || is_search() || is_404() ) {
return;
}
if ( is_singular() && post_password_required() ) {
return;
}
// ブラウザは毎回確認、CDN は 10 分、更新中・障害時は古い内容を許容
header( 'Cache-Control: public, max-age=0, s-maxage=600, stale-while-revalidate=60, stale-if-error=86400' );
} );
send_headers アクションではなく template_redirect を使っているのは、is_404() や is_singular() などの条件分岐タグが、クエリの解析が終わったこの時点から正しく使えるからです。WooCommerceや会員プラグインを使っている場合は、カート・決済・マイページを除外条件に加えてください(WooCommerceなら is_cart()、is_checkout()、is_account_page() があります)。ログイン中のユーザーにはWordPressが nocache_headers() でキャッシュ禁止のヘッダーを送るので、ここで上書きしないよう早めに return しています。
個人ごとに変わるものは、オリジンのヘッダーだけに頼らず、CDN側のルールでもキャッシュ対象外にしておきます。具体的には、/wp-admin/、/wp-login.php、/wp-json/、プレビューのURLと、wordpress_logged_in_、wp-postpass_、comment_author_、(WooCommerceなら)woocommerce_items_in_cart などのCookieを持つリクエストです。二重に防いでおけば、プラグインの更新でヘッダーが変わっても事故になりにくくなります。
CDN側の設定で気をつけること
多くのCDNは、既定ではHTMLをキャッシュせず、画像やCSSなど拡張子で判定したファイルだけをキャッシュします。HTMLをCDNでキャッシュするには、CDN側で「オリジンのCache-Controlに従う」設定にするか、キャッシュルールを追加する必要があります。
- Cloudflare:HTMLをキャッシュするにはキャッシュルール(Cache Rules)の設定が必要で、WordPress向けにはAPO(Automatic Platform Optimization)という有料オプションも用意されています。画面名や機能の提供範囲はプランや時期で変わります
- Amazon CloudFront:キャッシュポリシーとオリジンリクエストポリシーで、キャッシュキーに含めるCookie・クエリ文字列・ヘッダーを決めます。Cookieを全部キャッシュキーに含めると実質キャッシュされなくなり、逆に全部捨てるとログインが壊れます
どのCDNでも、キャッシュキーに何を含めるかが設計の中心です。クエリ文字列(utm_source などの計測パラメーター)でキャッシュが分かれすぎるとヒット率が下がるため、無視してよいパラメーターを整理しておきます。
更新時のパージを決める
HTMLにCDNで長めのキャッシュを設定するなら、記事の更新時に該当URLをパージする仕組みが欲しくなります。CloudflareにはCloudflare公式プラグインやSuper Page Cacheプラグイン、CloudFrontにはC3 Cloudfront Cache Controllerプラグインなど、投稿の更新に合わせてパージを依頼するプラグインがあります。対応範囲(記事URLだけか、トップや一覧ページも含むか)はプラグインごとに違うため、更新後にトップページやカテゴリ一覧が古いままになっていないかを実際に確認してください。
パージの仕組みを用意できない場合は、s-maxage を5〜10分程度に抑え、「最大でその時間だけ古い」ことを受け入れる設計が現実的です。
オリジンを守る
CDNを入れても、オリジンのIPアドレスに直接アクセスされれば、キャッシュもCDNのWAFも素通りされます。オリジンへの通信をCDNからに限定する方法として、CDNのIPアドレス範囲だけを許可するファイアウォール設定、Cloudflareの認証付きオリジンプル(Authenticated Origin Pulls)、CloudFrontからのカスタムヘッダーをオリジンで検証する方法などがあります。CDNのIPアドレス範囲は変わることがあるので、手作業で書き写すのではなく、公開されている一覧を定期的に取り込む運用にしてください。
また、CDN経由では、オリジンから見た接続元IPがCDNのものになります。コメントのスパム判定やログイン試行の制限が正しく動くよう、Nginxの real_ip モジュールなどで、CDNからのリクエストに限って本来の接続元IPを復元しておきます。
確認方法
設定後は、ログアウト状態でレスポンスヘッダーを確認します。
curl -sI https://example.com/ | grep -iE "cache-control|age|cf-cache-status|x-cache"
cf-cache-status(Cloudflare)や x-cache(CloudFront)が2回目以降にHITになり、age が増えていけばCDNでキャッシュされています。続けてログイン中のブラウザで開発者ツールを開き、管理画面やログイン後のページがHITになっていないことも確認します。この確認は、設定変更のたびに行ってください。
まとめ
- レスポンスを「静的ファイル」「メディア」「公開HTML」「個人ごとに変わるもの」に分けて期間を決める
- HTMLはブラウザに短く(
max-age=0)、CDNに長め(s-maxage)が基本 - バージョン付きのCSS・JSは長期キャッシュにし、テーマ側でバージョンを正しく更新する
- ログイン中・カート・プレビューなどは、オリジンのヘッダーとCDNのルールの両方で除外する
- 長めにキャッシュするならパージの仕組みを用意し、無理なら期限を短くする
- オリジンへの直接アクセスを制限し、接続元IPの復元も忘れない