はじめに:完璧に動くサイトなのにOGPが出ない?
Webサイト開発の現場において、最も恐ろしいバグの一つ。それは「ブラウザ上では完璧に動いているのに、裏側で致命的な問題が起きている」バグです。
今回は、私がNext.jsの案件で実際に遭遇した、「ローディング画面の実装ミスにより、SEOが全滅しかけた話」を共有します。
もしあなたが、「Next.jsでサイトを作ったのにOGPが反映されない」「Google検索結果にファビコンが出ない」といった症状に悩まされているなら、この記事が解決の糸口になるはずです。
事件の発生:ステータスコード200でも「認識されない」
サイトの構築が完了し、本番環境へデプロイ。ブラウザで動作確認を行いました。 アニメーションも滑らか、ページ遷移も爆速。「完璧だ」と思いました。
しかし、SNSでシェアしようとした瞬間、異変に気づきます。
- Twitter(X)やFacebookでURLを貼っても、OGP画像が表示されない。
- Googleの検索結果に、いつまで経ってもファビコンやディスクリプションが反映されない。
FacebookのデバッガーツールでURLを叩いてみると、HTTPステータスコードは 200 OK を返しています。サーバーは生きている。しかし、タイトルや画像情報は「取得できませんでした」となるのです。
迷走したデバッグ:認証やキャッシュを疑う日々
最初は、Next.jsやVercelの設定周りを疑いました。
- 「VercelのAuthentication(Basic認証など)が誤ってONになっている?」→ 確認したがOFF。
- 「CSP(Content Security Policy)ヘッダーが厳しすぎてクローラーを弾いている?」→ 無効化してみたが変わらず。
- 「ボットのキャッシュが残っているだけ?」→ URLパラメータを変えてアクセスしても状況は変わらず。
設定ファイルを見直しても、怪しい箇所は見当たりません。完全に迷宮入りしかけていました。
決定的な証拠:curlコマンドが見た「虚無」の世界
「ブラウザで見えているものが、なぜクローラーには見えないのか?」
原点に立ち返り、「ボットの視点」でサイトを見てみることにしました。ここで役立ったのが、ブラウザの検証ツールではなく、ターミナルで実行する curl コマンドです。
以下のコマンドで、Facebookのクローラー(facebookexternalhit)を偽装してアクセスしてみました。
curl -A "facebookexternalhit/1.1" [<https://www.tosakafunk.com>](<https://www.tosakafunk.com>)
返ってきたHTMLを見て、私は愕然としました。
<html>
<head>
</head>
<body>
<div id="__next">Loading...</div>
</body>
</html>中身がスカスカだったのです。<head> 内のタイトルもOGPタグも存在せず、ボディには「Loading...」の文字だけ。これでは、クローラーがサイトの内容を理解できるはずがありません。
ブラウザの検証ツールだけでは気づけない罠
普段私たちは、Chromeなどの「検証ツール(DevTools)」でHTMLを確認しがちです。しかし、最近のブラウザは賢いため、JavaScriptを実行した「後」のDOMを表示します。
対して、多くのクローラー(特にOGP取得ボット)は、サーバーから返された最初のHTML(初期レスポンス)を見て判断します。ここに致命的な乖離があったのです。
真犯人:SSR時の条件付きレンダリング
原因は、アプリケーションのルートとなる _app.tsx での条件付きレンダリング(Conditional Rendering)の書き方にありました。
認証状態などをチェックする際、ユーザー体験を良くしようとして「チェック中はローディング画面だけを見せる」という実装をしていたのです。
❌ 犯人のコード
// _app.tsx(SEOを殺していたコード)
function MyApp({ Component, pageProps }) {
const { isChecking } = useAuth(); // 認証チェックフック
// ⚠️ ここが問題!
// チェック中はPreloaderだけを返し、Component(本来のコンテンツ)を描画していない
if (isChecking) {
return <Preloader />;
}
return <Component {...pageProps} />;
}なぜこれがダメなのか?
useAuthやuseEffectなどのフックは、クライアントサイド(ブラウザ)でのみ実行されます。- Next.jsのSSR(サーバーサイドレンダリング)の時点では、
isCheckingは初期値(例えばtrue)のまま処理が進みます。 - サーバーは
if (true)に従い、<Preloader />だけが含まれたHTMLを生成してレスポンスを返します。 - クローラーはその「PreloaderだけのHTML」を受け取り、「中身のないページだ」と判断して帰っていきます。
人間が見るブラウザ上では、あとからJSが動いてコンテンツに切り替わるため気づきませんが、ボットにとっては「永久にローディング中のページ」だったのです。
解決策:「切り替え」ではなく「重ね合わせ」る
解決策は単純でした。 「読み込み中かどうかにかかわらず、常にコンテンツ(Component)を描画しておく」ことです。その上で、ローディング画面をCSSで上から被せる(オーバーレイ)実装に変更しました。
⭕️ 正しいコード
// _app.tsx(SEOに優しいコード)
function MyApp({ Component, pageProps }) {
const { isChecking } = useAuth();
return (
<>
{/* Preloaderは条件付きで表示するが、
メインコンテンツとは並列(または前面)に配置する
*/}
{isChecking && <Preloader />}
{/* ⚠️ 重要:
チェック中であっても、裏側で常にComponentを描画出力しておく!
これでSSR時にもHTMLの中にコンテンツが含まれるようになる。
*/}
<Component {...pageProps} />
</>
);
}
CSSでの制御(Preloader)
Preloader コンポーネントは position: fixed; z-index: 9999; などで画面全体を覆うようにスタイリングします。これで、見た目上の挙動(ローディング中はコンテンツが見えない)は維持しつつ、HTML構造上はコンテンツが存在する状態を作れます。
この修正をデプロイした直後、curl コマンドで確認すると、しっかりと <title> や <meta property="og:image"> が出力されるようになり、SNSでの表示も無事に復活しました。
まとめ:Reactの感覚でSSRを扱うとSEOで死ぬ
SPA(シングルページアプリケーション)やReactだけの開発に慣れていると、「条件によって return を分ける」という書き方を頻繁に行います。しかし、Next.jsのようなSSRを伴うフレームワークでは、「サーバーが返す初期HTMLに何が含まれているか」を常に意識しなければなりません。
今回の教訓:
- 条件付きレンダリングには要注意: SSR時にコンテンツが除外されないか確認する。
- curlコマンドは友達: SEO系のトラブルシューティングでは、ブラウザではなく
curlで生のHTMLを確認する癖をつける。
「なぜかOGPが出ない」と悩んでいる方は、ぜひ一度ターミナルから自分のサイトにアクセスしてみてください。意外な「虚無」が見つかるかもしれません。

