以前、任意の文字列をブログカードに変換できるような仕組みをプラグインを使わず実装したのですが、外部サイトについては用意されているOGP(Open Graph Protocol)画像、いわゆるアイキャッチ画像への直リンクとなっていたので、WP_Filesystem()を使い、対象となる外部リンクのアイキャッチ画像を自サーバーに取り込んでから使えるように改造しました。

当サイトで実装しているブログカードは、以下のような仕様です。

投稿画面でブログカードにしたいリンク情報を、独自にclass指定した<span>タグで囲んでおくと、

1
<span class="bcard ilink"><a href="https://cosybench.com/make-the-original-blog-card/" title="【WP】ブログカードの実装: 任意の文字列対応、プラグインなし | 日なたの縁台" target="_blank">【WP】ブログカードの実装: 任意の文字列対応、プラグインなし | 日なたの縁台</a></span>

このようなブログカードになります。

WordPress以外の環境に移った場合でもリンク情報をそのまま使えるようにしておきたくてこのような仕様にしたのですが、冒頭にも書いたとおり、外部サイトのブログカード画像だけは、そのままでは使用に問題がある状態だったので、そこをきちんと使えるように直しました! という記事です。

目次

実装した外部用ブログカードが抱えていた問題点

内部リンク(自分のサイト内でリンクを張ったもの)のブログカードについては、自分のサーバーからアイキャッチ用の画像を読み込ませるので何の問題もない一方、外部リンクのものについては、そのサイトで設定されているOGP画像への直リンクとなっており、外部サイトのサーバーに負荷をかけてしまうという、とてもヨロシクナイ仕様になっていました。

【直リンクNGな訳】
外部サイトのアイキャッチ画像が直リンクになっていると、外部サイトのブログカードが含まれた記事を開くときに、その記事が置かれているサーバーだけでなく、外部サイトが置かれているサーバーにも「情報を渡してください」という要求が出ることになり、相手方のサーバーにも負荷をかけてしまうことになるため。

なので、オリジナルブログカードを実装したものの、使うのは内部記事に対してだけで、外部サイトへのリンクはブログカード化せずに使っていたのですが、せっかくOGP情報までは取得出来ているのだから、なんとか有効活用したい…!

ということで、色々調べて試してきた結果、ようやくひとまず形になったので、やり方をまとめてみました。

外部サイトのOGP画像を自サーバーに保存してから利用することについて、著作権的にどうなんだろう? とは気になり、ちょっとググって見たりしたのですが、しっかりリンクが張られ出典が明記されていればOKでは? という雰囲気のようです。
【Web】OGP(Open Graph protocol)の著作権についてです – 弁護士ドットコム
【Web】OGP(Open Graph protocol)著作権 データ保存に関して – 弁護士ドットコム

WP_Filesystem()で解決!

PHPで外部サイトの画像を保存する方法を調べてみると、「file_get_contents()でデータを取得し、file_put_contents()で保存すればよい」ということが分かりました。が、WordPress環境でそれらの関数を使おうすると警告が出るとのこと。

代わりに、セキュリティ面(ファイルのPermissionやOwnership)が考慮されたWP_Filesystem()を使うことが推奨されています。

実装

そんな訳で、外部サイトのOGP画像(アイキャッチ画像)取得問題を解決するため、functions.phpに新たなオリジナル関数cb_fetch_ogp_image_file()を定義しました。

基本的には、file_get_contents()file_put_contents()で行える内容を、WP_Filesystem()によるものに置き換えているだけです。あとはプラスαとして、画像ファイルの拡張子判定やリサイズなども行っています。(と、さらっと書いていますが、だいぶ試行錯誤しました、笑)

より具体的には、

  1. 外部サイトのOGP画像のURLを取得できていたら、そのURLから画像データを取得し
  2. 拡張子判定やリサイズをした上で自サーバーに保存して
  3. 最後に、自サーバーに保存した画像のURLを返す

という処理をしています。

以下、コードです。

functions.phpの編集前は必ずバックアップを取り、いつでも元の状態に戻せるようにしておきましょう。

