はじめに
日本語と英語の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など、小さな無料ツールを作っています。
