多言語SSR SEO: hreflang、canonical、noindex 予算
1つの記事を複数の言語で提供すると、hreflang、canonical URL、noindex の方針が連携しない限り、検索ランキングが分散してしまいます。ここではその方法を紹介します。
1つの英語記事を複数の言語に翻訳し、それぞれをサーバーサイドレンダリングで提供することは、検索トラフィックにとって純粋な勝利のように聞こえます。それはまた、自身のランキングを分散させ、インデックスを低品質なページで溢れさせ、重複コンテンツとしてフラグを立てられるための近道でもあります。検索流入に依存するブログのすべてはインデックスがクリーンな状態を保つことにかかっているため、言語レイヤーは最後の仕上げとしてではなく、SEOを最初の制約として構築する必要があります。この投稿では、それをクリーンに保つための3つのコントロール、すなわち、絶対URLがどのように生成されるか、canonicalタグとhreflangタグがどのように関連するか、そしてnoindex予算が実際に公開する翻訳をどのように決定するかを順を追って説明します。
多言語サイトにおける重複コンテンツの罠
5つの言語で書かれた同じ記事は5つのURLとなり、注意を怠ると検索エンジンはこれらを競合として扱う可能性があります。この問題が発生するには2つの異なる方法があり、それらは複合的に作用します。
1つ目は言語の重複です。もし韓国語版と日本語版の構造がほぼ同一で、エンジンがそれらを1つのオリジナルからの翻訳であると判断できない場合、エンジンは1つを選んで残りを無視するか、あるいは被リンクのオーソリティをそれらすべてに分散させてしまうかもしれません。あなたが望んでいたのは、言語ごとに1つの強力なページでした。しかし手に入ったのは、1つのページの5つの弱い断片です。
2つ目はホストの重複であり、これは人々が予想するよりも簡単に発生します。もしサーバーが、受信したリクエストのホストから絶対URLを構築する場合、例えばベアドメインとwwwサブドメイン、あるいは流出したステージングエイリアスなど、2つのホスト名で同じコンテンツにアクセス可能になった瞬間に、エンジンはすべてのページの完全なコピーを2つ認識します。これで5つの言語バリエーションは10、あるいは15になり、それぞれの分割がランキングシグナルを失わせます。両方の問題の解決策は、同じところから始まります。サイトは、自身のURLが何であるかについて、ただ1つの見解を持たなければなりません。
リクエストホストから絶対URLを生成してはならない
最も重要なルールは、面白みのないものです。絶対URLは、リクエストからではなく、一つの固定されたベース値から生成されます。GoのSSRサーバーでは、canonical、hreflang、Open Graph、サイトマップのURLはすべて、設定されたSITE_BASE_URLから生成され、リンクを構築するためにr.Hostが読み取られることは決してありません。
// config.SiteBaseURL is the one source of truth, e.g. "https://lucidnote.net".
// Do not read r.Host here. A request arriving on any alias still emits
// the same canonical origin, so the index sees one copy, not one per host.
func absURL(base, lang, slug string) string {
return base + "/" + lang + "/" + slug
}
r.Host からリンクを構築すると、サーバーに解決されるすべてのホスト名が、インデックス内でサイト全体のパラレルワールドになってしまいます。固定のベース値は、それらすべてを1つにまとめます。これはページごとの決定やテンプレートの詳細ではありません。これはサーバー全体のプロパティであり、いずれかのハンドラーがリンクを作成するためにリクエストホストにアクセスした場合に失敗するテストを行う価値があります。
すべてのページで自身の絶対アドレスが統一されると、重複を管理する2つのタグ、canonicalとhreflangは、確固たる基盤を得ることができます。
canonicalは1つのURLをオリジナルとして指定する
canonicalタグは、あるコンテンツに対してどのURLが正規のものであるかをエンジンに伝えます。適切に構築された多言語サイトでは、各言語ページはそれ自体にcanonicalを設定します。韓国語ページはそのcanonicalを韓国語のURLに、日本語ページは日本語のURLに向けます。それらは互いの重複ではなく、代替ページであり、hreflangがその関係を別途表現します。
canonicalがその役割を果たすのは、単一言語内です。トラッキングパラメータ付きで到達可能なページや、末尾にソート引数がある場合とない場合で利用可能なカテゴリリストを考えてみてください。それらの亜種はすべて、クリーンなURLを指すcanonicalを持つべきです。そうすることで、クエリストリング付きのコピーが本物と競合しなくなります。ルールは単純です。canonicalは言語内の重複を解決し、上記と同じ理由で、常に固定ベースから構築された絶対URLを指す必要があります。
よくある間違いは、すべての翻訳を英語のオリジナルにcanonical設定することです。それは、他の言語は英語の重複であり、それら自体ではインデックスされるべきではないとエンジンに伝え、翻訳によって得られるはずだったトラフィックを捨てることになります。言語ごとの自己参照canonicalが、ほとんどの場合に望ましい設定です。言語間の関係はhreflangの役割です。
hreflangは、すべての言語版を対称的に対応付けます
hreflangは、ページが他の言語版を宣言する方法です。各ページは、自身を含む、利用可能なすべての言語に対してlink rel="alternate"をリストし、さらに、提供していない言語のユーザー向けのフォールバックを指定するx-defaultもリストします。エンジンはこれを使用して、最初に偶然クロールしたバージョンを表示するのではなく、韓国語の検索者には韓国語版を、日本語の検索者には日本語版を表示します。
以下は、ある記事の英語版のheadで、4つの翻訳された兄弟ページとデフォルトを宣言しています:
<link rel="canonical" href="https://lucidnote.net/en/multilingual-ssr-seo">
<link rel="alternate" hreflang="en" href="https://lucidnote.net/en/multilingual-ssr-seo">
<link rel="alternate" hreflang="ko" href="https://lucidnote.net/ko/multilingual-ssr-seo">
<link rel="alternate" hreflang="ja" href="https://lucidnote.net/ja/multilingual-ssr-seo">
<link rel="alternate" hreflang="zh-Hans" href="https://lucidnote.net/zh-Hans/multilingual-ssr-seo">
<link rel="alternate" hreflang="zh-Hant" href="https://lucidnote.net/zh-Hant/multilingual-ssr-seo">
<link rel="alternate" hreflang="x-default" href="https://lucidnote.net/en/multilingual-ssr-seo">
これには成否を分ける2つのプロパティがあります。1つ目は対称性です。英語のページが代替として韓国語をリストアップしている場合、韓国語のページも英語をリストアップしなければなりません。hreflang は相互宣言であり、エンジンは、元のページを指していない代替を信頼できないため、一方的なセットを静かに破棄します。サーバーがこれらのタグをレンダリングする際には、その記事で利用可能な言語の単一リストから完全な代替セットを生成し、すべてのバリアントが同じ完全で相互的なブロックを出力するようにすべきです。テンプレートごとに手動でリストを作成すると、非対称性が忍び込みます。
2つ目は、そのセットが実際に提供されるものを反映しなければならないということです。ある言語の翻訳に失敗し、そのページが存在しない場合、hreflang の代替として表示してはなりません。存在しない、または壊れているバリアントへのリンクは、クラスタ全体を汚染します。クリーンなアプローチは、サイトが理論的にサポートできる言語の静的なリストからではなく、その slug に対して正常に公開された言語版のセットから代替リストを導き出すことです。
noindex の予算:すべてを翻訳し、選択的に公開する
ここが、ほとんどのガイドが省略する部分です。hreflang と canonical を正しく設定することで、すべての言語を安全にインデックスさせることができます。しかし、そうすべきだという意味ではありません。
インデックスに登録するよう促すすべての URL は、エンジンが品質を判断するページです。若いサイトでは、合計3記事しかない言語の翻訳されたばかりのページは、薄いページ (thin page) です。つまり、周囲のコンテンツが少なく、兄弟階層の深さがなく、内部リンクもありません。そのようなページをすべての言語とカテゴリにわたって数百もインデックスさせると、インデックスされたセットの平均品質は低下します。その平均値は重要です。検索順位と広告ネットワークの審査はどちらもサイト全体を読み取り、大量の薄いページがあると、本当に重要なページの評価を下げてしまいます。
そこで、私たちは意図的に noindex の予算を費やします。翻訳とストレージは安価であり、すべての言語で実行されます。なぜなら、公開する価値が出た瞬間にコンテンツを準備しておきたいからです。インデックス登録は希少なリソースであり、自動的に行われるものではなく、許可されるものです。それを管理するための2つのルールがあります。
1つ目は、言語の許可リストです。インデックス可能な言語のセットは、提供される言語のセットのサブセットです。提供はされているがインデックスされていない言語に訪問者がアクセスした場合でも、完全で正しいページが表示されますが、そのページには noindex が付与され、サイトマップからは除外されます。例えば、5つの言語すべてを提供しつつ、自立できるだけの深さを持つ2つの言語だけをインデックスに招待する、といった具合です。
2つ目は、カテゴリごとのしきい値です。特定の言語のカテゴリページは、例えば3つなど、最低限の公開済み投稿が含まれて初めてインデックスされます。それを下回る場合、そのカテゴリリストは定義上薄いページとなるため、noindex が付与され、満たされるまでサイトマップから除外されます。翻訳は依然として存在し、ページはレンダリングされ、URL はそこにアクセスした人間に対して機能します。ただ、まだクローラーには告知されていないだけです。
その結果、言語とカテゴリの完全なマトリックスを翻訳・保存し、希薄化させるのではなく、助けとなるほど密度の高いセルのみを公開することになります。
ポリシーの一覧表
すべてのページはいくつかの状態のいずれかに分類され、4つのSEOコントロールは各状態で一致する必要があります。保存とレンダリングは、インデックス登録とは独立しています。
| ページの状態 | 翻訳・保存 | 訪問者への表示 | canonical | hreflang代替 | robots | サイトマップ内 |
|---|---|---|---|---|---|---|
| インデックス可能な言語、高密度カテゴリ | はい | はい | 自己 | 完全な相互セット | index, follow | はい |
| 提供されるがインデックス不可の言語 | はい | はい | 自己 | インデックスされた兄弟によってのみリストされる | noindex, follow | いいえ |
| 投稿しきい値を下回るカテゴリ | はい | はい | 自己 | その投稿の完全なセット | noindex, follow | いいえ |
| クエリパラメータのバリアント | 該当なし | はい | クリーンURL | クリーンURLから継承 | index, follow | いいえ |
| 翻訳に失敗した言語 | いいえ | いいえ | 該当なし | すべてで省略 | 該当なし | いいえ |
人々が戸惑うのは2行目です。インデックス不可の言語ページは、依然としてレンダリングされ、hreflangを介してその兄弟を宣言しますが、インデックス登録しているページによって代替としてリストされるべきではありません。なぜなら、noindexターゲットを真の代替としてクローラーに指し示させたくないからです。インデックス登録されたクラスターはインデックス登録されたメンバーを参照するようにし、追加の言語は直接そこに移動する人間のために存在させてください。
sitemap、robots、canonical、hreflangの整合性を保つ
これら4つのコントロールは、セットとしてのみ機能します。失敗のモードは、1つの間違ったタグではなく、2つのタグが一致しないことです。これはクローラーにとっては混乱したサイトとして読み取られ、保護しようとしていたクロールバジェットを無駄にします。
サイトマップには、インデックスさせたいURLだけをリストアップし、noindexを持つものは一切含めないようにすべきです。サイトマップにnoindexページが存在することは矛盾です。クローラーにフェッチを依頼した後に、見つけたものを忘れるように指示したことになります。robotsメタタグは、特定のページがインデックスされるかどうかの権威であり、サイトマップは単純にそれに従うべきです。canonicalはインデックス可能なURLのみを指すべきであり、これにより内容の薄い亜種がそれ自体を何かの権威あるコピーとして指定することはありません。そしてhreflangは、インデックスされたクラスター内で、それ自体がインデックスされているページのみを参照すべきです。
これらの整合性を保つ方法は、インデックスの決定を一度、一箇所で計算し、4つの出力すべてがそこから読み取るようにすることです。サーバーまたはインジェストステップが、特定の(言語、カテゴリ、投稿)がインデックス可能であると決定すると、その単一のブール値が、robotsタグ、サイトマップへの包含、canonicalのターゲット、そしてどのhreflang代替が出力されるかを決定します。各出力が独自の決定を行うと、それらは乖離し、その乖離がペナルティを受ける原因となります。
トレードオフと、すべてをインデックス登録するタイミング
インデックスを絞ることには、正当なコストが伴います。インデックス登録されるページが少なければ、初日から検索からのエントリーポイントが少なくなるため、すべてのスタブのすべての翻訳を公開した場合よりも初期のトラフィックは低くなります。これは、現在のリーチを将来の品質スコアと引き換えにしているのです。少数の強力なページが多数の弱いページを上回ることに賭けており、検索と広告で収収益を得るサイトでは、これは確実にそうなります。
このバジェットは永続的なものではありません。これは新しいサイトやコンテンツが少ないサイトに適した状態であり、コンテンツが蓄積されるにつれて緩和されるべきです。あるカテゴリーが投稿のしきい値を超え、それ自体でインデックス登録を開始します。ある言語がカテゴリー全体で十分な深さを獲得し、インデックス登録の許可リストに追加しても内容の薄いページが生成されなくなります。コントロールは同じで、しきい値が変わるだけです。
内容の薄いページのリスクがなくなった時点で、すべてをインデックス登録できます。それは、ある言語のほぼすべてのカテゴリーがすでにしきい値をクリアし、内部リンクが各ページに真の兄弟コンテキストを与え、ネイティブの検索ユーザーがアクセスしても直帰しないほど翻訳の品質が十分に維持されている場合に起こります。その時点で、noindex のバジェットはその役割を果たし、より広いインデックスは負債ではなく資産となります。それまでは、マトリックス全体を翻訳し、hreflang と canonical を厳密かつ対称的に保ち、自立できるページにインデックスを費やしてください。