コードの流れや説明は、コメント部分に記述しています。また使用した関数のマニュアルへのリンクを、参考までに、@link:として書いてあります。
またコメント部分に記載すると、さすがに冗長すぎるかな? と判断した情報については、「コードの補足」として分けました。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
/* 2018/11/21: ファイル名を付ける部分のプログラムを変更(データのhashではなく、ファイルパスのhashへ) */
/**
* 最初のif文で、定義されている関数名に重複がないか確認します。
* 当サイトはドメインが「cosybench.com」なので、接頭辞的な「cb_」を関数の頭につけることで、重複避け対策をしています。
**/
if (!function_exists('cb_fetch_ogp_image_file')){ //子テーマを使っている場合、推奨

// ここから関数本体
/**
* 外部サイトのOGP:imageに設定されている画像を、自分のサーバーの任意のディレクトリに保存する
* @param   string   image_url (OGP:imageのURL)
* @return  string   path(自サーバーのファイルへのURLパス)
**/
function cb_fetch_ogp_image_file ( $image_url ) {
  /*
  * WP_Filesystem()を使うためには「file.php」が必要になるので、読み込みます。
  * 「image.php」は画像を編集するwp_get_image_editorで必要になるので、ここで一緒に読み込んでいます。
  */
  //WP_Filesystemを使用するためのファイルの読み込み
  require_once(ABSPATH . 'wp-admin/includes/file.php');
  require_once(ABSPATH . 'wp-admin/includes/image.php');

  // サーバーの画像を保存するディレクトリを設定
  /*
  * FTPソフトを使い、あらかじめ「uploads」フォルダ内に「elink-img」という専用フォルダを作っておく方式にしました。
  * フォルダのパーミッションは、自分のみ読み書きOK、その他は読み取り専用にします。
  */
  $dir = ABSPATH . 'wp-content/uploads/elink-img/'; //ABSPATHで、サーバー上の「public_html」フォルダまでの絶対パスが取得される(取得されない場合はwp-config.phpファイルの設定を確認)

  /* OGP画像のURLパスから、ファイル名を付けます。 */
  // @link: http://php.net/manual/ja/function.hash.php
  $image_name = hash('md5', $image_url, FALSE); // ファイルURLからファイル名を決める

  /* [elink-img」フォルダ内にある画像ファイルのリストを取得します */
  // @link: http://php.net/manual/ja/function.glob.php
  $image_lists =  glob($dir .'*', GLOB_NOSORT); // $dirでのファイルのリストを取得

  // @link: http://php.net/manual/ja/function.preg-grep.php
  $file_existance = preg_grep("/$image_name/", $image_lists); //ファイルの有無を確認

  if (!$file_existance): //ファイルがまだ作られていなかったら、画像を取得・保存する
      /*
      * WP_Filesystem()の認証に問題が発生しなければ、$wp_filesystemというグローバル変数を使えるようになります
      */
      if ( WP_Filesystem() ): //WP_Filesystemの初期化
          global $wp_filesystem; //$wp_filesystemオブジェクトの呼び出し

          /*
          * WP_Filesystem_Direct()クラスに用意されているメソッドを使います。
          * @link: https://developer.wordpress.org/reference/classes/wp_filesystem_direct/
          * == コードの補足(1) 参照==
          */
          $image_data = $wp_filesystem->get_contents($image_url); // 画像のバイナリデータを取得(PHPのfile_get_contents()と同じ)

          /*
          * $wp_filesystem->get_contents($image_url) で画像データが取得できたら、そのデータから拡張子の判定をしています。
          * == コードの補足(2) 参照==
          * そして最後に、取り込んだ画像のリサイズをしています。
          */
          if ($image_data): //画像データが取得できたら

              // MIME Typeをチェック
              // @link: http://php.net/manual/ja/function.finfo-open.php
              // @link: http://php.net/manual/ja/function.finfo-buffer.php
              // @link: http://php.net/manual/ja/fileinfo.constants.php
              $finfo = finfo_open(FILEINFO_MIME_TYPE);
              $mime = finfo_buffer($finfo, $image_data);
              finfo_close($finfo);

              switch ($mime) { //拡張子の決定、現在は以下の4タイプの画像のみ可(bmpはなくてもよいかもしれない)
                  case "image/jpeg":
                      $extension = '.jpg';
                      break;
                  case "image/png":
                      $extension = '.png';
                      break;
                  case "image/gif":
                      $extension = '.gif';
                      break;
                  case "image/bmp":
                      $extension = '.bmp';
                      break;
                  default:
                      echo "対応していない画像ファイルです";
                      break;
              }

              //画像を保存(PHPのfile_put_contents()と同じ)
              //ファイル名には、接頭辞的に「cb_」を付けています
              $wp_filesystem->put_contents($dir. "cb_" .$image_name. $extension, $image_data);

              //画像の編集(リサイズ)
              //@link: https://developer.wordpress.org/reference/classes/wp_image_editor/
              $image_editor = wp_get_image_editor($dir. "cb_" .$image_name. $extension);

              if ( !is_wp_error($image_editor) ): //画像編集オブジェクトに画像が正常に読み込めたら
                  // @link: https://developer.wordpress.org/reference/classes/wp_image_editor/resize/
                  $image_editor->resize(400, 400, false); //画像のリサイズ(W400×H400 cropせず との設定.ここは用途に応じてお好みでOK)
                  // @link: https://developer.wordpress.org/reference/classes/wp_image_editor/save/
                  $image_editor->save( $dir. "cb_" .$image_name. $extension ); //編集した画像の保存
              else:
                  echo "image_editorでエラーが発生しました";
              endif;

              $upload_dir = wp_upload_dir(); //「uploads」フォルダまでのURLパスを取得する
              // @link: https://developer.wordpress.org/reference/functions/wp_upload_dir/
              return $upload_dir['baseurl'] . "/elink-img/cb_" .$image_name. $extension;

          else:
              echo "画像データ取得できませんでした";
          endif;

  else: // ファイルがあったら、ファイル名を取得する

      //ファイル名を配列($file_existance)からを取り出す
      //@link: http://php.net/manual/ja/function.array-shift.php
      $filename = array_shift($file_existance); //ファイル名は、ローカルパス($dir/$image_name.$extension)形式 になっている

      //@link: http://php.net/manual/ja/function.str-replace.php
      $filename = str_replace($dir,"",$filename); //ファイル名から$dirの文字列を削除する

      //URL形式でファイルへのパスを設定する
      //@link: https://developer.wordpress.org/reference/functions/wp_upload_dir/
      $upload_dir = wp_upload_dir();
      return $upload_dir['baseurl'] . "/elink-img/" .$filename;  // https://example.com/wp-content/uploads/elink-img/filename となる

  endif; // WP_Filesystem() 終了
} // 「function cb_fetch_ogp_image_file($image_url)」 閉

} // 「if (!function_exists('cb_fetch_ogp_image_file')){」 閉

