はじめに
「Next.jsで多言語サイトを作りたい? next-i18next 入れれば終わりでしょ?」
そう思っていた時期が私にもありました。
静的なLPならそれでいいでしょう。しかし、数百記事を抱えるメディアサイトにおいて、多言語対応は**「翻訳」の問題ではなく「データ設計」の問題**です。
私は現在、Next.js × Sanity で構築した多言語メディア ibis (アイビス) を運営しています。
ここでは、英語、ベトナム語、中国語など8ヶ国語に対応し、それぞれの言語で独立したSEO評価を得ています。
本記事では、Sanityの Document Level Internationalization を採用し、Next.js App Router と連携させる「最も管理コストが低い」実装パターンを共有します。
この記事で得られる知見
- 🧱 Sanity: 「Field Level」ではなく「Document Level」翻訳を選ぶ理由
- 🛣️ Next.js: Middlewareを使った言語ルーティング (
/en/,/vi/) - 🔗 Link Logic: 多言語間でも「内部リンク」を自動追従させるGROQテクニック
1. Sanityの多言語戦略:Field vs Document
Sanityで多言語をやる場合、大きく2つの派閥があります。
🅰️ Field Level (1つの記事の中に全言語を入れる)
{
"title": {
"en": "Hello",
"ja": "こんにちは"
}
}
- メリット: データ構造がシンプル。
- デメリット: 「日本語だけ先に公開したい」ができない。翻訳が増えると管理画面が縦に長くなりすぎて死ぬ。
🅱️ Document Level (言語ごとに記事を作る) ★採用
// 記事A (英語)
{ "_id": "article-en", "language": "en", "title": "Hello" }
// 記事A (日本語)
{ "_id": "article-ja", "language": "ja", "title": "こんにちは", "translationId": "uuid-123" }
- メリット: 言語ごとに公開/非公開を制御できる。スラッグ(URL)を言語ごとに変えられる(SEO最強)。
- デメリット: 記事数が増える(100記事×4言語=400ドキュメント)。
メディアサイトの場合、SEOと運用フロー(翻訳できた順に公開したい) の観点から、間違いなく Document Level が正解です。
Sanity公式プラグイン @sanity/document-internationalization を使うことで、これらを紐付けて管理できます。
2. Next.js (App Router) 側のルーティング
Next.js側では、ディレクトリ構造で言語を表現します。
app/
[lang]/
layout.tsx
page.tsx
[slug]/
page.tsx
Middlewareでの言語判定
ユーザーのブラウザ設定やCookieを見て、適切な言語パスにリダイレクトするMiddlewareを配置します。
// middleware.ts
import { match } from '@formatjs/intl-localematcher'
import Negotiator from 'negotiator'
export function middleware(request: NextRequest) {
const pathname = request.nextUrl.pathname
// すでに言語パス(/en/など)が含まれていればスルー
const pathnameIsMissingLocale = i18n.locales.every(
(locale) => !pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}`
)
if (pathnameIsMissingLocale) {
const locale = getLocale(request) // Negotiator等で判定
return NextResponse.redirect(
new URL(`/${locale}${pathname}`, request.url)
)
}
}
これで、ibis.prodouga.com/article-1 にアクセスした英語圏ユーザーは、自動的に /en/article-1 に飛ばされます。
3. GROQの魔術:内部リンクの自動解決
ここが最大の技術的ポイントです。
記事本文(Portable Text)の中に、別の記事へのリンク(Reference)が含まれているとします。
- 日本語記事A → リンク → 日本語記事B
これをそのまま翻訳して英語版を作ると、データ上はこうなります。
- 英語記事A → リンク → 日本語記事B (!?)
これでは、英語版を読んでいるユーザーがリンクをクリックすると、日本語の記事に飛ばされてしまいます。
そこで、GROQクエリを使って 「リンク先の記事の、現在の言語版」 を動的に解決します。
魔法のGROQクエリ
// 記事詳細を取得するクエリ
*[_type == "post" && slug.current == $slug && language == $lang][0] {
title,
body[] {
...,
// Portable Text内のリンク(markDefs)を処理
markDefs[] {
...,
_type == "internalLink" => {
"slug": @.reference->slug.current,
// ★ここ!参照先記事($lang版)のスラッグを引く
"translatedSlug": *[
_type == "post" &&
_id in [^._ref, "drafts." + ^._ref] // 参照ID
][0].translationId match *[
_type == "post" &&
language == $lang // 現在の言語
].translationId
}
}
}
}
※少し複雑に見えますが、要は 「リンク先の記事と同じ translationId を持ち、かつ言語が $lang である記事」 を探しに行っています。
これにより、Next.js側では何も考えずにリンクを生成するだけで、
「英語記事A → 英語記事B」 への遷移が自動的に成立します。
これが、私が提唱する 「雪だるま戦略(内部リンク網)」 を多言語でも維持する秘訣です。
4. 実稼働デモ:ibis (アイビス)
このアーキテクチャで実際に稼働しているのが、以下のメディアです。
サイト右上の言語切り替えスイッチ(🌐)を触ってみてください。
英語、ベトナム語、中国語などに瞬時に切り替わり、URLも /en/, /vi/ に変化します。
そして何より、記事内のリンクをクリックしても、その言語のまま遷移し続ける ことが確認できるはずです。
まとめ:多言語対応は「翻訳」ではなく「設計」だ
AI翻訳の進化により、テキストを多言語化すること自体のコストはゼロに近づいています。
しかし、それを管理する CMSのデータ設計 と フロントエンドのルーティング設計 が甘いと、運用はすぐに破綻します。
- Sanity: Document Level で管理し、翻訳IDで紐付ける。
- Next.js: Middlewareで振り分け、GROQでリンクを解決する。
このパターンを守れば、4ヶ国語だろうが10ヶ国語だろうが、管理コストを変えずにスケールさせることができます。
🔗 著者情報
山本 勇志 (Yushi Yamamoto)
株式会社プロドウガ代表 / フルスタックエンジニア
「AIは育ててこそ、最強の参謀になる」。
Next.js × Sanity × AI を駆使し、世界に通用するメディアアーキテクチャを構築中。
- X (Twitter): @UshiAiPro
- Company: PRODOUGA
- Media: ibis - Japan Living Guide
📢 開発・導入のご相談
「多言語サイトのSEO設計に悩んでいる」「Sanityでのi18n実装を手伝ってほしい」といったご相談を承っています。
XのDMまたはプロフィールリンクよりお気軽にご連絡ください。
