見出し画像

サーバー環境に制限のある小規模サイトでog:imageを動的生成する方法

ウェブサイトを作るとき、作る側からすると全くもって面倒なことこの上ないのがSNSにシェアされた際にテキストとともに表示される「og:image」画像です。

単にリンクが貼られているよりも見た目にキャッチーですし、これが有ると無いとではクリック率にも影響するのであった方が良いのは確かなのですが、ページのソース中に画像パスとして指定するのみで、実際のページ上には表示しない画像であることも多く、デザイナーさんもしょっちゅう作るのを忘れたりします。

で、いざページをシェアをしようとした時になって「ちょっと待ってくれog:imageを作ってなかった…」となることも少なくありません。

  • SNS上での見栄えは印象に直結するので、おざなりにするわけにはいかない。

  • 1200×630ピクセルが推奨とされているが、偶然このサイズ感でいい感じの画像が用意できているとは限らない。

  • かつ、記事内容更新時にog:imageの更新に気が回らないこともある。

ということで、また言いますけど面倒なのです。

そこでつい先日公開した弊社aguijeサイトではog:imageを動的生成にして楽することにしました。今回はその方法についての解説になります。


og:image の動的生成とは

SNSを眺めていて大規模なウェブサービスの記事ページのリンクが、美しいデザインテンプレートで生成されたog:imageを伴って表示されているのを見かけたことはないでしょうか。大きなサービスになると対応していることが少なくありませんが qiitaCookpad がその例です。

quiita の og:image の例
Cookpad の og:image の例

膨大な記事数を抱えるサービスで、その og:image を人力でひとつひとつ作るというのはナンセンスすぎるので、プログラムで動的生成しているわけですが、Cookpadはエンジニアの方がその方法について解説をされています。

各社やり方は違えど、おおむね

  • サーバー上のヘッドレスブラウザ(Chrome)で、og:image用にデザインしたページをスクリーンショットとしてキャプチャ。

  • 画像をキャッシュとしてサーバー上に保存し、FacebookやTwitterのクローラーにその画像を渡す。

ということをしている筈です。


スクリーンショットAPIサービス

上記のとおり、動的生成の方法についてはおおよそ理解できました。しかし、小規模サイトの場合には使用可能なサーバー環境が制限されているため

  • ヘッドレスブラウザが要求する環境を用意できない

  • 画像処理のためのモジュールをインストールできない

等で技術的に実現困難な場合もありますし、プログラムの開発コストもままならなかったりします。

ではどうしたら良いかと言うと、この困難な部分の処理を担ってくれる「スクリーンショットAPIサービス」というものを使います。僕が調べてみて、コスト感や信頼性の面で特に良いと思えたのがThum.ioApiFlashでした。


Thum.io

  • 後述のApiFlashと比較すると実用的な機能にとどまるが、必要十分。

  • 比較検討した同等サービスのなかで無償利用枠が1,000ショット/月と群を抜いて多い。

ApiFlash

  • スクリーンショット撮影時の設定可能パラメータが豊富で非常に高機能。

  • header情報を添付可能なため、Basic認証で保護したサイトのキャプチャを撮影することも可能。

  • 無料使用枠はThum.ioと比較すると少ないが、100ショット/月までと小規模サイトならカバーできる可能性もある。


og:image動的生成のフローを検討

aguijeサイトで行った実装はスクリーンショットAPIを利用したためシンプルです。クローラーが来た際にキャッシュ用の画像ディレクトリに、目的のog:imageがあればそのまま返し、無ければスクリーンショットAPIを利用して撮影&保存処理を走らせるという形をとりました。

シーケンスダイアグラム

懸念点

クローラーが訪れた際にサーバーにog:imageが未保存の場合、上述の通りその場でスクリーンショットを撮影するという手順ですが、API側での画像処理には時間がかかるため、その間にクローラー側で画像がタイムアウトしたと判断されてしまう場合があるかも知れません。

しかし、結果として画像は問題なく生成できていることを確認できているので、時間をおけばクローラー側で再取得されて補正されるだろうと予想しています。だめであればフローを再検討する予定。


用意したもの

Thum.io のアカウント

Thum.ioですぐに発行できます。アカウントを作成して無償枠で利用するだけであれば、クレジットカードの登録などは不要でした。

アカウント発行と同時に「Key」と呼ばれるAPI利用のための認証コードが発行されるので、これを控えておく。

--

キャプチャ用ページ

スクリーンショットAPIに撮影してもらうためのページです。

  • aguijeサイトではCMSにWordPressを使っているため、記事IDを与えることで記事の内容や投稿タイプを判断し、応じた内容で出力されるページをHTML • CSSコーディングして作成しました。

  • og:imageとして撮影してもらうため、アスペクト比が1200 (W) × 630 (H)としています。

レスポンシブなコーディングになっているのはHTMLやCSSの一部を本サイトと共用しているためで、1200×630ピクセルでキッチリ作っていただいても勿論問題はありません。

