0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

多言語サイトのリンクプレビュー設計:og:locale・hreflang・言語別OG画像を正しく出し分ける

0
Posted at

ogscope.png

はじめに

日本語と英語の2言語でサイトを運営していると、こんな報告を受けることがあります。

  • 日本語ページのURLをSlackに貼ったのに、英語のタイトルと英語の画像で展開された
  • 英語版の記事をXでシェアしたら、日本語のOG画像が出てきた
  • 言語切り替えボタンで日本語にしているのに、LINEに送ると英語のカードになる

ページ本文はきちんと翻訳されていても、リンクプレビュー(OGカード)は別の仕組みで作られます。SNSやチャットアプリのクローラーは、ログインもせず、Cookieも持たず、多くの場合ブラウザの言語設定も送らずにURLを取得し、返ってきたHTMLの<head>だけを見てカードを作ります。

この記事では、多言語サイトでリンクプレビューを言語ごとに正しく出すための設計を、URL設計・タグ・画像・検証の順にまとめます。日本語OG画像そのものの作り方(フォントや改行など)は別の話なので、ここでは「どの言語のカードを、どのURLで返すか」に絞ります。

1. 言語ごとにURLを分ける(これが9割)

一番大事なのはここです。カードはURL単位で作られ、URL単位でキャッシュされます。

同じURLで、CookieやAccept-Languageヘッダーを見て日本語・英語を出し分けている場合、クローラーにはどちらか一方(多くはデフォルト言語)しか見えません。しかも一度キャッシュされると、そのURLのカードは全員に同じものが表示されます。

おすすめは、言語をパスに含める形です。

https://example.com/ja/blog/release-notes   # 日本語
https://example.com/en/blog/release-notes   # 英語

サブドメイン(ja.example.com)でも構いません。大事なのは「1つのURL = 1つの言語」になっていることです。

自動リダイレクトに注意

トップページでAccept-LanguageやIPアドレスを見て/ja/や/en/へ自動リダイレクトしている場合、クローラーは送ったヘッダーや接続元の国によって、どちらかの言語へ飛ばされます。その結果、日本語ページのURLを貼っても英語のカードが出る、ということが起こります。

  • 言語付きURL(/ja/...)では絶対にリダイレクトしない
  • 自動判定はトップの/だけにするか、バナーで「日本語版はこちら」と案内する

この2点を守るだけで、ほとんどの「違う言語で展開される」問題は消えます。

2. og:locale と og:locale:alternate

Open Graphには言語・地域を示すog:localeがあります。書式は 言語_地域(アンダースコア区切り) で、ja_JP、en_USのように書きます。HTMLのlang属性やhreflangで使うja-JP(ハイフン)とは書式が違うので注意してください。

<!-- /ja/blog/release-notes -->
<html lang="ja">
<head>
  <meta property="og:locale" content="ja_JP" />
  <meta property="og:locale:alternate" content="en_US" />
  <meta property="og:title" content="リリースノート:v2.3で変わったこと" />
  <meta property="og:description" content="検索の高速化と、CSVエクスポートの文字化け修正が入りました。" />
  <meta property="og:url" content="https://example.com/ja/blog/release-notes" />
  <meta property="og:image" content="https://example.com/og/ja/release-notes.png" />
</head>

正直なところ、og:localeを見てカードの見た目を変えるプラットフォームは多くありません。カードの言語を決めているのは、実際にはog:title・og:description・og:imageの中身です。それでもog:localeは「このページは日本語です」という意図を明示する安価なメタデータなので、正しい値を入れておきましょう。

3. hreflang と canonical はセットで

検索エンジン向けには、言語版同士をhreflangでつなぎます。

<link rel="canonical" href="https://example.com/ja/blog/release-notes" />
<link rel="alternate" hreflang="ja" href="https://example.com/ja/blog/release-notes" />
<link rel="alternate" hreflang="en" href="https://example.com/en/blog/release-notes" />
<link rel="alternate" hreflang="x-default" href="https://example.com/en/blog/release-notes" />

