Hugo で作ったこのブログを多言語化しました。今のところ英語だけです。

なぜ多言語化したのか

会社で OSS 活動をしていて、英訳した記事を dev.to や Medium などにクロスポストする機会がありました。最近は個人でも、なんとなく英訳してクロスポストしています。

例えば [2025-08-15-1] の英訳版は以下の 3 サイトにクロスポストしました。

  • dev.to - 英訳のオリジナルとした
  • Hashnode - dev.to の記事を canonical に設定
  • Medium - dev.to の記事を canonical に設定

ただ、せっかくこのブログ masutaka.net を運営しているので、ここに英訳記事のオリジナルを置いて、上記 3 サイトへクロスポストする形にしようと思いました。canonical URL を自分のブログに向けることで、SEO 的にも良さそうです。

Hugo の多言語化機能

Hugo には標準で多言語化機能があります。設定も簡単で、基本は config.toml に以下のような設定をするだけです。

defaultContentLanguage = "ja"

[languages.ja]
  weight = 1
  languageName = "日本語"
  title = "マスタカの ChangeLog メモ"

[languages.en]
  weight = 2
  languageName = "English"
  title = "Masutaka's ChangeLog Memo"

ファイル名に .en.md を付けた記事が英語版になります。

  • 日本語版: content/posts/2025-08-15-1.md
  • 英語版: content/posts/2025-08-15-1.en.md

この設定により、以下のような URL でアクセスできるようになります。

  • 日本語版: https://masutaka.net/2025-08-15-1/
  • 英語版: https://masutaka.net/en/2025-08-15-1/

多言語化対応方針

多言語化にあたって、以下の方針で進めました。

1. 既存 URL は一切変更しない

最も重要な方針は、既存の URL を一切変更しないことです。20 年以上運営しているブログなので、URL が変わると多くのリンクが切れてしまいます。

そのため、日本語版はそのままで、英語版だけ /en/ プレフィックスを付ける形にしました。

  • /index.html - ほぼ変更なし、/en/index.html が追加される
  • /index.xml - 変更なし、/en/index.xml が追加される
  • /sitemap.xml - /ja/sitemap.xml/en/sitemap.xml への参照に変更
  • /llms.txt, /llms-full.txt - 変更なし、/en/llms.txt, /en/llms-full.txt が追加される

👉 前述の defaultContentLanguage, [languages.ja], [languages.en] の設定でこのような挙動になります。

2. 右上の英語版のメニューは最小限に

Device, History, About ページは日本語特有の内容が多く、英訳のメリットがあまりないため、今回は見送りました。

実際行った変更

実際に行った変更の概要です。

1. config.toml の変更

言語設定の追加と、各言語用のメニュー設定を追加しました。

👉 「付録 」に今回の差分と、変更後の config.toml を添付してあります。

2. 固定ページの英語版作成

  • content/archives.en.md - アーカイブページ
  • content/search.en.md - 検索ページ
  • content/privacy.en.md - プライバシーポリシー

⚠️ 最初は *.en.md から *.md へのシンボリックリンクを張って対応しようとしましたが、Hugo は参照してくれませんでした。

3. 自作ショートコードの変更

自作のショートコード post で引数 lang を受け取れるようにしました。

layouts/shortcodes/post.html:

{{- $id := .Get "id" | default (.Get 0) -}}
{{- $lang := .Get "lang" | default .Page.Lang -}}
{{- $url := relref . (dict "path" $id "lang" $lang) -}}
{{- $title := .Get "title" -}}

<a href="{{ $url }}">{{ with $title }}{{ $title }}{{ else }}[{{ $id }}]{{ end }}</a>

使用例です。

<!-- 引数に記事の id を指定すれば、現在の言語に応じたリンクになる -->
{{< post "2025-09-23-1" >}}
<!-- -> 現在の言語が English の場合は /en/2025-09-23-1/ へのリンクになる -->

<!-- 引数 lang を追加すると、別の言語への明示的なリンクになる -->
{{< post id="2025-09-23-1" lang="ja" >}}
<!-- -> 現在の言語が English の場合は /2025-09-23-1/ へのリンクになる -->

4. 自作パーシャルの翻訳対応

