WordPressの全CSS/JSの読込みでバージョンを自動付与 ~キャッシュで変更が反映されない問題対策

WordPressの使い方

WordPressの全JS/CSS読込にバージョンを自動的に付与する方法

この記事では「WordPressの全てのJavaScriptやCSSファイルの読み込みにバージョンを自動的に付与する方法」を解説いたします。

具体的にはこのように「?fver=」というパラメータ文字列を追記します。

<link rel="stylesheet" href="/test.css" madia="all">

              ↓↓↓

<link rel="stylesheet" href="/test.css?fver=20220129060248" madia="all">

このように「?ver=更新日時」のようにURLにパラメータ文字列を付与します。

通常、この対応は「CSSやJavaScriptなどの変更内容が反映・更新されない」といった問題を解消するために行われます。

今回紹介する方法の他の記事の対応と違うすごい所は「個別にバージョンを付与する」のではなく「自動でバージョン有無の判別をして全てに付与する」という所です。

WordPressに最初から組み込まれているCSSファイルなどはすでに対応していることが多いですが、新しく自分でCSSやJSを追加読み込みを記述した場合は通常は対応できません。都度、バージョン名を書き換えるなんてちょっと非現実的ですよね。

子テーマのhead.phpを修正するなんていう紹介も他では見かけますが、それもおすすめできません。
本体ファイルの上書きはバージョンアップの際にバグやエラーのリスク要因となってしまいます。

本記事ではそのような場合のバージョン文字列の自動付与についてやり方を解説していきます。

この記事を監修・執筆した専門家

こんにちは!この記事を監修・執筆した斎藤はじめと申します。
私は現役のエンジニアで、普段はWordPressの開発案件などをメインに担当しています。経歴としては誰もが知っている月間数千万人が利用するサービスの開発をしたり、今でも月間数百万人が訪問する規模のWordPressサイトの運営しております。WordPressを触らない日はありません。

そんな私がプロの目で解説していきます!

なぜJavaScriptやCSSにバージョン文字列を付けた方がいいのか?

そもそも、なぜJavaScriptやCSSにバージョン文字列を付ける対応が必要なのでしょうか?

それは「CSSやJSの変更内容の反映を即時行えるようにするため」です。

WordPressで作ったサイトにはCSSやJSファイルの変更内容が即座に反映されないという問題が付きまといます。原因の多くはキャッシュです。

キャッシュとは?

キャッシュとはアクセス負荷を下げるために一度表示したファイルを一時的に保存して表示速度を高速化する仕組みの事です。
「キャッシュする=一時的に保存する」と覚えて問題ありません。

一口にキャッシュといっても「ブラウザ」「サーバー」「プラグイン」など様々な場所に存在します。原則としてはURLに対して一時保存を行います。

キャッシュはその設定次第で、全てのファイルが対象になる可能性があります。
CSSやJavaScript、画像ファイルのみをキャッシュすることもありますし、元になるhtmlそのものをキャッシュすることもあります。

キャッシュされるとファイルの変更が反映されない

CSSやJSを変更してもURLが一緒だと以前保存したキャッシュが使われてしまい古いものが表示され続けます。これが更新が反映されない原因です。

キャッシュ対象ファイルにバージョンをつける理由

URLが変わればまた別のキャッシュとして保存されるので更新は反映されます。これが「?fver=更新日時」というパラメータ文字列をつける理由です。

URLを明示的に変更することにより、「別の新しいファイルだよ」とキャッシュに認識させるのです。

バージョンを自動付与の実装手順

WordPressで「JavaScriptやCSSファイルにバージョン文字列を自動付与」の実装をするにはfunctions.phpに以下のPHPプログラムのコードを記述します。
わからない方はfunctions.phpにコピペするだけで動くので解説などは飛ばしてください。

functions.phpのサンプルソースコード

PHPサンプルプログラムのソースコードを示します。