よくある間違いが、すべての言語版のcanonicalを英語版に向けてしまうことです。こうすると「日本語版は英語版の重複です」と宣言することになり、og:urlも英語版になっていればカードも英語版扱いになりがちです。

  • canonical と og:url は そのページ自身の言語版URL
  • 他言語版へはhreflangで案内する

と覚えておくと混乱しません。

4. 言語別のOG画像

タイトルを翻訳しても、OG画像に英語の文字が入っていれば、日本語のタイムラインでは英語のカードに見えます。画像に文字を入れるなら言語ごとに用意します。

/og/ja/release-notes.png
/og/en/release-notes.png

動的生成している場合も、パスかクエリに言語を含めて、画像URL自体を言語ごとに別にするのがポイントです。画像URLが同じだと、プラットフォーム側の画像キャッシュで、別言語の画像が使い回されることがあります。

画像URLは絶対URL(https://から始まる)にして、ログインなしで200が返ることを確認しておきましょう。

5. Next.js(App Router)での実装例

app/[lang]/blog/[slug]/page.tsxのような構成なら、generateMetadataで言語ごとに組み立てられます。

// app/[lang]/blog/[slug]/page.tsx
import type { Metadata } from "next";
import { getPost } from "@/lib/posts";

const SITE = "https://example.com";
const LOCALES = { ja: "ja_JP", en: "en_US" } as const;
type Lang = keyof typeof LOCALES;

export async function generateMetadata(
  { params }: { params: Promise<{ lang: Lang; slug: string }> }
): Promise<Metadata> {
  const { lang, slug } = await params;
  const post = await getPost(slug, lang);
  const url = `${SITE}/${lang}/blog/${slug}`;
  const others = (Object.keys(LOCALES) as Lang[]).filter((l) => l !== lang);

  return {
    title: post.title,
    description: post.excerpt,
    alternates: {
      canonical: url,
      languages: {
        ja: `${SITE}/ja/blog/${slug}`,
        en: `${SITE}/en/blog/${slug}`,
        "x-default": `${SITE}/en/blog/${slug}`,
      },
    },
    openGraph: {
      url,
      title: post.title,
      description: post.excerpt,
      locale: LOCALES[lang],
      alternateLocale: others.map((l) => LOCALES[l]),
      images: [`${SITE}/og/${lang}/${slug}.png`],
    },
  };
}

<html lang>はルートレイアウトでparams.langから設定します。翻訳がまだない記事は、alternates.languagesから外しておきましょう(存在しないURLをhreflangで指すのは逆効果です)。

6. 検証:言語ごとに「素の状態で」取得する

ブラウザで確認すると、Cookieや言語設定の影響を受けるので当てになりません。Cookieなし・言語ヘッダーなしで取得して比べます。

for lang in ja en; do
  echo "== $lang"
  curl -s "https://example.com/$lang/blog/release-notes" \
    | grep -Eo '<meta property="og:(locale|title|url|image)"[^>]*>'
done

あわせて、Accept-Language: enを付けても/ja/のURLが日本語のまま返るか(リダイレクトされないか)も確認します。

curl -sI -H "Accept-Language: en" https://example.com/ja/blog/release-notes | head -5

OG画像は、言語ごとの画像URLが実際に画像として返るかまで見ておくと安心です。私はOG ScopeのOG画像チェッカーで、ページに書かれたog:imageの値と、そのURLを実際にリクエストした結果(画像が返ったか、相対パスになっていないか)を言語版ごとに確認しています。

チェックリスト

  • 1つのURLが1つの言語に対応している(Cookie・ヘッダーで出し分けていない)
  • 言語付きURLでは自動リダイレクトしない
  • og:localeはja_JP形式、hreflangはja/ja-JP形式
  • canonical と og:url はそのページ自身の言語版URL
  • OG画像のURLが言語ごとに別、かつ絶対URLで200が返る
  • Cookieなしのcurlで言語ごとに確認した

多言語対応というと翻訳に目が行きがちですが、シェアされたときの第一印象はカードで決まります。URL設計さえ正しければ、あとはタグを言語ごとに埋めるだけです。


Abid Ali:パキスタンのWeb開発者。リンクプレビュー確認ツールOG Scopeなど、小さな無料ツールを作っています。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?