SPAでSNSカードが壊れる原因とJS実行問題

SPAでSNSシェアカードが壊れるのはなぜか

URLを貼ると、サムネイルが出ない。タイトルも説明文も空白。
ブラウザでは正しく表示されているのに、Twitter(X)やSlackでは崩れる。この現象は、フロントエンドのバグではなくSPAの仕組みそのものが原因である場合がほとんどです。

問題の核心は「SNSクローラがJavaScriptを実行しない」という点にあります。
そしてSPAは「JavaScriptを実行しないとページが存在しない」構造を持っています。

つまり、壊れているのはOGPではなく、クローラから見たページの存在です。

SNSクローラはブラウザではない

まず前提として、SNSはブラウザを使ってページを表示していません。
URLが投稿されると、SNSは独自のクローラ(bot)を使ってページを取得します。

クローラが行う処理は非常に単純です。

処理 内容
接続 HTTPリクエストを送る
取得 HTMLを受信
解析 headタグのmetaを読む
生成 カード情報を作る

ここに「JavaScriptの実行」は含まれていません。
安全性と速度のため、外部サイトのスクリプトを実行しない設計になっています。

SPAの初期HTMLの中身

SPAの初回レスポンスを実際に見ると、次のような内容になっています。

<!DOCTYPE html>
<html>
<head>
  <title>My App</title>
</head>
<body>
  <div id="root"></div>
  <script src="/main.js"></script>
</body>
</html>

ここには記事も画像も説明文もありません。
アプリケーションの本体は「main.js」にあります。

ブラウザは以下の流れでページを表示します。

  • HTMLを受信
  • JavaScriptをダウンロード
  • JavaScriptを実行
  • APIからデータ取得
  • DOM生成
  • メタタグ書き換え

しかしクローラは、JavaScriptを実行しないため、この後半の処理が一切行われません。

結果としてクローラが認識するページは「空のdivだけのページ」になります。

メタタグ書き換え問題

多くのSPAでは、ページ表示後にタイトルやOGPを設定します。

document.title = article.title

const meta = document.createElement('meta')
meta.setAttribute('property', 'og:title')
meta.setAttribute('content', article.title)
document.head.appendChild(meta)

ブラウザでは問題ありません。
しかしクローラの時系列は次の通りです。

1 HTML受信
2 head解析
3 OGP確定

この時点ではJavaScriptは実行されていません。
つまり、後から追加されたmetaタグは存在しない扱いになります。

これが「SPAでSNSカードが壊れる原因」です。

GoogleとSNSの違い

ここで混乱が起きやすいのが「GoogleはJSを実行する」という話です。
確かにGooglebotはJavaScriptレンダリングを行います。

しかし重要なのは以下です。

対象 JavaScript実行
Google検索 実行する
SNSクローラ ほぼ実行しない

SEOが問題ないのにOGPが壊れるのは、この差によるものです。

なぜSNSはJSを実行しないのか

理由は2つあります。

  • セキュリティリスク
  • クロールコスト

もしSNSがすべてのページのJavaScriptを実行した場合、悪意あるコードの影響を受けます。また、表示速度も著しく低下します。
そのため「静的HTMLのみ解析」という設計が採用されています。

実務でよくある誤解

特に多いのが次の対応です。

  • helmetを入れた
  • titleを変更した
  • React HelmetでOGPを書いた

それでも表示されません。
理由はシンプルで、Helmetは「ブラウザで実行された後」にheadを書き換える仕組みだからです。クローラには届きません。

解決策

解決方法は大きく3つあります。

1 SSR(Server Side Rendering)

サーバ側でHTMLを生成し、最初からmetaタグを含めます。
最も確実な方法です。

2 SSG(静的生成)

ビルド時にHTMLを生成しておきます。
ブログや記事サイトに向いています。

3 Prerender

クローラアクセス時のみレンダリング済みHTMLを返します。
既存SPAの応急処置として使われることがあります。

if ($http_user_agent ~* (facebookexternalhit|Twitterbot)) {
    proxy_pass http://prerender;
}

ただし、これは運用が複雑になりがちです。

ありがちな落とし穴

API依存ページ

SSRを導入しても、記事データをクライアントで取得していると意味がありません。
「HTML生成時点」でデータが存在している必要があります。

キャッシュ問題

SNSはOGPをキャッシュします。
修正後も変わらない場合、キャッシュの可能性が高いです。

https://cards-dev.twitter.com/validator
https://developers.facebook.com/tools/debug/

ここから再取得を行う必要があります。

どのサイトが影響を受けるか

特に影響が大きいのは以下です。

  • ブログ記事
  • ニュース
  • EC商品ページ
  • 採用ページ
  • LP

URL共有が前提のサイトでは、OGPはクリック率に直結します。
一方、社内システムや管理画面ではほぼ問題になりません。

注意点

SPAをやめる必要はありません。
問題なのは「公開ページまでSPAにしてしまう設計」です。

アプリケーション領域はSPA、公開ページはSSRという分離が現実的です。
ここを誤ると、リリース後にOGP問題が発覚し、構成を作り直すことになります。

まとめ

SNSシェアカードが壊れるのは、フレームワークの出来不出来ではありません。
SPAは「実行して完成するページ」、SNSクローラは「受信した瞬間のHTML」を見ています。

つまり、両者が見ているものが違います。
公開ページを作るときは、ユーザーのブラウザだけでなく、最初のHTTPレスポンスを読む存在を意識すると、設計の失敗を避けやすくなります。