技術 約5分で読めます

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]

却下理由:クローラーリストの管理が現実的でない

クローラーって世の中に無限大にある。主要なものだけでも相当な数で、新しいサービスが出るたびに追加が必要になる。

一応、クローラーリストを公開しているところはある:

国内のリストだと少なすぎるので海外ソースを参照することになるが、これをメンテし続けるのは現実的ではない。

2. History APIと履歴でOGP差し込み

考えた流れ:

  1. .htaccessで初回アクセス時に?init=1のようなクエリを付与
  2. 初回アクセスを全てPHPに流してOGP付きページを返す
  3. リダイレクトで記事ページに飛ばす
  4. History APIで履歴を消すことで「戻る」でOGPページに戻らないようにする
  5. ついでに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管理不要

考え方

  1. .htaccessで記事ページへのアクセスをすべてPHPにリライト
  2. PHPがAPIから記事情報を取得してOGPタグを生成
  3. 元のHTMLテンプレートを読み込み、<head>にOGPを注入して返す
  4. 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対応に困っている人の参考になれば。