コンテンツへ移動

出力の共通仕様

これは、RankbeamのSEOデータを出力する際に、すべてのフロントエンド構成の<head>が満たすべき唯一の共通チェックリストです。次の基準となります。

  • Coreのレンダラー出力構造の単体テスト(tests/Unit/Services/RenderingContractTest.php)。パッケージCIが実行する、高速でフレームワーク非依存のテストです。
  • rankbeam-examplesの構成別参照アプリ(Blade、Inertia + Vue / React / Svelte、Livewire)。ブラウザーとSSRのテストで、同じ条件を実際のDOMに対して検証します。
  • フレームワーク別ガイド(Blade、InertiaとJSON、Livewire)。この仕様に反する実装手順を記載してはいけません。

構成がある条項を満たせない場合は、不具合または明示された制約として扱います。仕様を緩める理由にはなりません。データ層(SEOResolver → 不変のSEODataTagRenderer)はフレームワーク非依存です。構成ごとに異なるのは、解決済みデータをDOMへ届け、クライアント側の画面遷移後も保ち、クローラーから見える状態にする方法だけです。この仕様は、その点を定めます。

この仕様は、独立した設計レビューを経て強化されています。 内容に大きな変更があった場合だけ、再レビューしてください。


1. 値 — 仕様に準拠する<head>の内容

タイトル、ディスクリプション、正規URL

  • 解決済みのタイトルを持つ**<title>が、ちょうど1つ**あります。サフィックスは二重に付けません。リゾルバーはseo.title_suffixを1回だけ追加し、すでに同じサフィックスで終わるタイトルへの重複追加を防ぎます。
  • ディスクリプションが解決された場合だけ、meta descriptionを1つ出力します。空のタグは出しません。
  • <link rel="canonical">を1つ出力します。

Robots

  • <meta name="robots">は、指示がサイトのデフォルトと異なる場合だけ出力します。冗長なindex,followは不要であり、そのタグがない状態をクローラーはindex,followとして扱います。比較では空白を区別せず(index, followindex,follow)、異なる指示はそのまま出力します。seo.robots.emit_default = trueでタグを強制出力できます。
  • 決定的な詳細指示に対応します。noindexnofollownoarchivenosnippetmax-snippetmax-image-previewmax-video-previewnotranslateunavailable_afterです。これらは解決済みの文字列値であり、優先順位はリゾルバーの連鎖(グローバル → ルート → モデル → 明示的な値)に従います。同じ入力なら同じ出力になります。

Open Graph

  • og:titleog:descriptionog:typeog:urlog:site_nameog:locale
  • article:*published_timemodified_timeauthorsectiontag)は、og:type === 'article'で、値が実在する場合だけ出力します。値を作り上げたり、記事でないページに出力したりしません。
  • og:imageには、値が分かる場合og:image:width / og:image:height / og:image:altog:image:typeを付けます。複数の画像はグループ化し、各og:imageの直後に、その画像自身の寸法・代替テキスト・型を置きます。

Twitter Cards

  • twitter:cardtwitter:titletwitter:descriptiontwitter:image、および画像の代替テキストが分かる場合のtwitter:image:alt
  • twitter:sitetwitter:creator任意かつ独立です。片方だけが存在してもよく、一方から他方を作り上げることはありません。

hreflangとロケール

  • hreflangには、モデルのgetSEOAlternates()フックを通る正式な解決経路があります。
  • hreflangの代替ページがある場合は、絶対URL、正規化済み、言語ごとに一意で、データが揃っていれば相互参照します。x-defaultは設定した場合だけ出力します。
  • og:locale:alternateは、実際のソーシャル向け言語版があるロケールだけを反映します。en-USen_USに変換し、変換後の形式で比較します。文字列の完全一致は要求しません。
  • <html lang>は解決済みロケールと一致させます。<html>要素を出力するのはアプリですが、この条項も共通仕様に含みます。

