豪鬼メモ

一瞬千撃

OGPの仕様の解釈と実用性

自作CMSで書いた記事を各種SNSでシェアできるようにするために、今更ながらOGPの仕様を読んでみた。自作CMSで実装もした。


OGPはOpen Graph Protocolの略で、任意のWebページをソーシャルグラフ上でリッチオブジェクトとして表現するための表現方法だそうな。現実的には、FacebookTwitterなどのSNSで記事をシェアするための機能である。仕様と言っても簡素なもので、ogp.me公式の短い説明文を読めば把握できるものだ。

わざわざ新しい規格を作らなくても、既存のtitle要素やmeta要素のname/contentペアを使い回せば良さそうな気がするが、OGPを策定した陣営が既存の規格との互換性を気にせずに任意に仕様を決められるというのがOGPの最大の利点なのだろう。仕様の要点は以下のものだ。

  • HTML要素かhead要素の属性に、「prefix="og: https://ogp.me/ns#"」という属性をつける
  • meta要素に「property="og:title" content="ページのタイトル"」という属性をつける
  • meta要素に「property="og:type" content="ページのタイプ"」という属性をつける
  • meta要素に「property="og:url" content="ページの正規化URL"」という属性をつける
  • meta要素に「property="og:image" content="ページのサムネイル画像のURL"」という属性をつける

その他に、省略可能だが、meta要素に以下の属性をを使っても良い。

  • og:descrtiption : ページの説明文。任意の文字列
  • og:locale : ページのロケール。en_USとかja_JPとか。
  • og:site_name : ページが属するサイトの名前。

og:title、og:type、og:url、og:imageの4つの属性は必須(required)と書いてあるが、画像がないWebページでも無理やりサムネイル画像を作らにゃならんのだろうかと疑問に思う。実際のところ、省略してもFacebookTwitterでシェア機能は動くので、なんで必須にしているのかはよくわからない。

og:title属性と元来のtitle要素の使い分けも悩ましいところだ。title要素はブラウザのタブのラベルやブックマークのラベルとして使われるから、「ブログ名: 記事名」という情報を載せるのが普通だろう。その一方で、OGPではog:titleとog:site_nameが分かれているので、og:titleは記事名、og:site_nameがブログ名だと考えるのが自然だ。ブログのトップページではog:titleもog:site_nameもブログ名になるだろう。

og:typeは、具体的に何に使われるのかよくわからないし、FacebookでもTwitterでも特に使われていないっぽい。とりあえずトップページはwebsite、各記事はarticleにしておけば問題ないだろう。

og:descriptionと元来のmeta name=descriptionとの違いだが、これは全く同じでいい気がする。従来の検索エンジンでは相変わらずname=descriptionが使われるだろうから、値が同じであっても両方指定することになるだろう。具体的な値は、ページの冒頭の一文か二文を取り出せばいいだろう。

og:localeとhtml要素のlang属性は役割が一部被っているが、地域コードが設定できる分だけog:localeに存在価値がある。Google検索もこの属性を読むらしいが、lang属性があればそちらも読むので、不要だ。とはいえ、OGPの仕様上はog:localeのデフォルトはen_USということになっている(米国中心主義かよ)ので、それ以外の言語の場合には一応指定した方がいい。地域コードの部分が不明または不特定の場合にはZZが使える(RFC5646)ので、ja_JPのJPの部分の設定が面倒ならja_ZZと書いても良いはずだ。

以上をまとめると、この記事のHTMLは以下のような感じにすべきだろう。

<html>
<head prefix="og: https://ogp.me/ns#">
<title>豪鬼メモ: OGPの仕様の解釈と実用性</title>
<meta name="description" content="自作CMSで書いた記事を各種SNSでシェアできるようにするために、今更ながらOGPの仕様を読んでみた。" />
<meta property="og:title" content="OGPの仕様の解釈と実用性" />
<meta property="og:type" content="article" />
<meta property="og:url" content="https://mikio.hatenablog.com/entry/2024/05/24/130444" />
<meta property="og:image" content="https://cdn-ak.f.st-hatena.com/images/fotolife/f/fridaynight/20240519/20240519144752.jpg" />
<meta property="og:description" content="自作CMSで書いた記事を各種SNSでシェアできるようにするために、今更ながらOGPの仕様を読んでみた。" />
<meta property="og:locale" content="ja_ZZ" />
</head>
<body>...</body>
</html>

BikiBikiBobにそのような実装を入れたところ、FacebookはてなブログでURLを張り込むとうまく表示できるようになった。以下がまさにはてなの例だ。
dbmx.net

Facebookだとこんな感じ。Facebookでうまく動くかどうかは、Facebook公式のデバッグ用サイトにURLを入れれば確認できる。

TwitterだとなぜかODPがちゃんと読まれないという問題には数時間ほど悩ませられた。Twitter公式のデバッグ用サイトもあるが、エラーの原因を全く教えてくれないので使いにくいし、そもそもそのサイトはメンテが放棄されている。実際のTwitterのWebサイトかアプリで確認しろというのが公式の見解だが、それは単にデバッグ環境の提供を放棄しているだけだろう。仕方ないので、こちらで原因をいろいろ推測して試行錯誤するしかなかった。結論としては、TwitterがページのOGPを確認するために動かすTwitterbotはMIMEがHTML(text/html)の場合しか動かず、XHTML(applicatoin/xhtml+xml)だと知らんぷりするというのが原因であった。BikiBikiBobはXHTMLを吐き出すが、HTMLとして解釈しても問題ないのでMIMEをHTMLにすればこれは解決できる。ただ、XHTMLとして配信した方がタグの不一致とかがすぐに気づけてメンテしやすいし、構造化文書を配信しているという矜持があるので、できればXHTMLとしての配信にしたい。となると、Twitterbotに対してだけMIMEをHTMLとして配信すればよい。Apache2の場合、.htaccessに以下のよう書けばいい。ちょっとダサいけど、いずれTwitterが改心してくれると信じてこの場当たり的解法で行く。

