GitHub Gist 埋め込み用の React コンポーネントを作った

前回の記事で GitHub Gist に挙げたスクリプトを記事中に埋め込んだ。通常は <script>
タグを埋め込むだけで実現できるが、 Next.js で実装されているこのブログでは一筋縄ではいかず、最終的に Gist を埋め込むための React コンポーネントを作成した。せっかくなので、その実装について紹介する。
環境
- React 19.0.8
- Tailwind CSS 4.0.4
- Next.js 15.1.6
- next-mdx-remote 5.0.0
React における Gist の埋め込みの問題
通常、 Gist のコードを特定の Web ページに埋め込む際は、 Gist のページ上に表示される埋め込み用の <script>
タグを利用する。
<script src="https://gist.github.com/username/gist_id.js?file=filename"></script>
自分のブログでは MDX で書いた記事を next-mdx-remote でレンダリングする方式を取っている。素朴な発想で MDX ドキュメントに上記の <script>
タグを埋め込むと次のようなエラーが発生する。
Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used
<script>
タグが展開されると SSR と CSR でレンダリング結果が一致しない状態になり、上記のようにハイドレーションのエラーが発生するようだ。通常の React コンポーネントに埋め込んだ場合にも同様のエラーが発生する。
この問題に対して、最初は useEffect
を使ってクライアントサイドで動的にスクリプトをロードする方法、 Next.js の Script コンポーネントで遅延ロードする方法などを試した。しかし、 Gist のスクリプトが document.write()
を使用して HTML を埋め込もうとする挙動に起因して、非同期ロードではうまく動作しなかった。
Gist 埋め込み用コンポーネントの作成
最終的に <iframe>
内部でスクリプトをロードすることでこの問題を回避できることが分かった。これを実現するための React コンポーネントの完成系は以下の通り。
Props に Gist の ID とファイル名を渡すことで、特定の Gist のファイルを表示できる。
前述の通り、内部的には <iframe>
内部でこれらの props の値を使った <script>
タグが実行されるようになっている。若干の工夫として、 <iframe>
の中身のコンテンツに対応する内部的なコンポーネントを作り、それを renderToStaticMarkup
で文字列展開したものを srcDoc
属性に渡す形にしている。
このコンポーネントを次のように呼び出せば、上記のような Gist のコードが文章中に埋め込まれる。
<GistEmbed gistId="hamakou108/4f9db207a828017a1ea3c932a33f3bc2" file="gist-embed.tsx" />
終わりに
よく考えると Gist の埋め込みを頑張るよりも、記事に直接書かれたコードブロックを良い感じにレンダリングするコンポーネントを単に作った方が良いのでは?と後から思った。とは言え、多少は汎用性のありそうな記事が生まれたのでまあ良しとしたい。