Docusaurus v2 では,Twitter card のための twitter:image
タグサポートこそあるものの,ソーシャルメディアとの連携はほとんど実装されていない.
まぁ,これを作っているチームが Facebook もとい Meta 社の人々なので,競合他社を積極的に利するだけの行為はあまり積極的ではないのかもしれない.が,どうにか Docusaurus の組み込み API をフル活用して,ツイートの埋め込みに成功したのでここに記録を残す.
スマイルプリキュア
— 橋下徹 (@hashimoto_lo) 2013年6月1日
It’s a new day in America.
— Joe Biden (@JoeBiden) 2021年1月20日
仕組み(三行まとめ)
- 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.js
の beforeDefaultRemarkPlugins
に必要な情報を入れてやれば良い.
beforeDefaultRemarkPlugins
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
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
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()
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 に改宗してほしい……
- こちらに詳細が書いてある → Set up Twitter for Websites | Docs | Twitter Developer Platform↩
- zenn-ediotr のソースを見るとわかる.ゆくゆくは zenn-embed-elements と同様の機能を目指したいものである↩