コンテンツへ移動

OG画像の生成

コア3.20以降、Chromeによる描画ではJavaScriptを無効にし、HTTP(S)、FTP、WebSocketによるアセットへのリクエストを遮断します。独自テンプレートも、同梱テンプレートと同様に、静的なHTML/CSSと埋め込みアセットを使う必要があります。

ソーシャルカードがないページでは、共有のdefault_og_imageにフォールバックし、どのページを共有しても同じ画像になります。この機能では、各ページに専用の1200×630のOpen Graph / Twitterカードを用意します。spatie/browsershotを介して実際のヘッドレスブラウザーでBladeテンプレートを描画するため、タイトルの複数行への折り返し、アクセント付き文字の表示、CJK文字に適したフォントへのフォールバック、長すぎるタイトルの省略に対応できます。独自の画像ライブラリだけでは、これらすべてを適切に処理するのは困難です。

無料のコア機能で、デフォルトは無効です。無効な場合はdefault_og_imageをそのまま使い、パッケージに追加の依存関係は生じません。

設計上、静的な事前生成を採用

カードはWebリクエスト中にその場で描画せず、artisanコマンドで事前生成します。ページがリンクするのはディスク上に存在するカードだけです。訪問者のリクエストでブラウザーを起動したり、存在しない画像(404)にリンクしたりすることはありません。リアルタイム描画用のエンドポイントはありません注意事項を参照)。

動作要件

ブラウザードライバーは任意の依存関係なので、無料コアはそれなしでインストールできます。この機能を有効にする場合、アプリケーションに次が必要です。

bash
composer require spatie/browsershot

さらに、Browsershotが使う実行環境も必要です。

  • ホスト上のNode.js
  • Nodeが解決できるように、アプリケーションのルートにインストールしたPuppeteer
    bash
    npm install puppeteer
  • Chrome / Chromium。Puppeteerはデフォルトで専用のChromiumをダウンロードします。本番環境では通常、システムのChromeを指定します(chrome_pathを参照)。

Windowsではpuppeteerをアプリケーションのルートにインストール

Windowsではnpm_module_pathに頼らず、puppeteerをアプリケーションのルートにインストールしてください。この設定キーはBrowsershotのsetNodeModulePath()に対応し、POSIX形式のNODE_PATH=…プレフィックスを出力するため、Windowsでは効果がありません。WindowsのNodeはアプリケーションから上位ディレクトリをたどってモジュールを解決するため、ルートへのインストールが有効です。注意事項も参照してください。

有効化

まだ設定を公開していなければ公開し(php artisan vendor:publish --tag=seo-config)、機能を有効にします。

php
// config/seo.php
'og_image' => [
    'enabled' => true,   // requires spatie/browsershot + Chrome
],

次にカードを事前生成します。これを行うまで画像は描画されません。

bash
php artisan seo:og-images

値の解決方法

生成画像が明示的に設定した画像を上書きすることはありません。有効時、リゾルバーはページ固有の画像がない場合にのみog:imageを補います。つまり、解決したog:imageが空か、サイト共通の静的なdefault_og_imageのままである場合です。モデルに明示した画像(getSEOImage()seo_metaの行、コンテンツフィールドなど)は、生成カードより常に優先されます。

値を決定するため、リゾルバーは生成サービスの存在確認を条件とする検索を呼び出します。カードの保存パスを計算し、設定したディスクにファイルがすでに存在する場合にのみ公開URLを返します。描画は行いません。安全性は次の仕組みに基づきます。

  • Webリクエストでブラウザーを起動することはありません。最悪の場合でも、この機能を導入する前と同じ静的なdefault_og_imageにリンクします。
  • ページが未生成の画像にリンクすることはありません。共有先の画像が一時的に404になる期間は生じません。

「コンテンツが変わった」時点から「カードが存在する」時点までの差は、デプロイ時や定期実行でseo:og-imagesコマンドを実行して埋めます。

seo:og-imagesコマンド

リゾルバーが配信できるように、カードを事前生成します。