コードの補足(1): $wp_filesystemで使えるメソッド

WP_Filesystem()は、プラグイン開発をされている方々にはお馴染みのもののようですが、私は「お初お目にかかります」だったので、これで合ってるのかな? と、いまだにちょっと首をひねりつつ使っている部分があります。

特に、プラグイン周りでこの関数を使う場合は、セキュリティ上のことを考慮し、SSHやFTPなど、どのような通信方式でファイルの移動や書き込みを行うかという設定をするのですが、adminとしてファイル操作を行うならば、そのような認証は必要ないようです。

そしてその場合、WP_Filesystem()とコールして使えるようになるグローバル変数$wp_filesystemから使えるメソッド(methods)は、WP_Filesystem_Directクラスで用意されているものになるのだと思われます。(ここがよくわからず、自信がないのですが、デバッグ中に吐き出されるエラーメッセージを見ている限りでは、そうでした)

▼ ちょっと情報は古いかもしれないですが、認証まわりに関することが書かれています。
ファイルシステムAPI – WordPress Codex 日本語版

WP_Filesystem_Directクラスと用意されているメソッド一覧
WP_Filesystem_Direct | Class | WordPress Developer Resources

▼ 他にも、WP_Filesystem_BaseWP_Filesystem_SSH2クラスなどがありますが、それらは使えませんでした。

