このブログも久しぶりにイノベーションしそうだ。

AMPという技術を恥ずかしながら最近知ったのだが。

AMPとはAccelerated Mobile Pagesの略で、モバイル向けにページを超高速で表示させる技術だ。

Googleを中心に、TwitterやPinterestやWordPressなども参加するプロジェクトで、2016年2月からGoogle検索で実用化が始まった。

どのくらい速くなるのかというと、本当に驚くほど一瞬だ。

で、このブログもぜひその最先端技術に対応したいのだが(新しいもの好き)、ページをAMPに対応させるには条件や制約がかなり多く、苦労しているところだ。

残念ながら、おそらくこのブログはAMPに対応できないと思われるが、勉強のためにも可能な限りで実装しようと取り組んでいる。

今回は、WordPressにおいてスタイルシート(CSS)をAMPに対応させる方法を紹介する。

AMP対応のために

AMP対応のための条件の一つに、「CSSは <link> タグで外部ファイルから読み込んではならず、 <head> 内でのみ50,000バイト以内で記述せよ」という無理難題ものがある。

3ファイルで約101キロバイトのCSSを使っているこのブログでは対応に迫られたが、これを汎用的に解決するためには、

  1. 外部CSSファイルをPHPで読み込み、テキストとして <head> 内に出力する。
  2. CSS内の空白文字などの不要な部分を削り、容量を削減する。

――という二つの処理が必要になる。

どちらも俺にとっては初めてで、苦労してなんとか解決したため今回紹介することにした。

外部CSSを読み込む

AMPに対応したHTMLでは、CSSについては <head> 内にある <style amp-custom>〜</style> の中にすべてを記述しなければならない。

そのためには普段 <link rel="stylesheet"> によって読み込んでいる外部CSSファイルを、HTML内にインラインで挿入しなければならない。

まあ、コピー&ペーストで挿入してもいいのだが、俺の場合はCSSを1箇所で管理したいのでPHPにより外部CSSファイルを取得することにした。

この場合、WordPressプラグインにこれを実現するものがあるが、自由度が低いので俺は自力で実装することにした。

PHPにおいて外部ファイルをテキストとして読み込むのに file_get_contents() という関数が使える。

この関数では、引数としてURLを渡すことで外部ファイルの内容を文字列として取得でき、あとは変数に代入するなり置換処理を行うなりすればいい。

ところが、サーバーによってはこの関数が使えない場合がある。

このブログのサーバー(Xdomainの無料版)がまさにそうで、 file_get_contents() を使うと以下のようなエラーが出る。

Warning: file_get_contents(): http:// wrapper is disabled in the server configuration by allow_url_fopen=0 in /home/***/***.wp.xdomain.jp/public_html/wp-content/themes/***/header.php on line 415

Warning: file_get_contents(http://***.wp.xdomain.jp/wp-content/themes/***/css/style.css): failed to open stream: no suitable wrapper could be found in /home/***/***.wp.xdomain.jp/public_html/wp-content/themes/***/header.php on line 415

エラーメッセージの例

エラーメッセージによると「allow_url_fopen」という設定が無効になっているせいで、 file_get_contents() (および fopen() 系の関数)が使えないとのこと。

この設定は php.ini ファイルを編集することで変更できるらしいが、残念ながらこのブログのサーバーでは php.ini ファイルにアクセスすることもできない。

そこで、この関数を使わない方法で外部CSSファイルの内容を取得する方法を調べ、以下のサイトで情報を見つけた。

file_get_contentsが使えない時の対処方法 | ScrapEngineer

このサイトの情報をもとに、以下のように自作関数を作成した。

if ( ! function_exists('curl_file_get_contents') ) {
    function curl_file_get_contents( $url = '', $fail = true ) {
        if ( ! is_string($url) || strpos($url, '://') == 0 )
            return false;
        $ch = curl_init();
        curl_setopt( $ch, CURLOPT_URL, $url );
        curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
        curl_setopt( $ch, CURLOPT_CONNECTTIMEOUT, 10 );
        curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, true );
        curl_setopt( $ch, CURLOPT_MAXREDIRS, 3 );
        if ( $fail )
            curl_setopt( $ch, CURLOPT_FAILONERROR, true );
        $return = curl_exec( $ch );
        curl_close( $ch );
        return $return;
    }
}