bash
php artisan seo:og-images                         # warm the configured models
php artisan seo:og-images --model="App\Models\Post"
php artisan seo:og-images --force                 # re-render even existing cards
php artisan seo:og-images --prune                 # + delete orphaned cards
  • --model=*:事前生成するモデルクラスを1つ以上指定します。繰り返し指定できます。省略するとseo.og_image.modelsを使い、未設定ならサイトマップのモデルseo.sitemap.models)にフォールバックします。seo:llms-txtと同じく、サイトマップのソースを共用する方針です。
  • --force:すでに存在するカードも再描画します。cache_versionを上げずにテンプレートやブランドカラーを変更した場合に使います。
  • --prune:事前生成後、設定パス内の保存済みカードのうち、現在のどのモデルのコンテンツにも一致しないものを削除します(後述)。安全のため、ファイル名が生成されたコンテンツハッシュであるファイルだけを削除し、同じディレクトリの他のアセットは削除しません。--modelで対象を絞った実行では無視されます。残すべきファイルの集合に他のモデルが含まれないためです。--modelなしで実行してください。

各モデルはHasSEOトレイトを使う必要があります。タイトルのないレコードはカードに表示する内容がないためスキップします。コマンドはgeneratedskippedfailed、および--prune指定時のprunedの件数を報告します。

定期実行

カードをコンテンツに追従させ、タイトル変更で不要になったカードを削除するため、定期的に事前生成します。

php
// routes/console.php
Schedule::command('seo:og-images --prune')->daily();

無効化の仕組み

カードのファイル名は、描画結果のピクセルに影響するすべての値のハッシュです。タイトル、サイト名、テンプレート名、ドライバー、寸法、ブランドのグラデーション色、cache_versionの値、インストール済みパッケージのバージョンが含まれます。

このハッシュがキャッシュキーとなるため、次の2つの挙動を理解しておく必要があります。

  • タイトルを変更 → 新しいハッシュ → 新しいファイル。 古いカードはディスク上で参照されないファイルになり、再生成するまでページは静的なデフォルト画像にフォールバックします。コマンドを実行すると新しいカードを生成し、--pruneで不要なカードを削除します。これが無効化の仕組みであり、別途「1ページ分のキャッシュを破棄する」手順はありません。
  • cache_versionを上げる、またはパッケージを更新 → すべてのハッシュが変化。 テンプレートやブランドカラーを編集した後はcache_versionを使ってすべてのカードを一度に無効化できます。パッケージの更新は自動的にハッシュへ反映されるため、同梱テンプレートを変更するリリースの後に古いカードが配信されることはありません。

同梱テンプレート

同じブランドグラデーションを使う1200×630のテンプレートを3種類同梱しています。

テンプレート適した用途表示内容
seo::og.default全般タイトル + サイト名
seo::og.articleブログ記事、ニュースセクション名の小見出し + タイトル + 著者・日付
seo::og.product商品、掲載情報ブランド表記 + カテゴリチップ + タイトル + ディスクリプション

seo.og_image.templateで全体のテンプレートを選択するか、モデル種別ごとにテンプレートを対応付けることで、記事と商品に異なるカードを自動的に使えます。

php
// config/seo.php
'og_image' => [
    'templates' => [
        App\Models\Post::class    => 'seo::og.article',
        App\Models\Product::class => 'seo::og.product',
    ],
],

モデルにgetOgImageTemplate(): ?stringを定義すれば、実行時に自身のテンプレートを上書きできます。ビュー名を返すか、対応表またはデフォルトにフォールバックする場合はnullを返します。優先順位はモデルのフック、templatesの対応表、全体のtemplateです。

テンプレートのカスタマイズ

カードはBladeビュー(デフォルトはseo::og.default)を自己完結したHTML文書に描画したものです。同梱フォントはdata URIとして埋め込まれるため、ブラウザーにネットワーク接続は不要です。変更方法は2つあります。

同梱ビューを公開して編集する場合:

bash
php artisan vendor:publish --tag=seo-views

その後、resources/views/vendor/seo/og/default.blade.phpを編集します。