今回は layouts/partials/*.html で一部翻訳が必要だったため、i18n/ja.tomli18n/en.toml を作成しました。

以下のように使用します。

<a href="https://example.com/" target="_blank" rel="noopener">{{ i18n "sendMessage" }}</a>

i18n/ja.toml:

sendMessage = "メッセージ送信"

i18n/en.toml:

sendMessage = "Send Message"

まとめ

Hugo で作ったこのブログを多言語化しました。想定していたよりもきれいに対応できました 👍

  • 既存の URL は一切変更せず、影響範囲を /en/ 配下に限定できた
  • Hugo の標準機能だけで実現でき、カスタマイズは最小限で済んだ
  • 英語版メニューを最小限にすることで、メンテナンスコストを抑えた
  • sitemap.xml や llms.txt も自動的に多言語対応された

今後は新しく書いた記事を必要に応じて英訳し、 https://masutaka.net/en/ をオリジナルとして、dev.to、Hashnode、Medium にクロスポストしていこうと思います。

英語圏の読者がいるかは怪しいところですが、自己満足はしています。最近は AI による英訳が割と正確なので、そこまで大変ではないとも思っています 😎

参考情報

付録

config.toml の変更内容

Show details
diff --git a/config.toml b/config.toml
index 0830be8b..a6bae12c 100644
--- a/config.toml
+++ b/config.toml
@@ -7,7 +7,6 @@ googleAnalytics = "G-K28CQCC064"
 hasCJKLanguage = true
 languageCode = "ja"
 theme = "papermod"
-title = "マスタカの ChangeLog メモ"
 
 [permalinks]
   posts = "/:filename"
@@ -27,14 +26,12 @@ title = "マスタカの ChangeLog メモ"
   isPlainText = true
   mediaType = "text/plain"
   rel = "alternate"
-  root = true
 
 [outputFormats.llmsfull]
   baseName = "llms-full"
   isPlainText = true
   mediaType = "text/plain"
   rel = "alternate"
-  root = true
 
 #
 # papermod configuration
@@ -51,14 +48,9 @@ title = "マスタカの ChangeLog メモ"
   author = "masutaka"
   comments = true
   defaultTheme = "auto"
-  description = "マスタカの変更履歴が記録されていくブログです。"
   showtoc = true
   tocopen = true
 
-[params.homeInfoParams]
-  Title = "マスタカネット"
-  Content = "マスタカの変更履歴が記録されていくブログです。"
-
 [params.assets]
   theme_color = "#ffffff"
   msapplication_TileColor = "#da532c"
@@ -79,46 +71,81 @@ title = "マスタカの ChangeLog メモ"
 [[params.socialIcons]]
   name =  "GitHub"
   url = "https://github.com/masutaka"
-[[params.socialIcons]]
-  name =  "Dev"
-  url = "https://dev.to/masutaka"
-[[params.socialIcons]]
-  name =  "Hashnode"
-  url = "https://masutaka.hashnode.dev/"
-[[params.socialIcons]]
-  name =  "Medium"
-  url = "https://medium.com/@masutaka"
 [[params.socialIcons]]
   name =  "RSS"
   url = "/index.xml"
 
-[[menu.main]]
+#
+# Multilingual
+#
+
+[languages.ja]
+  weight = 1
+  languageName = "日本語"
+  title = "マスタカの ChangeLog メモ"
+
+[languages.ja.params]
+  description = "マスタカの変更履歴が記録されていくブログです。"
+
+[languages.ja.params.homeInfoParams]
+  Title = "マスタカネット"
+  Content = "マスタカの変更履歴が記録されていくブログです。"
+
+[[languages.ja.menu.main]]
   identifier = "archives"
   name = "Archive"
   url = "/archives/"
   weight = 1
-[[menu.main]]
+[[languages.ja.menu.main]]
   identifier = "tags"
   name = "Tags"
   url = "/tags/"
   weight = 2
-[[menu.main]]
+[[languages.ja.menu.main]]
   identifier = "search"
   name = "Search"
   url = "/search/"
   weight = 3
-[[menu.main]]
+[[languages.ja.menu.main]]
   identifier = "device"
   name = "Device"
   url = "/device/"
   weight = 4
-[[menu.main]]
+[[languages.ja.menu.main]]
   identifier = "history"
   name = "History"
   url = "/history/"
   weight = 5
-[[menu.main]]
+[[languages.ja.menu.main]]
   identifier = "about"
   name = "About"
   url = "/about/"
   weight = 6
+
+[languages.en]
+  weight = 2
+  languageName = "English"
+  title = "Masutaka's ChangeLog Memo"
+
+[languages.en.params]
+  description = "This is a blog that records Masutaka's change history."
+
+[languages.en.params.homeInfoParams]
+  Title = "Masutaka Net"
+  Content = "This is a blog that records Masutaka's change history."
+
+[[languages.en.menu.main]]
+  identifier = "archives"
+  name = "Archive"
+  url = "/en/archives/"
+  weight = 1
+[[languages.en.menu.main]]
+  identifier = "tags"
+  name = "Tags"
+  url = "/en/tags/"
+  weight = 2
+[[languages.en.menu.main]]
+  identifier = "search"
+  name = "Search"
+  url = "/en/search/"
+  weight = 3

多言語化後の config.toml

Show details
baseURL = "https://masutaka.net/"
defaultContentLanguage = "ja"
disablePathToLower = true
enableEmoji = true
enableRobotsTXT = true
googleAnalytics = "G-K28CQCC064"
hasCJKLanguage = true
languageCode = "ja"
theme = "papermod"

[permalinks]
  posts = "/:filename"

[taxonomies]
  tag = "tags"

[markup.goldmark.renderer]
  hardWraps = true
  unsafe = true

[outputs]
  home = ["html", "rss", "llms", "llmsfull"]

[outputFormats.llms]
  baseName = "llms"
  isPlainText = true
  mediaType = "text/plain"
  rel = "alternate"

[outputFormats.llmsfull]
  baseName = "llms-full"
  isPlainText = true
  mediaType = "text/plain"
  rel = "alternate"

#
# papermod configuration
#

[params]
  AmazonJpAffiliateID = "masutaka04-22"
  DateFormat = "2006-01-02 (Mon)"
  ShowAllPagesInArchive = true
  ShowCodeCopyButtons = true
  ShowFullTextinRSS = true
  ShowPageNums = true
  ShowPostNavLinks = true
  author = "masutaka"
  comments = true
  defaultTheme = "auto"
  showtoc = true
  tocopen = true

[params.assets]
  theme_color = "#ffffff"
  msapplication_TileColor = "#da532c"

[params.social]
  fediverse_creator = "@[email protected]"
  twitter = "@masutaka"

[[params.socialIcons]]
  name =  "Mastodon"
  url = "https://mstdn.love/@masutaka"
[[params.socialIcons]]
  name =  "Bluesky"
  url = "https://bsky.app/profile/masutaka.net"
[[params.socialIcons]]
  name =  "Twitter"
  url = "https://twitter.com/masutaka"
[[params.socialIcons]]
  name =  "GitHub"
  url = "https://github.com/masutaka"
[[params.socialIcons]]
  name =  "RSS"
  url = "/index.xml"

#
# Multilingual
#

[languages.ja]
  weight = 1
  languageName = "日本語"
  title = "マスタカの ChangeLog メモ"

[languages.ja.params]
  description = "マスタカの変更履歴が記録されていくブログです。"

[languages.ja.params.homeInfoParams]
  Title = "マスタカネット"
  Content = "マスタカの変更履歴が記録されていくブログです。"

[[languages.ja.menu.main]]
  identifier = "archives"
  name = "Archive"
  url = "/archives/"
  weight = 1
[[languages.ja.menu.main]]
  identifier = "tags"
  name = "Tags"
  url = "/tags/"
  weight = 2
[[languages.ja.menu.main]]
  identifier = "search"
  name = "Search"
  url = "/search/"
  weight = 3
[[languages.ja.menu.main]]
  identifier = "device"
  name = "Device"
  url = "/device/"
  weight = 4
[[languages.ja.menu.main]]
  identifier = "history"
  name = "History"
  url = "/history/"
  weight = 5
[[languages.ja.menu.main]]
  identifier = "about"
  name = "About"
  url = "/about/"
  weight = 6

[languages.en]
  weight = 2
  languageName = "English"
  title = "Masutaka's ChangeLog Memo"

[languages.en.params]
  description = "This is a blog that records Masutaka's change history."

[languages.en.params.homeInfoParams]
  Title = "Masutaka Net"
  Content = "This is a blog that records Masutaka's change history."

[[languages.en.menu.main]]
  identifier = "archives"
  name = "Archive"
  url = "/en/archives/"
  weight = 1
[[languages.en.menu.main]]
  identifier = "tags"
  name = "Tags"
  url = "/en/tags/"
  weight = 2
[[languages.en.menu.main]]
  identifier = "search"
  name = "Search"
  url = "/en/search/"
  weight = 3