トップページ用 ( http://aguije.jp/ogimage/?post_id=2 )
Works 記事ページ用 ( https://aguije.jp/ogimage/?post_id=1420 )
Player 記事ページ用 ( https://aguije.jp/ogimage/?post_id=154 )
News 記事ページ用https://aguije.jp/ogimage/?post_id=1438 )


サイトの他のページと同様にHTML • CSSコーディングですし、このページ自体が動的なので、デザインを変更したい場合に一元管理が可能です。

--

クローラー向け画像取得プログラム(ogimage.php)

 <meta property="og:image" content="https://aguije.jp/wp/wp-content/themes/aguije/assets/php/ogimage.php?post_id=2">

としてクローラーに向けて設置したプログラムです。セキュリティの兼ね合いもありそのまま掲載するわけにはいかず、以下は色々と省いて簡素にしたものですが

  • サーバー上に指定の og:image 画像が既に存在する場合にはそれを読み込み

  • 存在しなければ Thum.io にスクリーンショットをリクエストし、取得されたデータを画像保存

して、最終的に画像を出力するという構造です。

<?php

	if (array_key_exists('post_id', $_GET) && $_GET['post_id']) {
		$post_id = intval($_GET['post_id']);
	}

	$time = time();
	$ogimage_url = "http://aguije.jp/ogimage/?post_id={$post_id}&time={$time}";
	$ogimage_path = (画像ディレクトリまでの絶対パス) . "{$post_id}.png";

	//

	function get_ogimage ($_post_id = null) {
		global $ogimage_url;
		global $ogimage_path;

		if ($_post_id) {
			if (!file_exists($ogimage_path)) {

				$params = http_build_query(array(
					'url' => $ogimage_url,
				));

				$image_data = file_get_contents("https://image.thum.io/get/auth/************//width/1200/crop/630/png/wait/3/?{$params}");

				if (!empty($image_data)) {
					file_put_contents($ogimage_path, $image_data);
				}

			}
			else {

				$image_data = file_get_contents($ogimage_path);

			}

			if ($image_data) {
				return $image_data;
			}
			else {
				return false;
			}
		}
	}

	//

	$result = get_ogimage($post_id);

	if ($result) {
		header('Content-type: image/png');
		header('Content-Length: ' . filesize($ogimage_path));
		header('Content-disposition: inline; filename="' . basename($ogimage_path) . '"');
		echo get_ogimage($post_id);
	}

?>

Thum.io へのスクリーンショットリクエスト

APIに対し以下のようにしてURLをリクエストするだけで、指定したURLのスクリーンショット画像データが返ってきます。

https://image.thum.io/get/auth/************//width/1200/crop/630/png/wait/3/?url=(スクリーンショットを撮影したいページのURL)
/auth/************/ → **の部分に自分の認証用コード(Key)を指定
/width/1200 → 横幅1200ピクセル
/crop/630 → 高さを630ピクセルで切り抜き
/png → PNG画像
/wait/3 → 撮影まで3秒待機

という具合に、リクエストにパラメータを含めることでサイズ指定や撮影までの待機時間などをセットすることが可能です。Thum.ioはあまり出来ることが多くはありませんが、公式ドキュメントに従ってパラメータを変更することでカスタマイズが可能です。


設置結果

以上のようにして設置して実際にサイトを稼働させた結果、

  • FacebookやTwitterのクローラーがogimage.phpにアクセスしたタイミングでThum.ioがトリガーされ、スクリーンショットが撮影された。

  • 撮影された画像はサーバーに保存された。

  • キャッシュとして↑の画像が機能するため、同じページのog:imageを重複して撮影してしまっていることもない

という良好な結果が得られています。

Thum.io のダッシュボード。最近のアクティビティが表示される。

以下のページはog:imageを作成していませんが、Twitterに投稿する際には動的生成されたog:imageが表示されます。

https://aguije.jp/works/sashiki/


撮影されたog:imageのクオリティは問題なく、HTML • CSSコーディングを忠実に再現していますし、Webフォントも読み込まれた状態で生成ができており、非常に満足しています。

弊社サイトはせいぜい数百ページのボリュームですので、Thum.ioの月1,000ショットの無償枠で十分に賄えそうな雰囲気ですが、これだけ高水準なスクリーンショットの撮影が出来るのであれば、og:imageと言わずその他の用途にも利用を検討したいなと感じました。便利すぎる。


記事内容更新時のリフレッシュ対策

記事タイトルや記事中の写真が更新された際には、og:imageも更新したいもの。その場合には、サーバーに保存された該当記事のog:imageを削除すれば、次にクローラーが訪れたタイミングでリフレッシュされることになります。

WordPressの場合には

function delete_ogimage ($_post_id = null, $_post = null) {
	// 更新された記事IDのog:imageファイルパス
	$file_path = (画像ディレクトリまでの絶対パス) . "{$_post_id}.png";
	
	if (file_exists($file_path)) {
		unlink($file_path);
	}
}

add_action('edit_post', 'delete_ogimage', 10, 2);

などとすれば、記事更新時に自動的にog:imageを削除してくれる。


参考)シーケンスダイアグラムのMermaid記法

今回文章中に貼ったシーケンスダイアグラムの図は、Mermaid記法を使ってNotionで作図してキャプチャしました。こうした細かな図を、グラフィックツールを使わずたったこれだけのコードで書けるのは便利なことこのうえない…

sequenceDiagram
	autonumber
	participant クローラー

	box DimGray WordPress
		participant ogimage.php
		participant 画像ディレクトリ
		participant ogimage表示用ページ
	end

	box DarkSlateGray thum.io
		participant API
		participant 画像生成エンジン
	end
		
	クローラー->>ogimage.php: 画像問い合わせ
	ogimage.php->>画像ディレクトリ: 存在チェック

	alt 画像が存在しない場合
		ogimage.php->>API: 画像キャプチャを依頼
		API->>画像生成エンジン: 画像生成を指示
		画像生成エンジン->>ogimage表示用ページ: ページを見に行く
		ogimage表示用ページ->>画像生成エンジン: ページをキャプチャ
		画像生成エンジン->>API: 生成した画像データ
		API->>ogimage.php: 画像データを転送
		ogimage.php->>画像ディレクトリ: 画像を保存
	else 画像が存在する場合
		画像ディレクトリ->>ogimage.php: 画像データを読み込み			
	end

	ogimage.php->>クローラー: 画像を出力