独自のビューを指定する場合:

php
// config/seo.php
'og_image' => [
    'template' => 'og.my-card',   // resources/views/og/my-card.blade.php
],

テンプレートには次の変数が渡されます。

変数補足
$titlestring設定済みのOGタイトル。なければページタイトル。
$siteName?string解決済みのog:site_name
$fontDataUristring同梱の太字フォントをdata: URIにしたもの。利用できなければ空文字列となり、ブラウザー自身のsans-serifを使います。
$gradientFromstringseo.og_image.gradient_from
$gradientTostringseo.og_image.gradient_to
$widthint出力幅(デフォルト1200)。
$heightint出力高(デフォルト630)。
$locale?string<html lang>属性に使う、解決済みのページロケール。
$author?string記事の著者(seo::og.articleで使用)。
$publishedDate?stringseo::og.articleの公開日。利用できる場合はページロケールのICU medium形式を使い、それ以外はCarbonで月名を翻訳し、M j, Yの順序で表示します。日付がなければnullです。
$section?stringコンテンツのセクションまたはカテゴリ(記事の小見出し、商品のチップ)。
$description?stringOGディスクリプション。なければページのディスクリプション(seo::og.productで使用)。

テンプレート名もキャッシュキーの一部

テンプレートの名前とグラデーション色はどちらもコンテンツハッシュに含まれるため、テンプレートの切り替えや色の変更で既存のカードは自動的に無効になります。同じテンプレートをその場で編集しても名前が変わらないため無効化されません。編集後はcache_versionを上げるか、--forceを実行してください。

設定

php
// config/seo.php
'og_image' => [
    'enabled' => false,             // master switch (off by default)
    'driver'  => 'browsershot',     // the render driver; register your own via OgImageManager::extend()
    'template' => 'seo::og.default', // the default Blade view rendered as the card
    'templates' => [],              // per-model-class template overrides (see "Bundled templates")
    'strip_title_suffix' => true,   // trim seo.title_suffix off the card title (the card shows the site name itself)

    'width'  => 1200,               // social-card standard
    'height' => 630,

    'disk' => 'public',             // must be publicly served — its url() becomes the og:image
    'path' => 'og-images',          // path prefix on that disk

    // Models seo:og-images warms. Empty → falls back to seo.sitemap.models.
    // Accepts a list [Post::class] or a map [Post::class => [...]].
    'models' => [],

    // Bump to invalidate every card after editing a template/colors in place.
    // The installed package version is folded in too, so an upgrade busts them.
    'cache_version' => 1,

    // Brand gradient (diagonal) for the bundled default template.
    'gradient_from' => '#1e2a5a',
    'gradient_to'   => '#3D5AFE',

    // Browsershot binary paths. null = its defaults (node/npx on PATH,
    // puppeteer's bundled Chromium). Set explicitly in production.
    'chrome_path'     => null,      // path to a system Chrome/Chromium
    'node_binary'     => null,      // path to the node binary
    'npm_module_path' => null,      // node_modules dir (no-op on Windows — see Caveats)

    'timeout' => 60,                // hard per-render timeout, seconds

    // Launch Chrome with --no-sandbox; weakens browser isolation.
    // Prefer configuring the host to support Chrome's sandbox (see below).
    'no_sandbox' => false,

    // Extra Chromium CLI flags, e.g. ['disable-dev-shm-usage', 'disable-gpu']
    // on a low-/dev-shm container. Leading "--" optional; map form for
    // value-bearing flags: ['proxy-server' => 'http://…'].
    'browsershot_args' => [],

    // Fallback font families for glyphs the bundled face lacks (CJK, Thai,
    // Arabic, …). null = the built-in Noto list; see "Fonts and non-Latin
    // scripts" below.
    'font_stack' => null,
],

ほとんどのスカラー値には対応する環境変数(SEO_OG_IMAGE_ENABLEDSEO_OG_IMAGE_DISKSEO_OG_IMAGE_CHROME_PATHSEO_OG_IMAGE_NO_SANDBOXなど)があります。全一覧は設定ファイルを参照してください。配列型のキー(templatesmodelsbrowsershot_argsfont_stack)は設定ファイルで直接編集します。