1行目は関数名の重複防止チェック、2行目は関数の定義、関数名はお好みで。

第2引数を false にすると、404エラーなどの場合に取得に失敗せず、エラーページの内容を取得する。

上のコードではPHPの「cURL」という一連の関数を利用している。

ところで、ローカル(同じサーバー内)のファイルを開く場合は「allow_url_fopen」という設定に関わらず fopen() 系の関数を使うことができるようだ。

fopen() 系の関数でローカルのファイルを取得する場合、ファイル名はローカルのパスで指定しなければならない。

上記の関数ではURLを指定することで簡単に外部ファイルを取得できるが、俺が検証したところ、上記の関数を使用するよりも fopen() 系の関数を使用したほうが高速であることがわかった。

そこで、URLをローカルのパスに変換しつつ、ローカルのファイル限定で上記の関数と同じように使える、 fopen() 系の関数を利用した以下の自作関数を作成した。

6月26日追記:URLのかわりにローカルのパスを指定しても動作するように改良してみた。

if ( ! function_exists('local_file_get_contents') ) {
    function local_file_get_contents( $filename = '' ) {
        if ( strpos($filename, ABSPATH) !== 0 ) {
            if ( strpos($filename, 'http') !== 0 )
                return false;
            $filename = str_replace( 'https://', 'http://', $filename );
            $blog_url = str_replace( 'https://', 'http://', get_option('home') ) . '/';
            if ( ! is_string($filename) || strpos($filename, $blog_url) !== 0 )
                return false;
            $filename = ABSPATH . substr($filename, strlen($blog_url));
        }
        $fp = fopen($filename, 'r');
        if ( $fp === false )
            return false;
        $return = fread($fp, filesize($filename));
        fclose($fp);
        return $return;
    }
}

引数としてURLを渡した場合、6行目から10行目にかけて、URLをローカルのパスに変換する。

※ひょっとしたら、サイトやサーバーによってはパスの構造が異なり、このコードのままでは使えないかもしれない。

引数としてパスを渡した場合はそのまま使う。

上のコードをWordPressテーマの functions.php ファイルに追加し、 local_file_get_contents() を以下のように使って外部CSSファイルを取得する。

/* URL */
$amp_css = local_file_get_contents( get_stylesheet_directory_uri() . '/style.css' ); 
/* パス */
$amp_css = local_file_get_contents( get_stylesheet_directory() . '/style.css' ); 

複数のファイルを取得したい場合は、普通にこの関数を複数回使い、あとで文字列を連結するなどすればよい。

CSSを圧縮(minify)する

ここまでの方法でCSSをHTML内に挿入することができたが、上記の通りこのブログではCSSが約101キロバイトもあり、これを50,000バイト以下にまで削減する必要がある。

CSSの容量を削減するには、AMPページ向けに記述を絞ったCSSを別に用意するか、余分な空白文字を取り除いて容量を削減する(圧縮:minifyと呼ばれる)かしなければならない。

圧縮ができるのは、普段CSSを書くときは(SASSやSCSSのツールを使っている場合は別として)、コードを見やすくするために大量の改行やタブ文字でコードを整形しているからである。

これを除去することで容量を何割か削減することができる。

そこで、俺は普段通りの外部CSSを取得し置換処理することで、容量を半減させることにした。

CSSの置換処理については間違えると表示がおかしくなるし、すでに確立されたPHPのライブラリが用意されているが、新たなファイルを導入したくない俺はこれも自力で実装することにした。

そして試行錯誤の末、以下のコードにたどり着いた。長い。

ただし、以下のコードは実際にこのブログで使用し、不具合が出ていないことを確認しているが、完璧ではないので使用の際は自己責任で。自己責任なのはこのコードに限らないが……

より良いコードがあれば教えてくださいな。