<?php

	//=====================================================================================================
	// WordPressのCSS/JavaScriptの外部ファイルのURLの末尾にバージョンIDを自動的に付けるスクリプト
	//
	//  【実例】
	//      このCSS読み込みの例でいうと
	//      <link rel="stylesheet" href="/wp-content/uploads/custom-css-js/test0000.css" madia="all">
	//      以下のようにhrefの値を変更する
	//      /wp-content/uploads/custom-css-js/test0000.css
	//          ↓
	//      /wp-content/uploads/custom-css-js/test0000.css?fver=20220129060248
	//
	//  【処理概要】
	//      ①WordPressの出力するheadの中を最後に1行ずつ調査して、CSSやJavaScriptの読み込みを検出。
	//      ②検出されたファイルのURLにパラメータがついてなければ、その実ファイルの存在をチェックする。
	//      ③実ファイルが存在すればその更新時間からfver=を設定して付与する。
	//
	//=====================================================================================================

	//----------------------------------------------------
	// HTMLをすぶに出力せずにためておく(テーマが読み込まれた直後に起動)
	//----------------------------------------------------
	add_action('after_setup_theme', function(){

		//ob_start($callback) → 出力のバッファをONにする。(HTMLが即座に出力されずにプールされている状態になる)
		//                       callbackはバッファが最後に出力されるときに呼び出され、このバッファに対して処理をする
		ob_start(function ($buffer_html) {

			$debug_mode = false; //デバッグモード設定(開発中にONにする)

			$lines = explode("\n", $buffer_html); //全HTMLを行ごとに分割
			$rownum = 0; //調査中のHTMLの行数
			$is_finished = false; //調査が終わったかどうか
			$is_found = false; //バージョン付与対象が見つかったか
			$new_buffer_html = ""; //変換後の全HTML

			foreach($lines as $line){ //1行ずつループ --------------------------------------------------

				$debug = ""; //デバッグメッセージ
				$message = ""; //必ず出すメッセージ
				++$rownum;
				$new_line = str_replace("\r", "", $line); //無駄な改行文字列削除

				if($is_finished == false){ //まだ調査が終わっていないなら
					
					$lower_line = strtolower($new_line);
					if(strpos($lower_line, "<body") !== false){ //headタグが終わり<bodyが出現したら調査終了
						$debug .= "→調査終了";
						$is_finished = true; //調査終了マーク
						if($is_found != true){ //調査終了時に変換対象のCSSもJSも見つからなかった
							break;
						}
					}
					
					//①CSSを検出した
					//  検知する例サンプル <link rel='stylesheet' href='/wp-content/test.css' media='all' />
					if(strpos($lower_line, "<link") !== false && strpos($lower_line, "stylesheet") !== false){

						$debug .= "→linkタグあり";
						preg_match('/href=[\'"]([^"\']+\.css)[\'"]/', $new_line, $matches);

						$file_url = null;
						if(isset($matches[1])){
							$debug .= "→hrefにパラメータなしURLがあるので対象";
							$file_url = $matches[1];
							$file_path = convert_url_to_local($file_url);
							if($file_path != null && file_exists($file_path)){ //変換したローカルパスが存在するか
								$debug .= "→CSS実ファイル有り。CSSバージョン自動付与";
								$message .= "【CSSバージョン自動付与】";
								$new_file_url = add_query_arg( 'fver', date_i18n('Ymdhis', filemtime($file_path)), $file_url); //バージョンIDを付与
								$new_line = str_replace($file_url, $new_file_url, $new_line); //URLを変換
								$is_found = true; //バージョン付与したフラグをON
							}else{
								$debug .= "→ファイル無し。処理対象外";
							}
						}else{
							$debug .= "→hrefにパラメータあるためスキップ";
						}

					//②JavaScriptを検出した
					// 検知する例のサンプル <script src='/js/test.js'></script>
					}else if(strpos($lower_line, "<script") !== false && strpos($lower_line, "src=") !== false){

						$debug .= "→scriptタグあり";
						preg_match('/src=[\'"]([^"\']+\.js)[\'"]/', $new_line, $matches);

						$file_url = null;
						if(isset($matches[1])){
							$debug .= "→srcにパラメータなしURLがあるので対象";
							$file_url = 	$matches[1];
							$file_path = convert_url_to_local($file_url); //ローカルのパスに変換
							if($file_path != null && file_exists($file_path)){ //変換したローカルパスが存在するか
								$debug .= "→JS実ファイル有り。JavaScriptバージョン自動付与";
								$message .= "【JSバージョン自動付与】";
								$new_file_url = add_query_arg( 'fver', date_i18n('Ymdhis', filemtime($file_path)), $file_url); //バージョンIDを付与
								$new_line = str_replace($file_url, $new_file_url, $new_line); //URLを変換
								$is_found = true; //バージョン付与したフラグをON
							}else{
								$debug .= "→ファイル無し。処理対象外";
							}
						}else{
							$debug .= "→srcにパラメータあるためスキップ";
						}

					}

					if($debug_mode) { //デバッグモードの時
						if($message != null || $debug != null) {
							$new_line .= PHP_EOL . "<!-- {$message} {$debug} -->" . PHP_EOL;
						}
					}else{ //デバッグモードじゃないとき
						if($message != null){
							$new_line .= PHP_EOL . "<!-- {$message} -->" . PHP_EOL;
						}else{
							//出さない
						}
					}

				}
				
				$new_buffer_html .= $new_line . PHP_EOL;

			} //ループここまで ------------------------------------------------------------------------

			if($is_found != true){ //対象が見つかっていないなら
				$new_buffer_html = $buffer_html; //元のHTMLそのまま出す
			}
			
			return $new_buffer_html;

		});
	}, 10000);


	//----------------------------------------------------
	//ためていたHTMLを出力する(WordPressが処理を終了する直前に起動)
	//----------------------------------------------------
	add_action('shutdown', function(){
		if(ob_get_length() > 0){
			ob_end_flush();
		}
	}, 10000); //第2引数は優先順位(デフォルト10, 大きいほど後にまわされる)


	//----------------------------------------------------
	//URLからローカルパスに変換する関数。存在しない場合はfalse
	//----------------------------------------------------
	if (function_exists('convert_url_to_local') === false){

		function convert_url_to_local($url){

			$is_local = false;
			preg_match('#(https*:){0,1}//([^/]+)#', site_url(), $matches);
			if(isset($matches[2])){
				$domain = $matches[2];
			}

			if (preg_match('#(https*:){0,1}//#', $url) > 0) { // CSSのURがhttpや//から始まる
				if (strpos($url, $domain) !== false) { //現在のサイト同じドメイン
					$is_local = true; //現在のサイト内のファイル
				}

			}else{ //CSSのURがhttpから始まらない
				$url = site_url($url); //現在のサイトのドメイン形式に直す
				$is_local = true;
			}

			if($is_local == true){ //現在のサイト内のファイルなら
				//ローカルパスに変換してファイルの存在チェック
				$local_path = $url;
				$pattern = "#^.*?" . preg_replace("#https*:#", "", site_url('/')) . "#";
				$local_path = preg_replace(
					$pattern
					, ABSPATH
					, $local_path
				); //ローカルファイルパスに変換
				$local_path = preg_replace('/(\?|#).+$/', '', $local_path); //パラメータはいったん削除

				if(file_exists($local_path) == true) { //ファイルが存在する
					return $local_path;
				}
			}
			return false;

		}

	}