リゾルバーがディスクのurl()og:imageの値として使うため、ディスクは公開配信されている必要がありますpublicディスクでは、public/storageから参照できるよう、php artisan storage:linkを一度実行します。

Linuxでの実行(サンドボックス)

Chromeのサンドボックス機構を制限するホストでは、php artisan seo:og-imagesが次のエラーで失敗することがあります。

No usable sandbox! Update your OS ... or see
https://chromium.googlesource.com/.../linux/suid_sandbox_development.md

原因の1つとして、Ubuntu 23.10以降のユーザー名前空間の制限が考えられます。Puppeteerのトラブルシューティングガイドと、実際のブラウザー起動エラーを確認してください。Chromeのサンドボックスを維持できるよう、ホストの設定を修正する方法を優先します。

1. 明示的な代替策:--no-sandboxでChromeを実行。 ブラウザーの隔離が無効になります。デプロイ環境でこのトレードオフを意図的に受け入れる場合にのみ使ってください。

php
// config/seo.php
'og_image' => [
    'no_sandbox' => true,   // or set SEO_OG_IMAGE_NO_SANDBOX=true
],

Rankbeamは静的に生成したHTMLを描画し、リモートアセットへのリクエストを遮断しますが、これらの制御はChromeのサンドボックスの代わりにはなりません。描画プロセスは非特権で実行し、関係のないワークロードや秘密情報から隔離してください。

2. サンドボックスを維持。 no_sandboxは無効のままにします。AppArmorが原因の場合は、実際のChrome実行ファイルに合わせたプロファイルを設定します。Chromiumのガイドを参照してください。例:

# /etc/apparmor.d/chrome-og
abi <abi/4.0>,
include <tunables/global>
profile chrome-og /path/to/chrome flags=(unconfined) {
  userns,
  include if exists <local/chrome-og>
}

その後、sudo apparmor_parser -r /etc/apparmor.d/chrome-ogでプロファイルを読み込み、サンドボックスを有効にした状態でChromeが起動することを確認します。

その他のフラグ

共有メモリが少ないコンテナでは、Linuxでよくある別の問題として描画中にChromeがクラッシュします。その場合はbrowsershot_argsでフラグを追加します。

php
'browsershot_args' => ['disable-dev-shm-usage'],

独自ドライバー

同梱ドライバーはbrowsershotだけですが、レンダラーはインターフェース(Rankbeam\Seo\Contracts\OgImageRenderer)の背後にあります。たとえばcanvasやサービスを利用する独自レンダラーを登録し、seo.og_image.driverで選択できます。

php
use Rankbeam\Seo\Services\OgImage\OgImageManager;

app(OgImageManager::class)->extend('my-driver', fn ($app) => new MyRenderer());

ドライバーの役割は、自己完結したHTML文字列を指定寸法のPNGバイト列に変換することだけです。レイアウトやテンプレート処理は担当しません。

フォントと非ラテン文字

同梱のカード用フォント(Noto Sans Bold、OFL)は、ラテン文字、キリル文字、ギリシャ文字をカバーします。それ以外の中国語、日本語、韓国語、タイ語、アラビア語、ヘブライ語、デーヴァナーガリー文字、絵文字は、seo:og-imagesを実行するマシンにインストールしたフォントを使います。他のフォントを同梱しないのは意図的です。CJKフォントは1つで16 MB以上になり、適切なフォントがホストにあればChromeの文字単位のフォールバックが機能します。