コードの補足(2): ファイルの名前付けや拡張子の判定について

当初は、外部サイトのOGP画像に元々付けられているファイル名から、PHP関数のsub_str()strpos()などを利用してファイル名の取得と拡張子の判定を行っていたのですが、まれに拡張子が付けられていないOGP画像があったり、ファイル名に特色のないものもあったり。

なので、もしかすると近い将来、元々ついているファイル名を利用しているとファイル名が重複してしまう日が来るかも? と思ったので、OGP画像のURLからファイル名を設定するようにしました。ファイル名はhash(ハッシュ)化しているため、無味乾燥な文字列になります。

【情報更新: 2018/11/21】
最初は、画像データからファイル名を付けていたのですが、URLを使えば処理が早そうだし、重複することもないか! と気づいたので、変更しました。それに伴い、プログラムの構成も少し変えました。

実装済プログラムの微修正(ブログカード用)

あとは、【WP】ブログカードの実装: 任意の文字列対応、プラグインなしで定義していたoriginal_blog_card()関数を、1箇所だけ修正します。

修正するのは、「外部リンクの場合」部分で、opengraph.phpというオープンソースを利用して、外部サイトのOGP情報から画像へのリンクを取得していたところです。 (使用しているシンタックスハイライター「highlight.js」は行数表示がないので、こういうときはちょっと不便ですね: 【WP】highlight.js の使い方: 必要ページだけで読み込むカスタマイズ付

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
(省略)
/* 該当部分のみ抜粋しています */
elseif ($m[1] == 'e') : //外部リンクの場合(external)
  //設定されているOGP情報を利用する
  //@link https://github.com/scottmac/opengraph
  require_once('opengraph.php');

  $graph = OpenGraph::fetch($url);
  if ($graph) : //OGP情報が設定されていたら
      $title = $graph->title ? $graph->title : $m[3];
      $excerpt = $graph->description ? $graph->description : '';
      /** ここ($src = ~)を修正↓ **/
      // 修正前: $src = $graph->image ? $graph->image : get_template_directory_uri().'/images/ogp_no_image.jpg';
      // 修正後:
      $src = $graph->image ? cb_fetch_ogp_image_file($graph->image) : get_template_directory_uri().'/images/ogp_no_image.jpg';
      $thumbnail = '<div class="blog-card-thumbnail"><img src="' .$src. '"alt="' .$m[3]. '"></div>';
  endif;
  $date       = ''; //外部リンクは投稿日なし
  $target     = 'target="_blank"'; //外部リンクは新規タブで

endif;
(省略)

旧バージョンでは$graph->imageで外部サイトのOGP画像への直リンクになってしまっていたのを、今回新しく定義したcb_fetch_ogp_image_file()関数を使い、その引数を$graph->imageで取得される外部サイトのOGP画像のURLにすることで、自サーバーに保存された画像データへのリンクに置き換えます。

▼ opengraph.php
今回の実装で、このブログカードに使用している画像が自サーバーに保存されたものになっています。

まとめ

そんなこんなで、オリジナルブログカードの実装例をまとめた記事を公開してから4ヶ月以上経ってしまいましたが、ようやく、相手方(外部サイト)のサーバーに負荷をかけることなく、もともと設定されていたOGP画像を自前のブログカードのアイキャッチ画像として使えるようになりました。

いまのところ、特に目立った不具合は生じていませんが、もしかすると迂遠なコードになっていたり、盛大に間違えていたりする部分もあるかもしれませんので、参考にされる場合はどうか自己責任でよろしくお願いいたします。

以上、外部サイトをブログカード化するときのアイキャッチ画像をどうすればいいか問題の解決策でした。
WP_Filesystem()を使ってアレコレやっている、日本語で書かれた事例もまだまだ少ないようだったので、どなたかの参考になれば幸いです。

さらに改造

ブログカードの表示に必要なデータの読み込みに時間がかかっていたので、キャッシュを利用するようにしました。