🦁

Docusaurus/ReactでTwitterをいい感じに表示するコンポーネントを最短で書く

2025/01/22に公開

Docusaurus/ReactでTwitterをいい感じに表示するコンポーネントを最短で書く

  • description: Docusaurus の Markdown で Twitter を上手に取り込む React コンポーネントについての解説です。

Docusaurus3.7をさわってます。久しぶりにReactをやると、今どきはどんな書き方が正しいのかわからないのでメモしておきます。

Markdownからの静的サイトをつくるうえで、X(Twitter)埋め込みがめんどいことがあります。理想をいえば、TwitterやXのURLを書いただけで "publish.twitter.com"で取得できる blockquoteと https://platform.twitter.com/widgets.js  などを含むコードに展開したいと思います、よね?

例えばこのツイートを埋め込みたいとします

https://x.com/o_ob/status/1601011352977305600

こんな感じにプレビューできるので Copy Codeします

こういうコードが生成されます

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">852話さんが作った 8528-diffusion-v0.2をGoogle Colab上で起動したWebUIで使用するコードです。<a href="https://t.co/slD5EtZwIZ">https://t.co/slD5EtZwIZ</a><br><br>Running on public URL: <a href="https://t.co/59uk61Jg19">https://t.co/59uk61Jg19</a><br>と出てくるURLをブラウザの <a href="https://t.co/kbnG05BECF">https://t.co/kbnG05BECF</a> のURLを別ウインドウで開くと、AUTOMATIC1111が動きます!<a href="https://twitter.com/hashtag/8528d?src=hash&amp;ref_src=twsrc%5Etfw">#8528d</a> <a href="https://t.co/OjfslEiYpr">pic.twitter.com/OjfslEiYpr</a></p>&mdash; Dr.(Shirai)Hakase - しらいはかせ (@o_ob) <a href="https://twitter.com/o_ob/status/1601011352977305600?ref_src=twsrc%5Etfw">December 9, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

これを noteなりqiitaなりhtmlなりmdなりで使えば良いのですが、大きく分けると

  • Blockquote 本来は引用だが "twitter-tweet"クラスで囲ってる
  • p ツイートの中身テキスト(おそらく最初の140文字を意識)
  • <a>とか<br>とか
  • 親ツイートへのリンクとかハッシュタグとか
  • ツイートしたしたユーザーのディスプレイ名、@名
  • そのツイートへのURL(twitter.com)
  • 日付(英語、長い形式)
  • script widget.js へのリンクという構成になっています。

とても長くて、かつTwitter.comやt.coを参照していて、さらに日付とかユーザー名とかもあって「これはもうサーバー側で動的取得せねばなのでは?」という感じがします。

さらにこれはReactだと色んな実現方法があると思います。ちなみに <br> は <br/> としないとMDX環境ではエラーになります。

で、さっきまでLLM (Gemini 2.0 Flash Thinking Experimental 01-21)とTwitter API v2を使う方法を試していたんだけど、cURLでスルッと通るような認証がnodeにすると401になったり、そもそもTwitterのAPI費がとても高くなってしまったので、やめました。

お客さんがアクセスするたびにTwitter APIを消費するような仕組みもアホらしい、docusaurus buildするときに参照するようにしたい、でもそのビルドのときにおかしいと気づいたら、もう直しておいてくれて欲しい。

そもそも僕は知っている、Twitter埋め込みコードで必要となる要素はとても少ない。

上記の要素を全部埋めるのは無理なんですが、そもそも https://x.com/o_ob/status/1601011352977305600 というURLにユーザー名とStatusが含まれています。そこからいろんなものを解決するのがwidget.jsの仕事なので、全部クライアントサイドのレンダリングにしてしまって良い気がしました。

方法としては、まずはこんな感じのReactコンポーネントに処理をさせるのが初手だと思ったので作っていきます。

// docs/src/components/Twitter.js
import React from 'react';

