Skip to main content

ツイートの埋め込みに対応した

Kiai

幸せを運ぶといわれる、青い鳥.Twitter(ツイッター)のアイコンにも使われている

Docusaurus v2 では,Twitter card のための twitter:image タグサポートこそあるものの,ソーシャルメディアとの連携はほとんど実装されていない.

まぁ,これを作っているチームが Facebook もとい Meta 社の人々なので,競合他社を積極的に利するだけの行為はあまり積極的ではないのかもしれない.が,どうにか Docusaurus の組み込み API をフル活用して,ツイートの埋め込みに成功したのでここに記録を残す.

仕組み(三行まとめ)

  • remark plugin で URL のみが含まれる一行blockquote に変換
  • 秘伝のタレを読み込む処理1<head> に挿入しておく
  • src/theme/Root.tsx を準備して,ページが変わるたびフックとして Widget 関数実行

詳細

remark-embedder プラグインで URL を blockquote へ変換

@remark-embedder/core@remark-embedder/transformer-oembed を活用して,Markdown 内部の行内に「URL が一つだけ」「前後に空白やマークアップなく」存在しているとき,それを埋め込みに対応した要素に変換している.これはサーバサイドで処理されるので,SSG として表示されるときには単なる blockquote のタグが表示されるだけとなる(クライアント側の負担にはならない).

具体的には,docusaurus.config.jsbeforeDefaultRemarkPlugins に必要な情報を入れてやれば良い.

beforeDefaultRemarkPlugins
docusaurus.config.js
beforeDefaultRemarkPlugins: [
[
require('@remark-embedder/core'),
{
transformers: [
[
require('@remark-embedder/transformer-oembed'),
// cf. https://developer.twitter.com/en/docs/twitter-api/v1/tweets/post-and-engage/api-reference/get-statuses-oembed
{ params: { maxwidth: 550, omit_script: true, align: 'center', lang: 'ja', dnt: true } }
]
],
// cf. https://github.com/remark-embedder/core#handleerror-errorinfo-errorinfo--gottenhtml--promisegottenhtml
// handleError: handleEmbedError
}
]
]

injectHtmlTags()<script> を仕込む

次に,injectHtmlTags を実行する.これは <head><body> の前の方 / 後ろの方にタグを挿入するためのプラグイン実装だ.<Head> という組み込みコンポネントも一応存在しているが,そちらは毎回クライアント側で DOM を置き換える処理が発生しているし,ここに適用する必要があり(いちいち Swizzle してカスタマイズせねばならない),手間の割にリターンが僅少なので採用しなかった.

injectHtmlTags.ts
src/components/Root.tsx
import type { LoadContext } from '@docusaurus/types'
import type { PluginOptions } from '@docusaurus/plugin-content-pages'

// Set up Twitter for Websites | Docs | Twitter Developer Platform
// cf. https://developer.twitter.com/en/docs/twitter-for-websites/javascript-api/guides/set-up-twitter-for-websites
const twttr = `
<script>
window.twttr = (function(d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0],
t = window.twttr || {};
if (d.getElementById(id)) return t;
js = d.createElement(s);
js.id = id;
js.src = "https://platform.twitter.com/widgets.js";
fjs.parentNode.insertBefore(js, fjs);

t._e = [];
t.ready = function(f) {
t._e.push(f);
};

return t;
}(document, "script", "twitter-wjs"));
</script>
`
.split('\n')
.map((line) => line.trim())
.join('')

// options は `docusaurus.config.js` にてオプション引数として指定する
const plugin = async (context: LoadContext, options: PluginOptions) => {
return {
name: 'docusaurus-plugin-inject-html-tags',
injectHtmlTags() {
return {
headTags: [ twttr ]
}
}
}
}

export default plugin

(※ docusaurus.config.js 内に injectHtmlTags() を定義することも可能だが,汚くなるし量が膨大になったり型によるサポートが受けられなかったりするため,別途 .ts ファイルとして作り,ビルドのたびに swc でトランスパイルさせることとした)

inject という字面から分かる通り,dangerouslySetInnerHTML 経由で生文字列のスクリプトを注入していることにだけ気をつけたい.

これでサーバサイド,もとい SSG までの準備は整ったが,このままでは単に <blockquote> タグ(+とツイート内容)が表示されるだけである. これをよりリッチに表示してくれるのが widget.js だ.上述した <script> において window に紐付いた twttr オブジェクトがグローバルに存在しているため,各ページに遷移するごとに widget.load() してやれば,あとはスクリプトが <blockquote> を探して書き直してくれる.

<Root /> コンポネントの作成

Root.tsx
src/components/Root.tsx
import React from 'react'
import { useTweetEmbed } from '@site/src/libs'

// Default implementation, that you can customize
// @see cf. https://docusaurus.io/docs/advanced/swizzling#wrapper-your-site-with-root
const Root = ({ children }: { children: React.ChildNode }): JSX.Element => {
// 埋め込みツイートをアクティベートする(単なる blockquote が tweet widget になる)
useTweetEmbed()
return <>{children}</>
}

export default Root

本来であれば,/docs, /blogs 両方の基幹ページコンポネントを Swizzle してきてゴニョゴニョするしかなかったところ,よく調べると <Root /> というコンポネントを自分で作成して使うことができるらしい(CLI で docusaurus swizzle するのではなく,単にファイルを作成して設置するだけ).

それさえわかれば後はかんたんで,ページ遷移するたびに widget.load() し直すだけですべてのページで埋め込み変換をやってくれるようになった.ページ遷移状態は useLocation() フックで取得し,useEffect() フックの第二引数に pathname を仕込むことで実現している.

useTweetEmbed()
src/libs/twitter.ts
import { useEffect } from 'react'
import { useLocation } from '@docusaurus/router'
import { default as useIsBrowser } from '@docusaurus/useIsBrowser'


declare global {
interface Window {
twttr: any
}
}

// 非同期関数を定義
const loadWidgetAsync = () =>
new Promise<void>((resolve, reject) => {
if (!window.twttr) {
const msg = 'Failure to load window.twttr, aborting load'
console.error(msg)
reject(msg)
} else {
try {
// <script> タグ内で定義した window.twttr.widgets をロードする
window.twttr.widgets.load(document.getElementsByTagName('article'))
resolve()
} catch (e) {
reject(e)
}
}
})

// ページ遷移してから少しの間ディレイを入れる
// ページ内の他のコンテンツの読み込みを優先させるため
const delay = 2000

export const useTweetEmbed = () => {
const isBrowser = useIsBrowser()
const { pathname } = useLocation()

// Debounced function
useEffect(() => {
const fn = async () => await loadWidgetAsync()
const timer = setTimeout(() => {
if (!isBrowser) return
void fn() // ここまで到達して初めて非同期関数を実行
}, delay)

return () => clearTimeout(timer)
}, [pathname])
}

終わりに

Markdown を変換してわちゃわちゃするのに remark 以外,もとい unified アーキテクチャ以外を採用するのはありえないという結論に至るくらい,remark 周りのあれこれは可能性があると感じている.本アイデアの着想元である zenn.dev でも早く markdown-it なんて投げ捨てて2 remark / rehype に改宗してほしい……


  1. こちらに詳細が書いてある  →  Set up Twitter for Websites | Docs | Twitter Developer Platform
  2. zenn-ediotr のソースを見るとわかる.ゆくゆくは zenn-embed-elements と同様の機能を目指したいものである