CloudinaryでOGP Imageを自動生成してみる
当ブログをNext.jsで作り直した事は前回投稿したが、せっかくなのでqiitaやZennのように、投稿タイトルをカードにしたOGP Imageを自動生成してみたくなった。
このブログは技術系のブログによくある、記事のヘッダにその内容に関連する(?)キーボードを打つ手元、みたいなフリー画像を設定していない(そういうのが好きじゃない)ので、記事ごとにOGP Imageを設定するなら上記のようなカードを用意する必要がある。まぁ、このブログにはOGP Imageなんて別に無くたっていいんだけど。
どう実装するか
前回の投稿に書いた通り、このブログのバックエンドは相変わらずWordPressなので、PHPをガリガリ書いて自前で実装することもできるんだろうけども、できないなりにモダンな構成にしたいのでどこかのサービスを利用してみようと。
色々と調べてみた結果、「Cloudinary」に決定。似たようなサービスで「imgix」というのもあるようで、どちらかというとサイトの感じはこちらの方が好みだったけど、Cloudinaryは無料で始められるがimgixは月に最低でも$10(執筆時点)かかるということで。
今回は見送ったけれどもimgixについては以下の投稿がとても参考になった。
やってみる
CloudinaryのFreeアカウントを作って、ベースとなるカードをアップロードするところまでは問題無し。
やりたい事の要件としては
・ベースのカードにはアクセスされたくない
・URLパラメータによって他者に画像をいじられたくない
・初投稿(公開)時/タイトルを変更した時のみにAPIを叩きたい
ちなみにベースとなるカードの画像はこれ。アクセスされたくないとかいってここに上げちゃってるけど。
順番は前後するけれど、3つ目の要件はWordPressのaction / filterフックでどうにでもできる。実際に実装したのは以下のようなコード。
// 投稿の保存前に発火するアクション
add_action('pre_post_update', function ($post_id, $data) {
// 指定の投稿タイプ且つ投稿ステータスが公開時に
if ($data['post_status'] === 'publish' && get_post_type($post_id) === 'xxx') {
$before_title = get_the_title($post_id);
$new_title = $data['post_title'];
$saved_ogp_image = get_post_meta($post_id, 'ogp_image', true);
// OGP Imageがまだ生成されていない/タイトルが変更されている場合
if (empty($saved_ogp_image) || ($before_title !== $new_title)) {
// CloudinaryのAPIを叩いてOGP Imageを生成するコード(後述)
$ogp_image = *****;
// 生成したOGP Image URLを保存
update_post_meta($post_id, 'ogp_image', $ogp_image);
}
}
}, 10, 2);
まぁここまでは問題なし。次はいよいよCloudinaryのAPIを叩く部分。
まずはNode.jsで試してみた
最初はAPIがどんなもんか、Node.jsで試してみた。
Cloudinaryはドキュメントが充実していて、あらゆる言語でのサンプルコードもあるのでわかりやすい。
% yarn add cloudinary
const cloudinary = require('cloudinary')
cloudinary.config({
cloud_name: '(cloud_name)',
api_key: '(api_key)',
api_secret: '(api_secret)'
})
const TEXT = 'テスト'
const options = {
sign_url: true,
type: 'private',
secure: true,
transformation: [
{
width: 1200
},
{
width: 1000,
overlay: {
font_family: 'NotoSansJP-Medium.otf',
font_size: 50,
font_weight: 'bold',
text: encodeURI(TEXT),
letter_spacing: 5,
text_align: 'center'
},
crop: 'fit',
color: '#222222'
}
]
}
const res = cloudinary.image('base_image.png', options)
上記のコードを実行するとres
に画像タグ(<img src="xxxx">
)が返ってくる。
sign_url: true
type: 'private'
のオプションはベースとなる画像を非公開としているために必要なものらしい。
DevelopersIO
Cloudinary におけるアクセス制限の設定方法と署名付きURL
これを設定することで、APIでの画像生成はできるけど、URLのパラメータをいじって元画像を変更されるようなことは無くなる。今回の要件としてはばっちり。
また、フォントはGoogleフォントのほとんどが使用できるほか、日本語フォントでも自分でアップロードすることで利用できるようになるんだとか。
問題はPHP
Node.jsで簡単にできたからこの調子でPHPも、と思ったらそうは簡単にはいかなかった。
まずはcomposer
でSDKを入れる。v1とv2があるらしいからそこはv2にしておく。
% composer require "cloudinary/cloudinary_php:^2"
ここまではいいんだけど、公式のPHPのサンプルコードが、、
(new ImageTag('front_face.png'))
->resize(Resize::thumbnail()->width(150)->height(150)->gravity(Gravity::focusOn(FocusOn::face())))
->roundCorners(RoundCorners::byRadius(20))
->effect(Effect::sepia())
->overlay(
Overlay::source(Source::image('cloudinary_icon_blue')
->transformation((new ImageTransformation())
->adjust(Adjust::brightness()->level(100))
->adjust(Adjust::opacity(60))
->resize(Resize::scale()->width(50))))
->position((new Position())
->gravity(Gravity::compass(Compass::southEast()))
->offsetX(5)->offsetY(5)))
->rotate(Rotate::byAngle(10));
オエッ!🤮なにこれ超わかりづらい。Node.jsとの差がエグい。
しかもtype
をprivate
にする方法がまぁ見当たらない。特にv2での情報が全然ない。
ググったり、SDKのソースコードを眺めたり、それっぽい関数に適当に引数をあててみたり(当然エラー)すること数時間、公式のドキュメントの中に以下のようなコードを発見。
ImageTag::fromParams("actor", ["transformation" => [
["effect" => "cartoonify"],
["radius" => "max"],
["effect" => "outline:100", "color" => "lightblue"],
["background" => "lightblue"],
["height" => 300, "crop" => "scale"]
]]);
このfromParams
という関数を利用すればNode.jsのような書き方ができるのでは!?
この記述があるのはSDK v1の方で、そりゃ気付かない訳だ。。
fromParamsで書いてみた
生成後の画像URLを返してくれるImage
クラスで以下のように書いてみた。
引数の構造はほぼNode.jsと一緒。
<?php
namespace Cloudinary;
require_once 'vendor/autoload.php';
Configuration\Configuration::instance([
'cloud' => [
'cloud_name' => '(cloud_name)',
'api_key' => '(api_key)',
'api_secret' => '(api_secret)',
],
'url' => [
'sign_url' => true,
'secure' => true,
]
]);
function create_ogp_image($text = ''){
if(empty($text)){
return '';
}
$text = urlencode($text);
return Asset\Image::fromParams('base_image.png', [
'type' => 'private',
'transformation' => [
[
'width' => 1200
],
[
'width' => 1000,
'crop' => 'fit',
'color' => '#222222',
'overlay' => [
'font_family' => 'NotoSansJP-Medium.otf',
'font_size' => 50,
'font_weight' => 'bold',
'text' => $text,
'letter_spacing' => 5,
'text_align' => 'center'
]
]
]
]);
}
すると見事に成功!🎉
上記のWordPressのフックと合わせて今回は完成。
自分の英語力不足のせいなのか、情報は豊富だけど見づらい(?)Cloudinaryのドキュメントのせいなのか、なんかとても疲れた。。
コメント0