nikkie-ftnextの日記

イベントレポートや読書メモを発信

Sphinxはmetaディレクティブだけで、property属性を持ったOGP用のmetaタグがHTMLに作れちゃうんです!

はじめに

CHASE!、nikkieです。

OGP(Open Graph protocol)に関するSphinxネタです。
発表スライドはSphinx拡張で作ってGitHub Pagesで公開している1ので、Sphinxで作るHTMLのOGP用のタグ設定はそれなりに経験があるつもりでした。
この週末、今回紹介するやり方を知って衝撃を受けました。

目次

結論:metaディレクティブでproperty属性を持ったmetaタグを作る方法

.. meta::
    :property=og:title: The Rock

:property=og:title:という指定により、ビルドしたHTML中のmetaタグのproperty属性がog:titleと指定されます!

Open Graph protocolとは(簡単に)

SNSFacebookTwitter)にWebページのURLを貼ると、そのSNS内のコンテンツのように展開されますよね。
この仕組みがOpen Graph protocol(略してOGP)という仕組みなのです!

ドキュメントの「Basic Metadata」に例があります。
HTMLのheadタグの中に、property属性とcontent属性を指定したmetaタグを書けばいいのです。

<head>
  <meta property="og:title" content="The Rock" />
</head>

HTMLをWebに公開するとき、headタグにOGP用のmetaタグを書いておけば、そのページがSNSでシェアされるときの見た目がいい感じになります。

SphinxでHTMLにメタデータを指定する

さて、ドキュメント変換ツールSphinxは、原稿をHTMLに変換する際、メタデータ(headタグの中のmetaタグたち)を指定できます。
https://www.sphinx-doc.org/ja/master/usage/restructuredtext/basics.html#html-metadata

metaディレクティブを使います。

.. meta::
   :description: The Sphinx documentation builder
   :keywords: Sphinx, documentation, builder

これでname="description"name="keywords"というmetaタグを指定できます。

<meta name="description" content="The Sphinx documentation builder">
<meta name="keywords" content="Sphinx, documentation, builder">

SphinxでHTMLにOGP用のmetaタグを設定できる!

OGPではname属性ではなくproperty属性が必要ですが、name属性は指定せずにproperty属性を指定するという書き方ができるんです!
MyST-Parser2のドキュメントを見ていて気付きました。
https://myst-parser.readthedocs.io/en/latest/syntax/syntax.html#setting-html-metadata

.. meta::
    :property=og:locale: en_US

:property=og:locale:!!
こんな指定の仕方が可能なんですね!
これにより、metaタグは、name属性の代わりにproperty属性を持ちます!

<meta content="en_US" property="og:locale" />

metaディレクティブでOGP用のmetaタグを指定した例

Sphinxで作ったランディングページ3に早速採用しました。

これまではSphinxのテンプレートを使ってOGP用のmetaタグを直に書いていました。
今回知った書き方をもとに、metaディレクティブでOGPの設定をすることにしました。
メタデータの設定が1箇所のmetaディレクティブに集まるのがいいなと感じています。

Future works: 完全にmetaディレクティブに置き換えきれず

metaディレクティブを使ったときに、以下のmetaタグがまだ作れていません。

<meta name="twitter:site" content="@pyconjapan">

content属性の値の@がHTMLエンティティになりました(エスケープ処理と理解しました)。

.. meta::
    :twitter:site: @pyconjapan
<meta content="&#64;pyconjapan" name="twitter:site" />

すぐに解決できなさそうだったので、現在はテンプレートとmetaディレクティブの併用となっています。

IMO:SphinxでOGP用のタグを設定する方法

ここでmetaディレクティブを使った例を紹介しましたが、常にオススメとは考えていません。
Sphinxで作ったランディングページは1枚の小さいWebページであり、ライブラリの依存を増やすよりはmetaディレクティブの使いこなしでできそうなので採用しました。
Webページが増えてきたら(例:私の発表スライドのGitHub Pages)、sphinxext-opengraphというライブラリがよさそうに思います。

私はこれから素振りするところですが、Sphinx-Users.jpで採用されています。
Twitter/Facebookへのページシェアでコンテンツを埋め込む(OGP) — Python製ドキュメンテーションビルダー、Sphinxの日本ユーザ会

Sphinx-users.jp 自体のOGP生成も独自実装(本ページで紹介しているコード)から、sphinxext-opengraphに切り替えました。

:property=og:locale:と書いて属性込みで指定もできるのはどういう仕組み?

なんでこの書き方ができるんだろう? この書き方をしたときにmetaタグにname属性ができないのはなぜだろう? と気になってしまい、ソースコードを覗きました。
Sphinxのドキュメントでmetaディレクティブが紹介されていますが、実装はdocutilsにあります4Sphinxは使っているだけという理解です)。

※時間を区切って調べたので、読み間違えている可能性もあります。お気づきの点があれば@ftnextまでお知らせいただけると大変助かります

ランディングページの開発環境のソースコードを読みました。

docutils                      0.17.1
Sphinx                        4.5.0

reSTに書いたmetaディレクティブをdocutilsのnodeにパースするのに使われるのが、docutils.parsers.rst.directives.html.MetaBodyクラスかなと思います。
関連すると判断した箇所を抜粋します。

class MetaBody(states.SpecializedBody):

    def field_marker(self, match, context, next_state):
        node, blank_finish = self.parsemeta(match)
        # 省略

    def parsemeta(self, match):
        name = self.parse_field_marker(match)
        # 省略
        tokens = name.split()
        try:
            attname, val = utils.extract_name_value(tokens[0])[0]
            node[attname.lower()] = val
        # 省略

parse_field_markerメソッドはSpecializedBodyクラスのベースクラス、docutils.parsers.rst.states.Bodyに定義されています。
MetaBody <- SpecializedBody <- Body

実装を引用します。

    def parse_field_marker(self, match):
        """Extract & return field name from a field marker match."""
        field = match.group()[1:]        # strip off leading ':'
        field = field[:field.rfind(':')] # strip off trailing ':' etc.
        return field

先頭と末尾のコロンを外すので、":property=og:locale:"という文字列は"property=og:locale"となりますね。

そしてextract_name_value関数が、key=valueという形式の文字列を(key, value)に変換します。
docutils/utils/__init__.pyに定義されており、以下に引用します。

def extract_name_value(line):
    """
    Return a list of (name, value) from a line of the form "name=value ...".

"property=og:locale"という文字列は("property", "og:locale")となりますね。

MetaBodyの実装に戻ると、node[attname.lower()] = valnode"property""og:locale"が設定されます。

なお、extract_name_valueNameValueErrorを送出すると(key=valueという形式ではないときですかね)、MetaBodynode"name"に値を設定します。

以上、metaタグがname属性の代わりにproperty属性を持つ実装の読み解きでした!

終わりに

metaディレクティブだけでOGP設定ができたという衝撃をアウトプットしました。
私は仕組みを知ることが好き、かつ、どうしても必要なものでない限りは開発の依存関係は増やしたくないという立場なので、metaディレクティブだけで設定できるというのはかなり好感触です。
一方、複数のHTMLからなるWebサイトも扱っているわけで、そちらではsphinxext-opengraphがなかなかよさそうに思われます(素振りだ!)。

metaディレクティブの使い方に詳しくなったことで、どこまでmetaディレクティブを使い、どこからはサードパーティの拡張(巨人)の肩に乗るか、指針が持てた気がします!