mesimasi Logo

ブログにOGP展開機能をつける

ブログにOGP展開機能をつける_thumbnail

はじめに

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

めしまし

サーバーサイドエンジニアの雑記
めしまし

↑こんなやつ

思った以上に大変だったので,最終的な実装方法とともに,試したことも書いていきたいと思います.

最終系

以下に示す図のような構成になりました.

最終系

投稿表示画面ではHTMLを持ったpost情報を返すようにし,投稿編集画面ではリアルタイムに変更をプレビューしたいので,サーバー側にmarkdownを受け取り,HTMLを返すだけのエンドポイントを増やしました.

サーバー(Go)側

GoでmarkdownからHTMLに変換するためにblackfridayというライブラリを用いています.

GitHub - russross/blackfriday: Blackfriday: a markdown processor for Go

Blackfriday: a markdown processor for Go. Contribute to russross/blackfriday development by creating an account on GitHub.
GitHub

実際のコードは ここで見れますが,解説用に多少簡略化したものを次に示します.

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

Open Graph Parser for Go. Contribute to otiai10/opengraph development by creating an account on GitHub.
GitHub

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

bluemonday: a fast golang HTML sanitizer (inspired by the OWASP Java HTML Sanitizer) to scrub user generated content of XSS - GitHub - microcosm-cc/bluemonday: bluemonday: a fast golang HTML saniti...
GitHub

を使っています.

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.

An in-memory key:value store/cache (similar to Memcached) library for Go, suitable for single-machine applications. - GitHub - patrickmn/go-cache: An in-memory key:value store/cache (similar to Mem...
GitHub

これはインメモリな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

Contribute to steelydylan/react-split-mde development by creating an account on GitHub.
GitHub

以下のように自作した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|エンジニアのための情報共有コミュニティ

Zennはエンジニアが技術・開発についての知見をシェアする場所です。本の販売や、読者からのバッジの受付により対価を受け取ることができます。
Zenn

うまくいかなかったこと

ここからは試したことと,なぜダメだったのかを書いていきます.

SimpleMDE(marked)を拡張する

元々このブログでは SimpleMDEというmarkedを使ったエディタを用いていました.

JavaScript Markdown Editor - SimpleMDE

GitHub - markedjs/marked: A markdown parser and compiler. Built for speed.

A markdown parser and compiler. Built for speed. Contribute to markedjs/marked development by creating an account on GitHub.
GitHub

そのため,markedのparserに手を加えようと思いましたが,markedはPromiseを受け入れてくれませんでした. 今回のOGP展開ではどうしても対象ページの情報を非同期で取得する必要があり,要件を満たせそうにありませんでした.

Promiseを受け入れるようにしようというissueはかなり前からあるのですが,リリースには至ってないようです.

async renderer support · Issue #458 · markedjs/marked

It is helpful if one needs to use http to retrieve rich data for rendering.
GitHub

react-split-mde + zenn-markdown-htmlを使う.

こちらの記事にあるように実装すればZennと同じようにできるのでは?と思いました.

Zennのためにプレビューしながら記事を書けるマークダウンエディターを開発していた話

Zenn

しかし,依存関係にある zenn-markdown-htmlはサーバーサイドでしか動かず,今回のような編集画面でどう扱えば良いのかわかりませんでした.

GitHub - zenn-dev/zenn-editor: Convert markdown to html in Zenn format

Convert markdown to html in Zenn format. Contribute to zenn-dev/zenn-editor development by creating an account on GitHub.
GitHub

サーバーサイドはGoで書いているので,これらの採用は見送りました.

さいごに

割とポピュラーな機能なので,何かしらのライブラリがあるだろうと思っていたのですが,意外に苦労しました.

これを見ている誰かの参考になれば幸いです.

タグ

Loading...

利用規約

copyright ©めしまし All Rights Reserved.