ページ別のJSON-LD

  • 解析可能で、</script>に対して安全であること。ペイロードをJSON_HEX_TAGでエンコードし、値がscript要素を途中で終了させないようにします。保存型XSSへの保護です。
  • 複数の<script>ブロックでも、結合した@graphでも構いません。
  • 安定した@idを使うのは、エンティティが実際に結び付く場所だけです(Organization ↔ WebSite ↔ WebPage)。独立したノードに安定した@idは必須ではありません。

2. 正規化と不変条件

  • canonicalog:urlog:imagetwitter:imageには、絶対http(s) URLを使います。空またはnullのタグをDOMに出してはいけません
  • canonicalog:urlは、同じ正規化済みURLに解決されなければなりません。 不一致は警告ではなく、必ず失敗とする条件です。
  • 正規URLの正規化方針を全体で統一します。スキーム、ホスト、ポート、パスの大文字・小文字、クエリ許可リスト、末尾スラッシュを常に同じように扱います。インデックス登録可能なページは自己参照とし、noindexページは他のページの正規URL方針を引き継ぎません
  • 出力先ごとにエスケープします。HTML属性、テキスト、JSONで、それぞれ適切なエンコーダーを使います。アサーションはバイトではなく、デコード後の意味上の値を比較します。
  • レンダラー間の同等性は意味の一致であり、バイト単位の一致ではありません。 正規化後render()(HTML)≡ toArray()toInertiaHead()となります。3つの表現は、タグの順序や形式が異なっていても正当です。単一値と繰り返し可能なプロパティのルールを明示します。og:titleは1つ、article:tagは複数です。
  • タグの所有者を区別します。クライアントレンダラーは、キー付きのパッケージ所有タグ(第4節参照)を置き換え、無関係なアプリ所有タグを削除しません。

3. 動作 — クライアント側の画面遷移

Inertiaの訪問、またはLivewireのwire:navigateの後は、毎回以下を満たします。

  • 単一であるべきタグが、それぞれちょうど1つ存在し、古い値が残りません。対象は<title>、description、canonical、各og:* / twitter:*です。
  • JSON-LDが蓄積しません。前ページのスキーマは削除し、重ねません。Livewireは<script>を削除しないアセットとして扱うため、スキーマのscriptにdata-seo-schemaとURLごとのIDを付け、livewire:navigatedで前ページ分を削除します。Livewireガイドを参照してください。
  • メタデータが豊富なページから少ないページへ移ると、余分なタグを削除します。遷移先に、前ページのdescription、OG、スキーマを残しません。
  • ハイドレーション警告は0件で、ハイドレーションの前後でメタデータの意味が一致します。

4. Inertiaのhead-key(タグの所有者)

toInertiaHead()は、すべてのmeta・link項目に安定した**head-key**を付けます。Inertiaはこの属性でhead要素を重複排除します。ページの<Head>内のタグがレイアウト側と同じhead-keyを持つ場合、重複を追加せず置き換えます

  • 基本キーは、metaではname ?? property、linkではrelです。
  • 繰り返し可能なタグには識別子を付け、キーを一意に保ちます。article:tagarticle:tagarticle:tag:1など。hreflangではalternate:en-USalternate:fr-FRです。

テンプレートでは**:head-key**としてバインドしてください。Vueの:keyではありません。そちらはv-forの差分更新に使う別のキーで、Inertiaのhead重複排除には作用しません。


5. クローラーからの可視性(明示的なモード)

  • SSR / プリレンダリングは、生のHTTP HTMLに共通仕様の出力をすべて含めなければなりません。これは、ハイドレーション後のDOMとは別に、JSを無効にして検証します。
  • CSRだけでは、クローラー対応を主張できません。 デフォルトのSSRなしInertiaは、メタデータをクライアント側で挿入します。クローラーが取得する最初のHTMLにはSEOメタデータがありません。この制約は隠さず記載します。クローラーに見えるメタデータにはInertia SSRまたはプリレンダリングが必要であり、クローラー向けのJSON-LDもサーバーで出力すべきです。

