Hugoの多言語機能で日本語ブログに英語版を別URLで公開した

Publishing an English Version of a Japanese Blog at a Separate URL with Hugo's Multilingual Support

* 本ページはプロモーションが含まれています

日本語で書いてきた技術ブログを、英語圏からも読めるようにしたくなった。
手軽な方法としては、ページ上でGoogleの翻訳ウィジェットを動かし、同じURLのまま英語表示へ切り替えるやり方がある。別サイトではそれで足りた。

ただし今回の目的は「来た人の便宜」ではなく、英語クエリでの発見とインデックスである。ウィジェットではそこが満たせないと判断した。


ウィジェットではインデックスされない

Google Website Translatorは、公開HTMLを日本語のまま返し、ブラウザ上でDOMを英語に差し替える。URLは変わらない。
Googleが言語を判定するのは、クローラが取得したHTMLの本文である。hreflanghtml langが主根拠ではない。Cookieで後から英語にした表示は、英語ページとしてほぼ存在しない。

hreflang自体も「別URL同士の対応」を伝える仕組みなので、同じURLにJA/ENを載せても英語版としては登録できない。

やりたいこと向き
日本語ページに来た読者が英語で読むウィジェットやブラウザ翻訳で足りる
英語検索から流入するクローラが読める英語HTMLを別URLで出す

無料のWebsite Translatorは2019年に提供終了している。動いていてもサポート対象外である。

未レビューの機械翻訳を大量に別URLで公開すると、Googleの Scaled content abuse(自動変換の例に翻訳がある)にも近い。英語版は翻訳として公開するが、正本は日本語に置き、古いローカル寄りのメモまでは訳さないことにした。

Chromeはもともとページを翻訳する。ウィジェットを足しても、英語SERPからの新規流入は増えない。


Hugoの多言語で別URLを出す

正本は日本語のままルート直下。英語は/en/配下。
既存の記事ファイルをディレクトリごと移したくなかったので、言語ごとのcontentDirは使わない。Hugoのファイル名方式で対応させる。

  • 日本語: content/post/example.mdhttps://example.com/example/
  • 英語: content/post/example.en.mdhttps://example.com/en/example/

このサイトでは実際のドメインがhttps://scribble.washo3.com/ である。

hugo.tomlの核は次のとおり。

DefaultContentLanguage = "ja"
defaultContentLanguageInSubdir = false

[languages.ja]
  locale = "ja-JP"
  label = "日本語"
  weight = 1

[languages.en]
  locale = "en-US"
  label = "English"
  weight = 2

以前はDefaultContentLanguage = "en"のままだった。本文は日本語なのにhtml langが英語になっていた。多言語化する前に、正本の言語コードをjaに合わせた。

defaultContentLanguageInSubdir = falseなので、日本語は/ja/を付けない。英語だけサブディレクトリになる。


hreflang

英語版があるページだけ、次をHTMLのheadに出す。

  • hreflang="ja"hreflang="en"(自己参照を含む)
  • hreflang="x-default"は日本語(正本)

実在しない英語URLは宣言しない。対応する翻訳が無いページにhreflangを付けると、双方向リンクが成立せずGoogle側で無視されやすい。

canonicalは各言語が自分自身を指す。英語を日本語へ向けない。

IPやAccept-Languageでの強制リダイレクトもしない。Googlebotが一方の言語しか見られなくなる。

実装は.AllTranslationsを回すだけである。自己参照も含まれるので、Googleが求める「自分自身と他言語を列挙する」条件はそのまま満たせる。

{{- if .IsTranslated }}
  {{- range .AllTranslations }}
  <link rel="alternate" hreflang="{{ .Lang }}" href="{{ .Permalink }}" />
  {{- end }}
{{- end }}

サイトマップ

多言語サイトでdefaultContentLanguageInSubdir = falseにすると、Hugo既定のsitemapindexが存在しない/ja/sitemap.xmlを列挙することがある。

ルートの/sitemap.xmlは索引ではなく、日英のURLを並べた1つのurlsetとして出した。
Googleはhreflangを HTML・HTTPヘッダ・サイトマップのどれか1つで受け取れば足りる。HTML側で出しているので、サイトマップには重複して書かない。

検索結果から外しているtaxonomy(categories / tags)は、サイトマップからも除く。


llms.txtを言語で分ける

このサイトはLLM向けにllms.txtを出している。outputFormatsroot = trueだと、日英のホームが同じ/llms.txtを奪い合う。

root = falseにすると、日本語は/llms.txt、英語は/en/llms.txtに分かれる。テンプレート側は.Langで説明文を切り替える。

RSSも言語ごとに出るので、英語版をインデックスさせるなら言語別のフィードもそのまま使える。


言語スイッチャー

テーマ(beautifulhugo)には多言語用のリンクがもともとある。hugo.IsMultilingualかつそのページに翻訳があるときだけ、別言語への<a href>が出る。

Cookieで表示だけ切り替えるボタンは使わない。クローラが英語URLを辿れないからである。

ナビ以外のUI文言(関連記事見出し、検索プレースホルダ、プロモーション表示など)は、テーマのi18nファイルをサイト側で上書きした。英語ページに日本語の固定文言が残らないようにするためである。


どこまで訳すか

全記事を機械翻訳して載せるのは避けた。価値の薄い英語ページを大量に置く方が、スパム判定のリスクがある。

対象判断
content/post/直下の現行記事英語版を付ける。今回は129本
2011〜2018年の旧記事ディレクトリ訳さない(264本)。ローカルすぎるメモが多く、hreflangも出さない
Aboutなどの固定ページ英語版を付ける

英語titleは日本語の語順を引きずらず、英語圏で検索されそうな語で書き直す。subtitleはもともと英語だったので流用または改善した。

dateは日本語版と1文字も変えない。ずれると両言語で並び順が食い違う。

コマンド、ログ、パス、URL、バージョン番号は訳さない。コードブロック内の日本語コメントだけ英語にする。

Hugoのrefショートコードは、現在の言語の中だけを探す。英語版が無い旧記事へ張るときは、言語を日本語に固定しないとビルドがREF_NOT_FOUNDで落ちる。

{{< ref path="old-post.md" lang="ja" >}}

日本語を直したあとの英語

英語は翻訳であり、日本語に無い情報を足さない。

公開前に日本語を直したら、英語を数行だけ直すより、確定した日本語から英語を作り直す方が食い違いが少ない。章の追加・削除・並べ替えが英語側に残りやすい。

ビルド前に見る点は次のとおり。

  • 各現行記事に対応する.en.mdがある
  • dateが日英で一致している
  • セクション区切り(***---)が揃っている
  • 英語ページのHTMLにhreflangが双方向で入っている
hugo --quiet && echo "build ok"
hugo server -D

確認するURLの形は次のとおり。

  • 日本語: /<slug>/
  • 英語: /en/<slug>/

まとめ

海外からの検索流入が目的なら、クライアント側の翻訳ウィジェットではなく、クローラが読める英語HTMLを別URLで出す。

今回採用した形は次のとおり。

項目内容
正本日本語。ルート直下
英語版/en/*.en.md
hreflang双方向、x-defaultは日本語
翻訳範囲現行記事だけ英語化し、古いアーカイブは日本語のまま
言語切替リンクのみ。Cookieや自動リダイレクトは使わない

Cloudflare Pagesのような静的ホスティングなら、ビルド時に両言語を出せば追加のオリジンロジックは不要である。

関連項目