<Files ~ "(\.(xhtml)$)|(^$)">
  SetEnvIf User-Agent "Twitterbot/" ua_html_only
  Header set Content-Type "text/html; charset=UTF-8" env=ua_html_only
</Files>

Twitterに関しては、さらにmeta要素で以下のようなことを書き加える。実際のところ、twitter:cardだけが必要で、でかい画像を表示したい場合は値をsummary_large_imageにして、小さめ画像で良いなら値をsummaryにする。twitter:siteとtwitter:creatorTwitter上のアカウント名を指定するのだが、何に使われているのかよくわからんし、省略しても普通に動く。

<meta name="twitter:card" content="summary_large_image"/>
<meta name="twitter:site" content="@estraier"/>
<meta name="twitter:creator" content="@estraier"/>

これで、Twitterでもめでたくシェア機能が動くこととなった。以下のように表示される。

カードの種類をsummary_large_imageからsummaryに変えると、以下のような感じになる。こちらの方が説明文も入るので、アイキャッチ画像の出来不出来だけに左右されなくて使いやすそうな気もする。自分でシェアしてその際に任意の説明文を書きたいならsummary_large_imageが適して、ページ内の説明文を誰がシェアしても表示されるようにしたいならsummaryが適しているだろう。

ちょっと前に話題になったBlueSkyでもちゃんと表示される。

ところで、property=og:descriptionないしname=descriptionの内容を自動抽出するにあたって、記事の最初のテキスト150文字くらいを取り出すという実装が普通に考えつくし、実際に私もその実装を施した。それを前提とすると、記事の書き方が変わってくる。記事のタイトルに続く最初の行は見出しではなく、トピックセンテンスの地の文であるべきである。例えば以下のような感じだ。

@title エロマンガ先生

高校生兼ラノベ作家の和泉マサムネには、引きこもりの妹・和泉紗霧がいる。まったく部屋から出てこない妹を心配しつつもマサムネは自立のため、作家業に勤しんでいた。
...

上記の記事からdescriptionを生成すると、「高校生兼ラノベ作家の和泉マサムネには、引きこもりの妹・和泉紗霧がいる。まったく部屋から出てこない妹を心配しつつもマサムネは…」みたいな感じになる。

一方で、従来のWebサイトとしての書き方をすると、全ての地の文の前に見出しを置きたくなってしまうので、以下のようになる。

@title エロマンガ先生

* 概要

高校生兼ラノベ作家の和泉マサムネには、引きこもりの妹・和泉紗霧がいる。まったく部屋から出てこない妹を心配しつつもマサムネは自立のため、作家業に勤しんでいた。
...


上記の記事からdescriptionを生成すると、「概要 高校生兼ラノベ作家の和泉マサムネには、引きこもりの妹・和泉紗霧がいる。まったく部屋から出てこない妹を心配しつつも…」みたいな感じになる。大して変わらないとも言えるが、ほぼ全ての記事が概要から始まるのだから、descriptionの最初に「概要」が来るのがダサい。ならば、そもそもdescriptionの自動抽出時に見出しは省くという方法も考えられるし、実際そうしようかとも思った。しかし、結論としては、CMS側でfool proofな対応をするのではなく、ユーザが記事の書き方を修正すべきだというのが結論だ。Wikipediaの多くの記事も最初の段落の前には見出しは来ないようになっているし、このブログだって同じような方針で書いている。

サイトのトップページだけは、最初の一文をそのままdescriptionにするのは不都合がある。「このサイトについて」的なものがdescriptionに来るべきだが、ページを開いた際に毎回「このサイトについて」をファーストビューにするわけにはいかない。ほとんどの場合、新着記事やおすすめ記事のリストがファーストビューになるべきで、「このサイトについて」はページの下の方かサイドバーあたりにひっそり置く感じになるだろう。descriptionはそのページを見たことがない人のための情報だが、トップページは再訪してくれた常連さんのために最適化しなければならない。よって、CMSの機能として、descriptionを明示的に指定できるようにすべきだ。同様にして、アイキャッチ画像も任意に指定できるようにすべきだ。そうすると、例えばこのブログのトップページをCMSの記事データとして書き直すなら以下のような感じになるだろう。

@title 豪鬼メモ
@description プログラミングと自転車とカメラの話を取り止めもなく書き散らすブログです。
@image https://example.com/data/eyecatch.jpg [eyecatch]

* 新着

@index [order=date_r] [max=10]  // 新着記事のリストを自動挿入 

* このサイトについて

プログラミングと自転車とカメラの話を取り止めもなく書き散らすブログです。DMは hogehoge@example.com まで。

まとめ。自作CMS上の記事を各種SNSにシェアするために、OGPの仕様を把握した。Twitter以外はOGPのデータを書けば素直に動くが、Twitterだけはちょっと面倒な手順が必要だったが、他はOGPだけちゃんと実装すればいける。OGPの仕様を把握すると、それに最適化して記事を書くようになる。検索エンジンの仕様に基づいてSEO的に記事を書くのと同じ理屈だ。