refプラグインにCloudflare Imagesの導入

Cloudflare Imagesとは
Cloudflare Imagesは、画像の管理や配信を効率化するサービスだ。画像の変換や圧縮を自動で行い、不要な負荷を減らしながら高速に配信できる。専用のストレージを使うことで、サーバーの管理を簡単にし、画像の最適化も自動化できる。世界各地にあるCloudflareのネットワークを利用するため、どこからアクセスしても安定した速度で画像を表示できる。
Cloudflare Images Transportations機能について
Cloudflare ImagesのTransportations機能を使えば、画像を移動させずにCloudflare経由で配信できる。ローカルサーバーに置いた画像のURLを書き換えるだけで、Cloudflareの画像最適化やキャッシュ機能を利用できる。既存の画像をそのまま使えるので、手間をかけずに配信を改善できる。サーバーの負担を減らしながら、画像の読み込みを速くするのに役立つ。
Workersを使ったURL転送の断念
Cloudflare Workersを使えば、リクエストの内容を動的に書き換えられる。そこで、画像URLをCloudflare Imagesのものに自動変換する処理を組み込めば、既存のシステムを変更せずに移行できると考えた。実際、このサイトを参考にして試したところ、画像の変換自体は問題なく動作した。

しかし、この方法には致命的な問題があった。Workersが画像URLを書き換えることで、Pukiwikiの各種プラグインも当該URLにアクセスできなくなり、キャッシュ機能などが正常に動作しなくなった。特に、PukiwikiのrefプラグインやOGP関連のプラグインがCloudflare ImagesのURLを認識できず、適切な動作をしなくなった。
Workersに投入したがうまく行かなかったコード
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request))
})
async function handleRequest(request) {
const userAgent = request.headers.get('user-agent') || '';
const url = new URL(request.url);
const cacheDirectoryPath = '/path/to/your/cache/directory/'; // キャッシュディレクトリのパス
// 1. Cloudflare Images bot の判定 (User-Agentに基づく)
if (userAgent.includes('Cloudflare-Images')) {
// Cloudflare Images bot の場合はオリジンに転送
return fetch(request);
}
// 2. OGP取得プラグインの判定 (URLに基づく)
else if (url.pathname.startsWith(cacheDirectoryPath)) {
// キャッシュディレクトリへのリクエストの場合はオリジンに転送
return fetch(request);
}
// 3. それ以外 (来訪者) の場合は、Cloudflare Images のエッジに転送
else {
// Cloudflare ImagesのURL変換処理が必要であれば、ここに記述。
const imageURL = 'https://your-domain.com/cdn-cgi/image/...' + url.pathname; //例
return fetch(imageURL, {
headers: request.headers // 元のリクエストヘッダーを保持(重要)
});
}
}
最終的には、Workersを使うのを諦め、PukiwikiのrefプラグインやOGPプラグインを改造し、強制的に画像URLをCloudflare Imagesのものに変換するという強引な解決策を講じた。システムの根本的な修正は避けたかったが、動作を確保するにはこの方法を選ばざるを得なかった。
改造後の各種プラグイン
PukiwikiのrefプラグインとOGP関連のプラグインを改造し、画像URLを強制的にCloudflare Imagesのものに変換するようにした。具体的には、プラグイン内で画像のURLを取得する処理に手を加え、Cloudflare Imagesの形式に自動で書き換えるようにした。
なお、このサイトは画像遅延読み込みのためのlazysizes.jsも併用している。
ref.inc.php
30行目付近に設定項目PLUGIN_REF_CLOUDFLARE_IMAGESを作成
// Cloudflare Imagesを契約しているか
define('PLUGIN_REF_CLOUDFLARE_IMAGES', TRUE); // FALSE, TRUE
350行目付近の、refプラグインに画像のURLを投入した場合の挙動を修正
if ($is_image) { // 画像
if(strpos($url,$script) !== false){
$url = str_replace($script, '', $url); //ローカルサーバーでは相対パスにする
}
if ( $params['eager']) {
$lazy = "eager";
} else {
$lazy = "lazy";
}
if ( PLUGIN_REF_CLOUDFLARE_IMAGES ) {
$params['_body'] = "<img class=\"lazyload\" src=\"data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7\" data-src=\"/cdn-cgi/image/q=70,f=auto/$url\" data-srcset=\"/cdn-cgi/image/q=70%2Cw=860%2Cf=auto%2Cfit=scale-down/$url 860w, /cdn-cgi/image/q=70%2Cw=1290%2Cf=auto%2Cfit=scale-down/$url 1290w, /cdn-cgi/image/q=70%2Cw=1920%2Cf=auto%2Cfit=scale-down/$url 1920w\" alt=\"$title\" data-aspectratio=\"$width/$height\" $info>"; //Cloudflare ImagesとLazysizesを使用する場合
} else {
$params['_body'] = "<img class=\"lazyload\" src=\"data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7\" data-src=\"$url\" alt=\"$title\" data-aspectratio=\"$width/$height\" $info>"; //Lazysizesを使用する場合
}
if (! $params['nolink'] && $url2) {
$params['_body'] = "<a href=\"$url2\" title=\"$title\">{$params['_body']}</a>";
}
} else {
$icon = $params['noicon'] ? '' : FILE_ICON;
$params['_body'] = "<a href=\"$url\" title=\"$info\">$icon$title</a>";
}
ogp.inc.php
<?php
// 'ogp' plugin for PukiWiki
// author: m0370
// Twitter: @m0370
// ver1.0 (2019.9.10)
// ひとまずOGPを取得して表示する機能を実装しました。
// ver1.1 (2019.9.17)
// Cache機能を実装しました。CACHE.DIRのogpというサブフォルダにキャッシュを配置します。
// ver1.2 (2020.5.1)
// 第2引数をスタイルシートとして引用
// ver1.3 (2020.5.2)
// ファイル形式(GIF・PNGなど)を反映したキャッシュファイル名になるようにしました。従来のキャッシュも利用できます。
// ver 1.4 (2020.7.16)
// HTMLパース,noimg対応
// OGP画像がjpg拡張子になっていても中身がwebPやgzipの場合に画像が表示できない問題がある
// ver 1.5 (2021.6.14)
// キャッシュをJSON形式で保存するように変更
// ver 1.6 (2021.12.23)
// PHP8.0でエラーが出ないよう微修正
// ver 1.7 (2023.01.13)
// 無限ループにならないようにUser Agentでの読み込み防止を実装
// ver 1.8 (2025.02.09)
// Cloudflare images transformationに対応する分岐を作成
/////////////////////////////////////////////////
// 画像サイズ
define('PLUGIN_OGP_SIZE', 100); // TRUE, FALSE
// Cloudflare Imagesによるキャッシュを利用する
define('PLUGIN_OGP_CLOUDFLAREIMAGE', TRUE); // TRUE, FALSE
// WEBPファイルがあるときWEBP表示を試みる fallback
define('PLUGIN_OGP_WEBP_FALLBACK', FALSE); // TRUE, FALSE. Cloudflare imagesを使う場合はFALSE推奨
/////////////////////////////////////////////////
function plugin_ogp_convert()
{
$args = func_get_args();
$uri = get_script_uri();
$ogpsize = PLUGIN_OGP_SIZE;
$ogpurl = (explode('://', $args[0]));
$ogpurlmd = md5($ogpurl[1]);
$datcache = CACHE_DIR . 'ogp/' . $ogpurlmd . '.txt';
$gifcache = CACHE_DIR . 'ogp/' . $ogpurlmd . '.gif';
$jpgcache = CACHE_DIR . 'ogp/' . $ogpurlmd . '.jpg';
$pngcache = CACHE_DIR . 'ogp/' . $ogpurlmd . '.png';
if(PLUGIN_OGP_WEBP_FALLBACK) {
$webpcache = CACHE_DIR . 'ogp/' . $ogpurlmd . '.webp'; //webp対応
}
$browser = $_SERVER['HTTP_USER_AGENT'];
if(file_exists($pngcache)) { $imgcache = $pngcache ; }
else if(file_exists($gifcache)) { $imgcache = $gifcache ; }
else { $imgcache = $jpgcache ; }
if(file_exists($datcache) && file_exists($imgcache)) {
$ogpcache = json_decode(file_get_contents($datcache), true);
$title = $ogpcache['title'];
$description = $ogpcache['description'];
$src = $imgcache ;
} else {
require_once(PLUGIN_DIR.'opengraph.php');
$graph = OpenGraph::fetch($args[0]);
if ($graph) {
$title = $graph->title;
$url = $graph->url;
$description = $graph->description;
if( isset($graph->{'image:secure_url'}) ){
$src = $graph->{'image:secure_url'};
} else {
$src = $graph->image;
}
$title_check = mb_convert_encoding($title, 'ISO-8859-1', 'UTF-8');
$description_check = mb_convert_encoding($description, 'ISO-8859-1', 'UTF-8');
if(mb_detect_encoding($title_check) == 'UTF-8'){
$title = $title_check; // 文字化け解消
}
if(mb_detect_encoding($description_check) == 'UTF-8'){
$description = $description_check; // 文字化け解消
}
$detects = array('ASCII','EUC-JP','SJIS','JIS','CP51932','UTF-16','ISO-8859-1');
// 上記以外でもUTF-8以外の文字コードが渡ってきてた場合、UTF-8に変換する
if(mb_detect_encoding($title) != 'UTF-8'){
$title = mb_convert_encoding($title, 'UTF-8', mb_detect_encoding($title, $detects, true));
}
if(mb_detect_encoding($description) != 'UTF-8'){
$description = mb_convert_encoding($description, 'UTF-8', mb_detect_encoding($description, $detects, true));
}
$grapharray = array('title' => $title, 'description' => $description, 'src' => $src, 'url' => $args[0], 'date' => date("Y-m-d H:i:s"));
file_put_contents($datcache, json_encode($grapharray, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
if(file_exists($imgcache)) {
$src = $imgcache ;
} else if($src == '') {
$is_noimg = TRUE ;
$imgfile = touch('imgfile.jpg');
file_put_contents($jpgcache, $imgfile) ;
} else {
$imgfile = file_get_contents($src);
$filetype = exif_imagetype($src);
if( $filetype == IMAGETYPE_GIF ){
file_put_contents($gifcache, $imgfile) ;
} else if ( $filetype == IMAGETYPE_PNG ){
file_put_contents($pngcache, $imgfile) ;
} else {
file_put_contents($jpgcache, $imgfile) ;
} //どの拡張子でもない場合、ダミーjpgファイルを作る
}
} else {
error_log("OGP fetch failed for URL: " . $args[0], 3, CACHE_DIR . 'ogp/error-fetch.log');
return '#ogp Error: Failed to fetch OGP data.';
}
}
if($is_noimg != TRUE){
$is_noimg = (in_array('noimg', $args) || ( file_exists($imgcache) && filesize($imgcache) <= 1 ));
}
if($is_noimg) {$noimgclass = "ogp-noimg" ;}
if(in_array('ogp2', $args)) {$ogpclass = 'ogp2';} //ogp2
//XSS回避
$description = htmlspecialchars($description);
$args[0] = htmlspecialchars($args[0]);
//WEBP表示のfallback
if ( PLUGIN_OGP_CLOUDFLAREIMAGE) {
$src = '/cdn-cgi/image/q=60,w=300,f=auto,fit=scale-down/' . $src ;
$fallback1 = '';
$fallback2 = '';
} else if( PLUGIN_OGP_WEBP_FALLBACK && file_exists($webpcache) ) {
$fallback1 = '<picture><source type="image/webp" srcset="' . $webpcache . '"/>';
$fallback2 = '</picture>';
} else {
$fallback1 = '';
$fallback2 = '';
}
return <<<EOD
<div class="ogp $ogpclass">
<div class="ogp-img-box $ogpclass $noimgclass">$fallback1<img class="ogp-img $ogpclass lazyload fadein" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" data-src="$src" alt="$title" width="$ogpsize" height="$ogpsize">$fallback2</div>
<div class="ogp-title $ogpclass"><a href="$args[0]" target=”_blank” rel="noreferrer">$title<span class="overlink"></span></a></div>
<div class="ogp-description $ogpclass">$description</div>
<div class="ogp-url $ogpclass">$args[0]</div>
</div>
EOD;
}
?>
refプラグインの場合、通常はローカルサーバーの画像パスを出力するが、改造後はこのパスをCloudflare ImagesのURLに変換する処理を挟んだ。OGPプラグインについても同様に、OGP画像を取得する部分でCloudflare ImagesのURLを生成するように変更した。これにより、Pukiwikiの画像関連機能がCloudflare Images経由で動作するようになった。
srcsetとCloudflare Imagesの問題
imgタグにはsrcsetを使うことで、画面サイズに応じた適切な画像を指定できる。しかし、srcsetの仕様上、複数の画像URLを指定するためにコンマ(,)を使う必要がある。一方、Cloudflare ImagesのURLもパラメータの区切りにコンマを使用するため、この競合によってリンクが正しく認識されず、画像が表示されない問題が発生した。
解決策として、URL内のコンマをエンコード(%2C に置換)してみたが、Cloudflare Images側がこの形式を受け付けないのか、画像が正しく表示されなかった。ただし、Cloudflareの公式文書を見るとコンマ付きのURLでも実際の使用は問題なく行えるらしいので、何らかの設定間違いであったよう。(→現在修正を検討中)

この記事に対するコメント
このページには、まだコメントはありません。
更新日:2025-02-11 閲覧数:840 views.