この動作を支える仕組みは3つあります(3.15)。

  1. すべての同梱テンプレートに文字体系別のfont-familyスタックを設定。 bodyでは最初に同梱フォントの'OGBrand'、次にseo.og_image.font_stack、最後にsans-serifを宣言します。デフォルトの中間スタックはNoto Sans、4つのNoto Sans CJKファミリー、Noto Sans ThaiNoto Sans ArabicNoto Sans HebrewNoto Sans DevanagariNoto Color Emojiです。Chromeは文字ごとに、インストール済みの最初のファミリーへフォールバックし、ないものは飛ばします。同じ漢字コードポイントでも各地域のフォントで字形が異なるため(漢字統合)、ページの言語に対応するCJKファミリーを先頭に移しますja → JP、zh-Hans → SC、zh-Hant / zh-TW / zh-HK → TC、ko → KR)。<html lang>属性にはBCP 47形式のページロケールを設定します。スタックもキャッシュキーに含まれるため、変更するとすべてのカードが再描画の対象になります。

  2. seo:og-imagesによる事前確認。 描画前にfontconfig(fc-list :lang=jatharなど)を使い、タイトル、サイト名、ディスクリプションに含まれる文字体系をフォントがカバーしているか確認します。混在テキストに少数含まれる文字体系も対象です。足りない場合は文字体系ごとに1回、インストールするパッケージとともに警告します。

    No installed font covers cjk text — its cards may render as boxes. Install one: apt-get install fonts-noto-cjk

    fontconfigがない環境(Windows、macOS、最小構成のコンテナ)では推測せず、警告を出しません。フォントがなくても描画自体は失敗せず、Chromeは.notdefの四角を表示します。そのため、この警告が必要です。

  3. 実描画スモークテストの文字体系別グリフフィクスチャ。 SEO_OG_IMAGE_LIVE_TEST=1を指定すると、tests/Feature/OgImage/BrowsershotSmokeTest.phpはja、zh-Hans、zh-Hant、ko、el、ru、tr、th、ar、he、hiのタイトルと、同じ長さの未割り当てコードポイント(必ず四角になる)からなる対照画像を描画します。2つのPNGがバイト単位で一致すると、文字体系とパッケージ名を示して失敗します。これはスモークチェックであり、全グリフの表示を証明するものではありません。一部のグリフが欠けていても、混在するラテン文字や折り返しの違いで画像が異なることがあります。デプロイ先ホストで実際の描画結果と使用フォントを確認してください。言語単位のFontProbe警告も事前確認であり、完全なフォント対応の証明ではありません。コアにseo:doctorコマンドはありません。この確認にはseo:og-imagesを使います。

Debian/Ubuntuの場合:

bash
apt-get install fonts-noto-cjk fonts-noto-core fonts-noto-color-emoji
fc-cache -f

3.15より前に--tag=seo-viewsで公開した独自テンプレートも動作し続けます。新しい変数$fontFamily$langを受け取りますが、使わなくても構いません。

注意事項

本番環境で問題になり得るため、制約を明記します。

  • 事前生成のみで、リアルタイム描画エンドポイントはありません(v1)。 リクエスト時にカードを描画するルートはありません。Webリクエストで描画しないため、設定や防御が必要な署名付きURL / SSRF / DoSの公開面はありません。その代わり、カードを用意するにはデプロイ時や定期実行でseo:og-imagesを実行する必要があります。
  • npm_module_pathはWindowsでは効果がありません。 BrowsershotのsetNodeModulePath()に対応し、コマンドの前にPOSIX形式のNODE_PATH=…を付けますが、Windowsでは無視されます。Windowsではpuppeteerアプリケーションのルートにインストールし、Nodeが上位ディレクトリをたどって解決できるようにします。Linux/macOSでは設定どおりに動作します。
  • 非ラテン文字にはホスト上のフォントが必要です。 フォントと非ラテン文字を参照してください。同梱フォントはラテン文字、キリル文字、ギリシャ文字をカバーし、それ以外はデプロイイメージにインストールしたフォントを使います。不足するとコマンドが知らせます。
  • 失敗してもページ配信は継続。 描画に失敗した場合(パッケージ不足、ブラウザーのクラッシュ、タイムアウト)、コマンドが報告し、ページは静的なdefault_og_imageを使い続けます。ブラウザーが壊れてもページが500になることはありません。

rankbeam/laravel-seoはMITライセンスで公開されています。