ブログにOGP展開機能をつける
はじめに
今回,このブログにURLを貼り付けるとOGP情報を取ってきてカード形式で表示する機能をつけました.
めしまし

↑こんなやつ
思った以上に大変だったので,最終的な実装方法とともに,試したことも書いていきたいと思います.
最終系
以下に示す図のような構成になりました.
投稿表示画面ではHTMLを持ったpost情報を返すようにし,投稿編集画面ではリアルタイムに変更をプレビューしたいので,サーバー側にmarkdownを受け取り,HTMLを返すだけのエンドポイントを増やしました.
サーバー(Go)側
GoでmarkdownからHTMLに変換するためにblackfridayというライブラリを用いています.
GitHub - russross/blackfriday: Blackfriday: a markdown processor for Go
実際のコードは ここで見れますが,解説用に多少簡略化したものを次に示します.
func (r *HTMLRenderer) RenderNode(w io.Writer, node *blackfriday.Node, entering bool) blackfriday.WalkStatus {
switch node.Type {
case blackfriday.Link:
if entering {
// Linkの情報からOGP情報を取得する
ogp, _ := opengraph.Fetch(string(node.LinkData.Destination))
imageURL := ""
if len(ogp.Image) > 0 {
imageURL = ogp.Image[0].URL
}
// card表示するためのHTMLを構築する
card := fmt.Sprintf(`</p><a href=%s class='web-card'><div class='web-card-main'><h2 class='web-card-title'> %s </h2><div class='web-card-description'> %s </div><div class='web-card-meta'><span class='web-card-link-hostname'> %s </span></div></div><div class="web-card-thumbnail"><img src="%s"/></div></a>`, node.LinkData.Destination, ogp.Title, ogp.Description, ogp.SiteName, imageURL)
w.Write([]byte(card))
// 子要素はskipする
return blackfriday.SkipChildren
}
}
// Link以外はデフォルトのまま処理する
return r.html.RenderNode(w, node, entering)
}
リンクからOGP情報を取得するために opengraph というライブラリを使っています.
GitHub - otiai10/opengraph: Open Graph Parser for Go
markdownからHTMLに変換するPOST /markdown-html でも,投稿を取得する GET /posts/[id] のエンドポイントでも上記コードを使って変換しています.
ちなみに,ブログなのでmarkdownを書くのは自分だけですが,念の為サニタイズ(無害化)はしておきましょう.
GitHub - microcosm-cc/bluemonday: bluemonday: a fast golang HTML sanitizer (inspired by the OWASP Java HTML Sanitizer) to scrub user generated content of XSS
を使っています.
OGP情報のキャッシュ
本番デプロイし終わってから気づいたのですが,OGP情報の取得がかなり重たいらしく,記事ページの表示に5秒ほどかかってしまう状態でした. 流石に遅すぎるので,1度取得したOGP情報はキャッシュするように変更しました.
現在はmemcachedやRedisなどは動かしておらず,このために動かすのも重厚すぎるので go-cache を使用しました.
GitHub - patrickmn/go-cache: An in-memory key:value store/cache (similar to Memcached) library for Go, suitable for single-machine applications.
これはインメモリなkey-valueストアで,期限なども設定できるので簡単に使い始められます.
変更後は以下のようにOGP情報を取得しています.
var ogp *opengraph.OpenGraph
var err error
if ogp, err = r.ogpCache.FindByURL(string(node.LinkData.Destination)); err != nil {
if errors.Is(err, entity.ErrOGPCacheNotFound) {
ogp, err = opengraph.Fetch(string(node.LinkData.Destination))
if err != nil {
// error logging
}
r.ogpCache.Set(ogp)
} else {
// error logging
}
}
フロント(Next.js)側
投稿表示画面では単に受け取った情報を表示しているだけなので割愛します.
投稿編集画面では以下のような画面で,左側の編集部に変更があるたびにmarkdownをHTMLに変換し,右側に表示しています.
エディタにはreact-split-mdeというライブラリを用いています.
GitHub - steelydylan/react-split-mde
以下のように自作したparserを渡すことができるので
<Editor
parser={parser}
value={markdown}
onChange={handleMarkdownChange}
previewClassName='prose'
/>
サーバー側でmarkdownからHTMLに変換した結果を流し込んでいます.
import axios from 'axios';
type HTMLRes = {
html: string;
}
const markdownToHTML =
(markdown: string): Promise<string> =>
axios.post<HTMLRes>(
`${process.env.NEXT_PUBLIC_HOST}/api/v1/markdown-html`,
{ markdown },
).then(res => res.data.html);
const parser = (value: string): Promise<string> => (
markdownToHTML(value)
)
export default parser;
あとはcssを良い感じに頑張ればカード型になります()
zenn.devのデザインを参考にさせていただきました🙇
Zenn|エンジニアのための情報共有コミュニティ

うまくいかなかったこと
ここからは試したことと,なぜダメだったのかを書いていきます.
SimpleMDE(marked)を拡張する
元々このブログでは SimpleMDEというmarkedを使ったエディタを用いていました.
JavaScript Markdown Editor - SimpleMDE
GitHub - markedjs/marked: A markdown parser and compiler. Built for speed.
そのため,markedのparserに手を加えようと思いましたが,markedはPromiseを受け入れてくれませんでした. 今回のOGP展開ではどうしても対象ページの情報を非同期で取得する必要があり,要件を満たせそうにありませんでした.
Promiseを受け入れるようにしようというissueはかなり前からあるのですが,リリースには至ってないようです.
async renderer support · Issue #458 · markedjs/marked
react-split-mde + zenn-markdown-htmlを使う.
こちらの記事にあるように実装すればZennと同じようにできるのでは?と思いました.
Zennのためにプレビューしながら記事を書けるマークダウンエディターを開発していた話

しかし,依存関係にある zenn-markdown-htmlはサーバーサイドでしか動かず,今回のような編集画面でどう扱えば良いのかわかりませんでした.
GitHub - zenn-dev/zenn-editor: Convert markdown to html in Zenn format
サーバーサイドはGoで書いているので,これらの採用は見送りました.
さいごに
割とポピュラーな機能なので,何かしらのライブラリがあるだろうと思っていたのですが,意外に苦労しました.
これを見ている誰かの参考になれば幸いです.
タグ
Loading...