Hugo で作ったこのブログを多言語化しました。今のところ英語だけです。
なぜ多言語化したのか
会社で OSS 活動をしていて、英訳した記事を dev.to や Medium などにクロスポストする機会がありました。最近は個人でも、なんとなく英訳してクロスポストしています。
例えば [2025-08-15-1] の英訳版は以下の 3 サイトにクロスポストしました。
ただ、せっかくこのブログ 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.toml
と i18n/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