/* 改行文字・タブ文字などを除去(後述) */
$amp_css = str_replace( array("\r\n", "\r", "\n", "\t", '@charset "utf-8";', "@charset 'utf-8';", ' !important'), '', $amp_css );
/* AMPページにおいて不要な部分を除去(後述) */
$amp_css = preg_replace( '/\/\*no-amp-start\*\/.*?\/\*no-amp-end\*\//', '', $amp_css );
/* CSS内のコメントを除去 */
$amp_css = preg_replace( '/\/\*.*?\*\//', '', $amp_css );
/* 連続する空白文字を一つの半角空白に置換 */
$amp_css = preg_replace( '/\s\s+/', ' ', $amp_css );
/* 小括弧・角括弧内の余分な空白を除去 */
$amp_css = str_replace( array('( ', ' )', '[ ', ' ]', ' ::'), array('(', ')', '[', ']', '  ::'), $amp_css );
/* 中括弧・プロパティ区切り文字・セレクター区切り文字の前後の空白文字を除去 */
$amp_css = str_replace( array(' {', ' }', ' :', ' ;', ' ,', ' +', ' >'), array('{', '}', ':', ';', ',', '+', '>'), $amp_css );
$amp_css = str_replace( array('{ ', '} ', ': ', '; ', ', ', '+ ', '> '), array('{', '}', ':', ';', ',', '+', '>'), $amp_css );
/* ブロック内の最後のセミコロンを除去 */
$amp_css = str_replace( ';}', '}', $amp_css );
/* 空のブロックを除去 */
$amp_css = preg_replace( '/[^{}]+\{\}/', '', $amp_css );
/* 相対URLを絶対URLに置換(後述) */
$amp_css = str_replace( "url('img/", "url('" . get_bloginfo('template_url') . '/img/', $amp_css );
$amp_css = str_replace( 'url("img/', 'url("' . get_bloginfo('template_url') . '/img/', $amp_css );

①:改行文字・タブ文字のほかに @charset 宣言や、AMPページのCSSにおいて使用を禁止されている !important を除去している。

②:このコードにより、CSSコード中の /*no-amp-start*/ から /*no-amp-end*/ までをすべて除去できるようになる。AMPページにおいて不要な部分を指定することで、容量を削減できる。これらはCSSコメントなので、通常のページに影響を与えない。

③:img ディレクトリを指定する相対URLを絶対URLに置換する例。AMPページのCSSにおいては http:// または https:// で始まる絶対URLを使用しなければならない。このコードでは ../ で始まる相対URLである場合に工夫が必要となる。また、取得するCSSファイルはテーマディレクトリの直下にあることを前提としている。なお、上の行と下の行の違いはクオーテーションマークの違いなので、コーディングポリシーに合わせて片方だけを採用しても良い。

まず、このコードは元のCSSがすでに正確に記述され、かつ正常に動作していることを前提としていることに注意。

そうでなければそもそもエラーが出るはずだが、このコードはそれを悪化させることこそあれ、是正する機能は一切ない。

その他、このコードの注意点として、CSSにおける文字列値に含まれる空白文字も置換・除去してしまう。

このブログではそういう記述を用いていないので問題ないが。

このブログでは上記のコードを使うことで、CSSの容量をギリギリ50,000バイト以内に抑えることができた。

おまけとして、上記の③においてCSSファイルがテーマディレクトリの子ディレクトリである css ディレクトリにあり、かつ ../ で始まる相対URLである場合のコードの例も紹介する(1行目と2行目はクオーテーションマークの違い)。

$amp_css = str_replace( "url('../", "url('" . get_bloginfo('template_url') . '/', $amp_css );
$amp_css = str_replace( 'url("../', 'url("' . get_bloginfo('template_url') . '/', $amp_css );

まとめ

以上の二つの方法により、このブログも最新技術であるAMPに対応したCSSを挿入することができた。

WebページをAMPに対応させるためには、他にも多くの仕様や制約に従わなければならない。

その方法のすべては長くなるので今回は触れない。

WordPressはHTMLの構築を自動で行うので、もとより難易度が高いAMPへの対応をさらに難しくする。

導入するだけでAMPへの対応を行うとされるプラグインがWordPress公式によって公開されているが、実際にはそれだけではエラーが頻発するらしいし。

ブルーレット置くだけとはいかないわけだ。

で、このブログではAMPへの対応をほぼ済ませたのだが、残念ながらAMP対応版ページを公開することはできない。

なぜなら、このブログのサーバーが挿入する広告のためのスクリプトが、AMPで用意されたものを除く一切のJavaScriptの使用を禁じるという仕様と相容れないためだ。

利用規約により、このスクリプトを除去することはできない。

つまり、どうあがいても無理。負け。

orz