6. 対象外・目的としないこと

  • レンダラーではなくアプリの担当なのは、charsetviewport、faviconです。<meta charset>は非ASCIIのメタデータより前に置く必要があるため、これらのhead要素の順序はアプリが管理します。
  • E2Eテストが検証するのは出力だけです。Googleのインデックス登録、正規URLの選択、リッチリザルトの対象になるか、順位は検証しません。リモート画像のMIMEや取得可能性も検証しません。これらは任意の統合・HTTPテストで扱い、ブラウザーマトリックスには入れません。

7. 準拠状況

現時点で各条項を何が検証するかを示します。単体RenderingContractTest(Core、パッケージCI)、ブラウザー/SSRrankbeam-examples(定期実行マトリックス)、アプリはホストアプリが担当することを意味します。計画中は、仕様上の目標ではあるものの、まだSEODataにデータがモデル化されておらず、レンダラーは安全な部分集合を出力する状態です。

条項状況
解決済みの<title>がちょうど1つ、サフィックスの重複なし単体 + ブラウザー
存在する場合だけmeta descriptionを出力単体 + ブラウザー
<link rel="canonical">が1つ、空ではない単体 + ブラウザー
デフォルトと異なる場合だけrobotsをそのまま出力。emit_defaultで切り替え単体 + ブラウザー
詳細なrobots指示をリゾルバーの優先順位で解決単体(リゾルバー)
og:title/description/type/url/site_name/locale。ロケールen-USen_US単体 + ブラウザー
og:type=articleで実在する場合だけarticle:*を出力単体 + ブラウザー
og:imageが存在し、絶対URLである単体 + ブラウザー
og:image:width/height/altog:image:type、複数画像のグループ化計画中SEODataは単一のogImage文字列を持ち、寸法・代替テキスト・型は未モデル化です。レンダラーは絶対URLのog:imageを1つ出力します。
twitter:card/title/description/imagesite / creatorは独立単体 + ブラウザー
twitter:image:alt計画中 — 画像の代替テキスト用フィールドは未モデル化です。
hreflangが絶対URLで、言語ごとに一意単体 + ブラウザー
hreflangの相互参照、設定時のx-defaultブラウザー(データ依存)
og:locale:alternateが実際のソーシャル向け言語版を反映計画中 — ロケール別のソーシャル向け言語版マップは未モデル化です。
<html lang>の一致アプリ(ブラウザーでも検証)
JSON-LDが解析可能で、</script>に対して安全単体 + ブラウザー
複数scriptまたは@graph。エンティティが結び付く場所に安定した@id単体(Merchantのグラフ)+ ブラウザー
絶対URL、空・nullのタグなし単体 + ブラウザー
canonicalog:url。不一致は必ず失敗単体 + ブラウザー
一貫した正規URLの正規化、自己参照、noindexの分離ブラウザー
出力先別エスケープ、デコード後の意味の一致単体
レンダラー間の意味の一致(render()toArray()toInertiaHead()単体
Inertiaのhead-keyが安定し、繰り返しタグを区別単体 + ブラウザー
クライアント遷移:単一タグの一意性、古い値なし、JSON-LD非蓄積、不要タグの削除ブラウザー — レンダラーが削除に必要なdata-seo-schemaフックを提供
ハイドレーション警告0件、前後の一致ブラウザー
SSRの生HTMLが仕様全体を出力。CSRのみは非準拠と明記ブラウザー + ドキュメント

計画中の条項は、意図的に記録された未対応部分です。共通仕様は長期的な目標であり、これらは今後の作業で追加する後方互換の拡張です。新しいSEODataフィールド・カラムと、SemVerのマイナーバージョン更新が必要です。現在のレンダラーは安全な部分集合を出力し、持っていない値を作り上げることはありません。

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