publish.twitter.comのoEmbedがx.comドメインのURLに反応しない

公開日:
目次

twitterの埋め込みをpublish.twitter.comを利用して実装していたのですが、X に変わってからコピーしたURLを貼ったら埋め込みが表示されませんでした。今回はこれに対応したので備忘録を残します。

oEmbed に投げる前に x.com を twitter.com に書き換える

結論を先に書くと、リクエストURLを twitter.com ドメインに正規化してから oEmbed に渡せば動きます。

const normalized = url.replace('https://x.com/', 'https://twitter.com/')
const res = await fetch(`https://publish.twitter.com/oembed?url=${normalized}`)

これだけで、X からコピーした x.com/<user>/status/<id> 形式のURLも今までどおり埋め込めます。サーバー側の Markdown → HTML 変換でも、ユーザーが書いたURLをそのまま投げず、必ずこの置換を挟むようにしておくと、今後もドメインが変わっても対応できるようになります。

oEmbed のURL一致は厳密

publish.twitter.com/oembed[1] は、リクエストの url パラメータをホワイトリスト形式で照合しています。受理されないドメインのURLが来た場合は、エラーを返すのではなく、HTML キーが入らないJSON(実質空の応答)を返す挙動になっています。

X(Twitter)は2023年以降、ユーザーから見えるドメインを x.com に切り替えています。一方で publish 側のホワイトリストは長らく twitter.com 固定のままなので、x.com の URL は対応外です。エラーレスポンスではなく空応答を返してくる仕様のため、HTTPステータスだけ見て成功扱いしていると気付きにくいので注意が必要です。

正規化を埋め込みパイプラインのどこに置くか

oEmbed を呼ぶ処理が複数の入り口にある場合(記事本文の埋め込み、コメント欄の埋め込み、サイドバーの埋め込みなど)、それぞれで x.com 対応を入れていくと抜けが出ます。URLを抽出して oEmbed に投げる1関数の中で、必ず正規化してから fetch する形にしておくと安全です。

oEmbed を呼ぶ関数をひとつ用意して、その中で必ず正規化してから fetch するのがいいです。

embed.ts
export const getEmbedHtml = async (link: string): Promise<string> => {
  const match = /href="([^"]*)"/g.exec(link)
  if (!match) return ''
  const url = match[1].replace('https://x.com/', 'https://twitter.com/')
  const res = await fetch(`https://publish.twitter.com/oembed?url=${url}&align=center`)
  const json = await res.json()
  return json.html
}

入力側のURL抽出も x.com に対応させる

前提として、ほとんどの場合は投稿等の url を埋め込み用のURLに変換するはずです。その場合、投稿URLを文章等から抽出する必要があります。

前節の正規化は、URLを抽出できている前提の話です。その手前で、ユーザーが書いた Markdown から「埋め込み対象URL」を抽出する正規表現が twitter.com 固定のままだと、そもそも x.com のURLが拾えず、正規化どころか oEmbed まで届きません。抽出側の regex も (?:twitter|x)\.com のように両ドメインを受け付ける形にしておきます。

embed.ts
const twitterLinkRegex = /href="https:\/\/(?:twitter|x)\.com\/[^"']*\/status\/(\d+)"/g

ユーザー側はXから貼ったままで書いてよくて、システム側で抽出から正規化までを吸収する分担にしておくと、執筆フローと表示フローを切り離せます。

脚注
  1. oEmbed API - X Developer Platform (2026-05-26 アクセス) ↩︎