hamakou108 blog

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

Cover Image for 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 の埋め込みを頑張るよりも、記事に直接書かれたコードブロックを良い感じにレンダリングするコンポーネントを単に作った方が良いのでは?と後から思った。とは言え、多少は汎用性のありそうな記事が生まれたのでまあ良しとしたい。