SPA/動的生成記事でのOGP付加の簡易解決
某所の仕事で、後から編集しやすいようにとのことで、ReactやVueを使わずに、HTMLテンプレートにJSで動的にコンテンツを流し込むサイトを作成した。
サーバ環境はPHP実行可能なレンタルサーバー。Nodeは使えず、CI/CDを構築できるような環境でもないので、SSRもSSGも選択肢に入らない。ただしPHPは使える。
サイトを作ること自体は特段難しいことはないが、問題が、
「これ、記事ページのOGP入らないな」
ということ。
動的生成をOGPクローラーは待ってくれない。サイト固有のOGPを入れておいたらそれしか出ない。
じゃどうするか、という思考と解決策をメモっておく。
🤔 問題:動的生成とOGPの相性
OGPクローラー(TwitterBot、Facebook、LINE、Slackなど)は、基本的にJavaScriptを実行しない。ページのHTMLを取得して、<head>内のmetaタグを読み取るだけ。
つまり、JSで動的に生成されるコンテンツのタイトルや説明文は、OGPには反映されない。
結果として、どの記事をシェアしても「サイト共通のOGP」しか表示されないという問題が発生する。
❌ 検討して却下した方法
1. .htaccessでクローラーUA判定して振り分け
最初に思いついたのがこれ。クローラーのUser-Agentを判定して、クローラー専用のページに振り分ける方法。
RewriteCond %{HTTP_USER_AGENT} (Twitterbot|facebookexternalhit|Slackbot) [NC]
RewriteRule ^articles/(.*)$ /ogp-pages/$1.html [L]
却下理由:クローラーリストの管理が現実的でない
クローラーって世の中に無限大にある。主要なものだけでも相当な数で、新しいサービスが出るたびに追加が必要になる。
一応、クローラーリストを公開しているところはある:
- monperrus/crawler-user-agents - GitHub管理のJSON形式
- HUMAN Security - Crawlers List - セキュリティ企業のボット解説
- UserAgentString.com - シンプルな一覧
- Foundation Web Dev - AI Crawlers - AI系クローラー特化
国内のリストだと少なすぎるので海外ソースを参照することになるが、これをメンテし続けるのは現実的ではない。
2. History APIと履歴でOGP差し込み
考えた流れ:
.htaccessで初回アクセス時に?init=1のようなクエリを付与- 初回アクセスを全てPHPに流してOGP付きページを返す
- リダイレクトで記事ページに飛ばす
- History APIで履歴を消すことで「戻る」でOGPページに戻らないようにする
- ついでにURLからクエリも消す
却下理由:管理が煩雑すぎる
初回判定、リダイレクト、履歴操作、クエリ除去…と処理が多段階になり、どこかでバグったときのデバッグが地獄になる。そもそもここまでやるなら別のアプローチを考えた方が良い。
3. HTMLテンプレートにPHP埋め込み
テンプレートの.htmlファイル内にPHPを埋め込んで、URL判定でOGPだけPHPから出力する方法。
却下理由:納品時に説明できない
.html拡張子でPHP実行するサーバー設定が必要- 「HTMLにPHP入ってるの?」と納品先に言われると面倒
- そもそも生のHTMLにJSで建付けしているのは「素のHTMLとして編集できるように」という要件のため
- 将来「やっぱりVueにします」となったときに使えない
✅ 採用:全アクセスをPHPに通してOGP注入
結論として採用したのは、動的生成記事へのアクセスを全てOGP処理用PHPに投げるという方法。
最初は「PHPを噛ますしかない」とは思っていたものの、なぜかOGPだけ先に出してリダイレクトする方向で考えていた。
HTMLに納品先が手を入れる前提で考えていたので、「HTMLを加工する」という発想に至らなかった。
でも冷静に考えれば、HTML出力前にいじっちゃえばいい。この方法なら<head>内が余程怪しいことにならない限り問題ない。
「1番目と一緒やん」と思われるかもしれないが、ちょっと違う。
- ❌ 1番目:UA判定でクローラーだけを振り分ける → クローラーリスト管理が地獄
- ✅ 採用案:全アクセスをPHPに通す → UA管理不要
考え方
.htaccessで記事ページへのアクセスをすべてPHPにリライト- PHPがAPIから記事情報を取得してOGPタグを生成
- 元のHTMLテンプレートを読み込み、
<head>にOGPを注入して返す - JSの動的生成はそのまま動作(人間向け表示)
クローラーかどうかを判定する必要がない。全員に同じ処理をするだけ。
実装例
RewriteEngine On
# 記事ページへのアクセスをPHPに流す
RewriteRule ^news/([0-9]+)/?$ ogp_injector.php?target=news&id=$1 [L]
RewriteRule ^works/([0-9]+)/?$ ogp_injector.php?target=works&id=$1 [L]
<?php
$id = $_GET['id'] ?? null;
$target = $_GET['target'] ?? null;
// ターゲットに応じた設定
$templateFile = '';
$apiUrl = '';
if ($target === 'news') {
$templateFile = 'news_single.html';
$apiUrl = 'https://api.example.com/news/' . $id;
} elseif ($target === 'works') {
$templateFile = 'works_single.html';
$apiUrl = 'https://api.example.com/works/' . $id;
} else {
header('Location: /');
exit;
}
// APIから記事情報を取得
$ogTags = '';
if ($id && $apiUrl) {
$response = file_get_contents($apiUrl);
if ($response) {
$data = json_decode($response, true);
$title = htmlspecialchars($data['title'] ?? 'タイトル', ENT_QUOTES, 'UTF-8');
$description = htmlspecialchars($data['description'] ?? '', ENT_QUOTES, 'UTF-8');
$image = htmlspecialchars($data['thumbnail'] ?? '', ENT_QUOTES, 'UTF-8');
$url = 'https://example.com/' . $target . '/' . $id;
$ogTags = <<<OG
<meta property="og:title" content="{$title}">
<meta property="og:description" content="{$description}">
<meta property="og:image" content="{$image}">
<meta property="og:url" content="{$url}">
<meta property="og:type" content="article">
<meta name="twitter:card" content="summary_large_image">
OG;
}
}
// テンプレート読み込みとOGP注入
if ($templateFile && file_exists($templateFile)) {
$html = file_get_contents($templateFile);
if ($ogTags) {
$html = str_replace('<head>', "<head>\n" . $ogTags, $html);
}
echo $html;
} else {
header('Location: /');
exit;
}
💡 この方法のメリット
UA管理不要
クローラーかどうかを判定しない。全アクセスに同じ処理をするので、新しいクローラーが出てきても対応不要。
HTMLテンプレートはそのまま
納品するHTMLファイルは純粋なHTML。PHPは「裏側の配信処理」として分離されているので、納品先への説明もしやすい。
フレームワーク移行時も流用可能
将来「やっぱりVueで作り直します」となっても、静的出力(SSG)であればOGP注入のPHP部分はそのまま使える。フロントエンドの実装に依存しない。
ただしSSRの場合はサーバー側でHTMLを生成するため、この方法は使えない。その場合はフレームワーク側のメタタグ管理機能(NuxtならuseHeadなど)を使うことになる。
SPAモードの場合でも、index.htmlの<head>に差し込めるならこの方法は使える。ただしフレームワーク側のOGP出力(useHeadなど)は消さないこと。直リンクはPHP経由になるが、サイト内遷移はJSでの動きになるので、titleなどが入らないと困る。
SPAや動的生成サイトでOGP対応に困っている人の参考になれば。