※2022/2/5修正済

ソースコードの解説

できるだけソース中にコメントを書いてわかりやすくしたつもりですが簡単に補足しておきます。

よくわからないという方は、この解説は飛ばしても大丈夫です。

このソースのポイントは3つ。

  • ・アクションフック
  • ・出力バッファリング
  • ・実行するページの絞り込み条件
・アクションフック
add_action('after_setup_theme', function(){セットアップ処理後の内容}, 10000);

アクションフックはWordPressの特定の場所に処理を差し込める機能の事です。この場合はafter_setup_themeを指定して「テーマのセットアップ」直後に②のバッファリング処理を差し込んでいます。

・出力バッファリング
ob_start(function($buffer_html){バッファした文字列に対しての処理の内容})

出力バッファリングは、HTMLの出力表示をためておくという意味です。

通常はWordPressの処理の進行と同時に処理がすすむごとにHTML出力表示がされますが、全部メモリにためておいて処理が全ておわってから出力させるようにするのがこのHTMLバッファリングです。

テーマのセットアップ直後にバッファリングを始めることで全HTMLをメモリ内にためます。そのためたHTMLを書き換えてから出力することによって、今回の自由な変換処理を実現させています。

・実行するページの絞り込み条件
if(is_admin() === false){ →管理者ページでないときだけ実行

何も条件を指定しないと全ページに処理されてしまうので、実行するページの絞り込みが必要です。

大抵の場合は管理者ページの場合はいらないと思うので、サンプルではこの条件で記述されています。

他にも条件の絞り方はあると思うので参考にしてください。

その他の条件の絞り込み

絞り込み条件をいろいろと使いたい場合は以下の判定用の関数を参考にしてみてください。自由度が無限に広がります。

関数名 説明 引数 返り値
is_single() 投稿・添付・カスタム投稿タイプの個別ページかどうか mixed $post →投稿のIDやスラッグ、またはその配列 bool
is_page() 固定ページかどうか mixed $page →ページのIDやスラッグ、またはその配列 bool
is_attachment() 添付ページかどうか mixed $attachment →添付ファイルのIDやスラッグ、またはその配列 bool
is_singular() is_single()・is_page()・is_attachment()のどれかかどうか mixed $post_types →カスタム投稿タイプ、またはその配列 bool
is_archive() アーカイブページであるかどうか
アーカイブとは投稿の一覧ページのこと。
(カテゴリ、タグ、作成者、日付、カスタム投稿タイプ、およびカスタム分類など)
なし bool
is_category() カテゴリーのアーカイブページかどうか
引数にカテゴリーIDやカテゴリースラッグを指定できる
配列で複数の指定も可能
mixed $category →カテゴリID、名前、スラッグ、またはその配列 bool
is_tag() タグのアーカイブページかどうか mixed $tag →カテゴリID、名前、スラッグ、またはその配列 bool
is_admin() 管理画面を表示中かどうか bool
is_author() 投稿者別のアーカイブページであるとき
引数に投稿者IDや投稿者のニックネームを指定できる
配列で複数の指定も可能
$author →ユーザーID、ニックネーム、nicename、またはその配列 bool
is_date() 日付のアーカイブページかどうか bool
is_year() 年別のアーカイブページかどうか bool
is_month() 月別のアーカイブページかどうか bool
is_day() 日別のアーカイブページかどうか bool
is_search() 検索結果ページかどうか bool
is_404() 404ページかどうか bool
is_paged() 複数ページ構成の2ページ目以降かどうか bool
is_home() || is_front_page() トップページかどうか bool
get_the_ID() 投稿のIDを取得 int
get_permalink($id) パーマリンクを取得 $id 投稿のID string

引数の使い方としては、is_single()だと投稿全体ならtrue, is_single(123)だと投稿IDが123のページの時だけtrueといった具合になります。

念のためfunctions.phpの編集方法についても触れておきます。

functions.phpの編集方法

functions.phpはWordPressの機能追加をするためのPHPファイルです。

(※実ファイルは/wp-content/themes/<テーマ名>/の直下にあります)

functions.phpを修正する場合は必ず親テーマではなく子テーマのfunctons.phpを修正してください。

※その理由や親テーマと子テーマについてはこちら↓の記事を参考に。

管理画面からfunctions.phpを参照・編集するには以下のように「外観>テーマ(ファイル)エディター」にアクセスしてテキスト編集をします。

PHPファイルの記述ルールとしてプログラムコードは以下のようにphpタグの囲みの中にある必要があります。

<?php

	→PHPのプログラムを書く場所

?> ←最後PHPで終わる場合だけ省略可能

また、直接functions.phpを編集するよりもこちらのプラグインで編集した方が便利なので参考にしてみてください。

まとめ

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

最後に今回の内容に特に関連が深い別の記事をご紹介します。
全てチェックすると理解がとっても深まりますよ。

JavaScriptやCSSのベストな管理方法はこちら

この記事で使用しているテクニック(アクションフックや出力バッファリング)についての元ネタ。シンプルにフォーカスして紹介している記事はこちら

最後までお読みいただきありがとうございました。

コメント

タイトルとURLをコピーしました