export default function Twitter({ children, url }) {
  // URLから username と status を抽出 (twitter.com と x.com 両対応)
  const usernameMatch = url.match(/(twitter|x)\.com\/([^\/]+)/);
  const statusMatch = url.match(/status\/(\d+)/);
  const username = usernameMatch ? `@${usernameMatch[2]}` : 'Unknown User'; // usernameMatch[2] に変更
  const status = statusMatch ? statusMatch[1] : '';

  // 現在の日付を取得
  const datetime = new Date().toLocaleDateString();

  return (
    <div>
      {children && <p>{children}</p>} {/* children がある場合は、ツイートの上に表示 */}
      <blockquote className="twitter-tweet">
        <p lang="ja" dir="ltr">
          {/* children の有無に関わらず、常にこのプレースホルダーテキストを表示 */}
          Loading X.com tweet...
        </p>
        — <a href={`https://twitter.com/${username.replace('@', '')}?ref_src=twsrc%5Etfw`}>{username}</a> <a href={`https://twitter.com/${username.replace('@', '')}/status/${status}?ref_src=twsrc%5Etfw`}>{datetime}</a>
      </blockquote>
      <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
    </div>
  );
}

https://gist.github.com/kaitas/d71ca0980417f897caadc4cb9ae31f2e

要は正規表現で、x.comとかtwitte.comといったURLがきたら、そこからユーザーネームを拾って、/status/~/をみて、正規表現のmatchでそのステータスIDを取得して…なければないでまあ空白にしておいて、日付は今日の日付でも取っておいて、それをフォーマットに沿って流し込んでいきますと、うまくいきます。

「うまくいきます」とするっと書いてますが、実際には後半の Aタグでtwitter.com をロードしないと正しく動作しないとか、中盤のPタグでプレースホルダしないとうまく動作しないとか、けっこう色々ノウハウが有ることを思い出しましたので、そのへんも味わっていただけると幸いです。

widget.jsはここで読み込む必要はないです、ページの最後にまとめてロードする、という方法もありますが、UX的に速くて、早くて、安定しているのは、同じdivの中にいれることだと思います。

これでMarkdown中では、<Twitter url="https://x.com/o_ob/status/1601011352977305600"/>とセルフ閉じタグで書くか、<Twitter url="https://x.com/o_ob/status/1601011352977305600"> このへんにつぶやきの内容が入る</Twitter>とすれば children として渡すことができます。実際にはローディングが終わるまでの一瞬読まれるだけテキストなので「Loading X.com tweet...」をデフォルトとしておきました。引用元のURLも x.com だけでいいかと思ったけど、時々 twitter.com が混ざってくるのでどちらも動くようにしておくと良さそうです。

Docusaurus全体で使いたい。

こうなるとDocusaurus全体で使いたいですよね。自分が使っている最新のDocusaurus3.7だけかもしれないですが、こんな感じに全体で使えるコンポーネントをつくることができます。docs\src\theme\MDXComponents.jsにMDXで使いたいコンポーネントを登録して行きます。

import React from 'react';
// Import the original mapper
import MDXComponents from '@theme-original/MDXComponents';
import Highlight from '@site/src/components/Highlight';
import Twitter from '@site/src/components/Twitter';

export default {
  // Re-use the default mapping
  ...MDXComponents,
  // Map the "<Highlight>" tag to our Highlight component
  // `Highlight` will receive all props that were passed to `<Highlight>` in MDX
  Highlight,
  Twitter,
};

参考資料・補足

実際にはフロントマター(markdownの冒頭にあるYAML形式の拡張情報のこと)もparseFrontMatter機能を使用して、独自のフロントマター パーサーを提供したり、デフォルトのパーサーを拡張したりできます。

Docusaurusのデフォルトのパーサーを再利用して、独自のカスタム専用ロジックでラップすることもできます。便利なフロントマター変換やショートカットを実装したり、Docusaurus プラグインがサポートしていないフロントマターを使用して外部システムと統合したりすることが可能になります。

export default {
  markdown: {
    parseFrontMatter: async (params) => {
      // Reuse the default parser
      const result = await params.defaultParseFrontMatter(params);

      // Process front matter description placeholders
      result.frontMatter.description =
        result.frontMatter.description?.replaceAll('{{MY_VAR}}', 'MY_VALUE');

      // Create your own front matter shortcut
      if (result.frontMatter.i_do_not_want_docs_pagination) {
        result.frontMatter.pagination_prev = null;
        result.frontMatter.pagination_next = null;
      }

      // Rename an unsupported front matter coming from another system
      if (result.frontMatter.cms_seo_summary) {
        result.frontMatter.description = result.frontMatter.cms_seo_summary;
        delete result.frontMatter.cms_seo_summary;
      }

      return result;
    },
  },
};

Docusaurus 3.7 を使ってる人がどれぐらいいるかわかりませんのと、それぐらいの人であれば Reactでツルッと作ってしまえそうなのですが、LLMで逆に混乱することもあったので書き起こしてみました。

Discussion