LOST IN BLUE

2024/01/13

OGPを使ったリンクカード実装の悩み

以下のような OGP を使ったリンクカードをこれまで Next.js や Remix で実装してきたので、今回 Remix で実装した際にちょっと苦労したところも併せてご紹介します。

(追記)この記事を上げたあとに気づきましたが、今回の Remix での実装方法だと、OGP の情報を取得するまでレスポンスがブロックされるので、ファーストビューの表示が遅くなってしまいます。項目を加筆しました。

外部データを取得する場所と使う場所の乖離

OGP を使ったリンクカードが一筋縄ではいかないのは、クライアントサイドでは同一オリジンポリシーによって、外部サイトの OGP 情報はほとんど取得できないことに端を発します。

そのため、Next.js や Remix だけで実装するためには、クライアントでのレンダリング処理よりも前に外部サイトから OGP 情報 を取得しておく必要があります。

ルートファイルで、データを使おうとするコンポーネントのリストを取得してデータをそれぞれに引き渡すという方法が考えられますが、コンポーネント自身がデータを取得して使う方が見通しが良いです。
そのため、コンポーネントで外部データを取得する(ように見える)方法を Next.js と Remix の両方で取りました。

Next.js での実装

Next.js (SSG) では、以下で書いたように、ビルド時とレンダリング時に異なる挙動をするコンポーネントを用意して、Context を用いて GetStaticProps 内でデータの取得を行い、レンダリング時にはそのデータを取り出して使う実装をしていました。

Remix での実装

(追記)この記事を上げたあとに気づきましたが、今回の Remix での実装方法だと、OGP の情報を取得するまでレスポンスがブロックされるので、ファーストビューの表示が遅くなってしまいます。なんとかしたいですね。

Remix でも、コンポーネントでサーバーサイドfetchをしないといけません。
調べてみると、同じようなことを既にしている方がいらっしゃいました。

上のコンポーネントで fetch する Remix アプリの例は、同じ作者の以下のライブラリを使って作られています。

上のライブラリについて、作者の方が以下の記事で解説されています。

他にも、コンポーネントでサーバーサイドfetchする方法として、ルートファイルでコンポーネントを定義してそれをインポートして用いる手法をご紹介されている方もいらっしゃいました。

しかし、自分が試したところ、この方法で実装したコンポーネントをレンダリングする際に、loaderが無限ループするようでした。
useEffectなどで 1 回だけfetcher.loadするようにしても、そもそものサーバーサイド側の処理で無限ループしているようでした。
Cloudflare で動かそうとしているので@remix-run/cloudflareを使っているなど、考えられる原因はいくつかありますが、調査・特定できていません。

今回はnext-ssrを使ってリンクカードを実装しました。
リンクカードの実装も含めたこのブログの実装は以下のリポジトリで公開しています。

ここからは Remix で実装する上でちょっと苦労したところをご紹介します。

JSDOMが使えない

OGP の情報を取得するためには、HTML をパースして OGP の情報を取り出す必要があります。
javascript で HTML をパースするのには、jsdom がよく使われるもので、OGP をパースするのにも jsdom を使っているものが多いです。
しかし、jsdom は Node.js に依存しているため、Remix (Cloudflare Pages Functions) では動きませんでした。
Remix にはbrowserNodeBuiltinsPolyfillserverNodeBuiltinsPolyfillで Polyfill を使うことができますが、それでも jsdom を動かすことはできませんでした。

そこで、jsdom の代わりにhtmlparser2 とその周辺ライブラリを使いました。

jsdom は HTML のパース以外にも DOM 操作・jsdom 内でのスクリプトの実行などブラウザとほとんど同じことができるのですが、OGP の情報を取得するのにはパースと DOM のフィルタだけで十分でした。
htmlparser2 は速度が速いことをアピールしているライブラリで、Remix で動かす際に特に Polyfill も不要でした。

(追記) Remix の実装での問題点と悩み

今回実装した方法では、OGP の情報を取得するまでレスポンスがブロックされるので、ファーストビューの表示が遅くなってしまいます。
特に、リンクカードの多いこの記事は特に遅くなっているようで、ファーストビューの表示に 30 秒以上かかることもあり、大問題です。

この問題を解決するために、Remix のリソースルート機能を使って、OGP データを取得できるエンドポイントを用意し、クライアント側で取得することで、ファーストビューの表示を早くすることができます。
一方で、エンドポイントを公開してしまうことにもなるので、(そんな来ないでしょうが)負荷や大量にリクエストが来てしまう恐れを考えると、ちょっと躊躇してしまいます。

他のサイトを見てみると、例えば Zenn は埋め込み用の API を用意してそれを用いる実装のようです。
この API は非商用に限り利用できる(zenn-editor で指定できるのであって、自由に叩くのが良いのかは怪しそう)ようです。太っ腹。

https://github.com/zenn-dev/zenn-editor/tree/main/packages/zenn-markdown-html#zenndev-%E3%81%A8%E5%90%8C%E3%81%98%E5%9F%8B%E3%82%81%E8%BE%BC%E3%81%BF%E8%A6%81%E7%B4%A0%E3%82%92%E4%BD%BF%E7%94%A8%E3%81%99%E3%82%8B

今は一旦キャッシュで誤魔化していますが、どうにかしないといけないですね。

まとめ

OGP を使ったリンクカードの実装について書いてきました。
単なるリンクや、ビルド時に固定の文言を使うようなリンクカードではなく、リンク先の情報を使ってリッチなリンクカードにしようとするのはユーザー的には嬉しいですが、実装側としては考えさせられることが多いです。

Twitter や Notion など、リンクの情報をリンクカードとして表示するにするサービスが多く当たり前のように使われていますが、自分で実装しようとすると結構歯ごたえがあります。
自分が実装したのはユーザーからの入力という要件は入っていないので、他のサービスはもっと大変なんだろうなぁと思えました。