I had come to want my Japanese-language tech blog to be readable from the English-speaking world too.
An easy way to do this is to run Google’s translation widget on the page and switch to an English display at the same URL. On another site that had been enough.
This time, though, the goal was not “convenience for visitors already here” but discovery and indexing from English queries. I judged that a widget could not satisfy that.
A widget does not get indexed
Google Website Translator returns the published HTML as-is in Japanese and swaps the DOM to English in the browser. The URL does not change.
What Google uses to judge language is the body of the HTML the crawler fetches. hreflang and html lang are not the primary grounds. A display switched to English later via a cookie barely exists as an English page.
hreflang itself is a mechanism for conveying “correspondence between separate URLs”, so putting JA/EN on the same URL cannot be registered as an English version.
| What I want to do | Direction |
|---|---|
| A reader who reaches the Japanese page reads it in English | A widget or browser translation is enough |
| Get traffic from English search | Serve crawler-readable English HTML at a separate URL |
The free Website Translator ended service in 2019. Even where it still works, it is unsupported.
Publishing large volumes of unreviewed machine translation at separate URLs is also close to Google’s Scaled content abuse (translation is one of the listed examples of automated conversion). I decided to publish the English version as a translation, keep Japanese as the source of truth, and not translate the older, more locally scoped memos.
Chrome already translates pages on its own. Adding a widget on top does not increase new inflow from English SERPs.
Serving a separate URL with Hugo’s multilingual support
The source of truth stays Japanese, directly under the root. English goes under /en/.
I did not want to move the existing article files into separate directories, so I did not use a per-language contentDir. I made Hugo’s filename scheme handle it instead.
- Japanese:
content/post/example.md→https://example.com/example/ - English:
content/post/example.en.md→https://example.com/en/example/
For this site the actual domain is https://scribble.washo3.com/ .
The core of hugo.toml is as follows.
DefaultContentLanguage = "ja"
defaultContentLanguageInSubdir = false
[languages.ja]
locale = "ja-JP"
label = "日本語"
weight = 1
[languages.en]
locale = "en-US"
label = "English"
weight = 2It used to stay at DefaultContentLanguage = "en". The body text was Japanese, yet html lang was English. Before going multilingual, I aligned the source language code to ja.
Because defaultContentLanguageInSubdir = false, Japanese does not get a /ja/ prefix. Only English becomes a subdirectory.
hreflang
Only for pages that have an English version, I output the following in the HTML head.
hreflang="ja"andhreflang="en"(including self-reference)hreflang="x-default"is Japanese (the source of truth)
I do not declare an English URL that does not actually exist. Attaching hreflang to a page with no corresponding translation breaks the bidirectional link, and Google tends to ignore it.
canonical points each language at itself. English is not pointed back at Japanese.
I also do not force a redirect based on IP or Accept-Language. That would leave Googlebot able to see only one of the two languages.
The implementation is just looping over .AllTranslations. Since it includes self-reference, it satisfies as-is the condition Google asks for: “list yourself and the other languages”.
{{- if .IsTranslated }}
{{- range .AllTranslations }}
<link rel="alternate" hreflang="{{ .Lang }}" href="{{ .Permalink }}" />
{{- end }}
{{- end }}Sitemap
On a multilingual site with defaultContentLanguageInSubdir = false, Hugo’s default sitemapindex can end up listing a /ja/sitemap.xml that does not exist.
I made the root /sitemap.xml not an index but a single urlset listing both the Japanese and English URLs together.
Google only needs to receive hreflang through one of HTML, HTTP headers, or the sitemap. Since I already output it in HTML, I do not duplicate it in the sitemap.
Taxonomies (categories / tags) that are excluded from search results are also excluded from the sitemap.
Splitting llms.txt by language
This site outputs llms.txt for LLMs. With root = true in outputFormats, the Japanese and English homepages would fight over the same /llms.txt.
Setting root = false splits it so Japanese gets /llms.txt and English gets /en/llms.txt. On the template side, the description switches on .Lang.
RSS is also emitted per language, so if I want the English version indexed, the per-language feed is already usable as-is.
Language switcher
The theme (beautifulhugo) already has a link for multilingual use. An <a href> to the other language only appears when hugo.IsMultilingual is true and that page actually has a translation.
I do not use a button that only switches the display via a cookie. That is because a crawler cannot follow it to the English URL.
For UI text outside the nav (related-posts heading, search placeholder, promotional text, and so on), I overrode the theme’s i18n files on the site side. This was to keep fixed Japanese text from remaining on English pages.
How much to translate
I avoided machine-translating every article and publishing all of it. Placing a large number of low-value English pages carries more risk of being judged as spam.
| Target | Decision |
|---|---|
Current articles directly under content/post/ | Add an English version. 129 articles this time |
| Old article directories from 2011–2018 | Not translated (264 articles). Many are overly local memos, and no hreflang is output for them either |
| Fixed pages such as About | Add an English version |
For the English title I do not drag along the Japanese word order; I rewrite it with terms likely to be searched in English-speaking regions. subtitle was already in English, so I reused or improved it.
I copy date from the Japanese version without changing a single character. If it drifts, the ordering of articles ends up mismatched between the two languages.
Commands, logs, paths, URLs, and version numbers are not translated. Only Japanese comments inside code blocks are turned into English.
Hugo’s ref shortcode only searches within the current language. When linking to an old article that has no English version, the build fails with REF_NOT_FOUND unless the language is pinned to Japanese.
{{< ref path="old-post.md" lang="ja" >}}English after fixing the Japanese
English is a translation, and I do not add information that is not in the Japanese version.
If I fix the Japanese before publishing, rebuilding the English from the finalized Japanese causes fewer mismatches than patching just a few lines of the English. Section additions, deletions, and reordering tend to linger on the English side otherwise.
Points to check before building:
- Every current article has a corresponding
.en.md datematches between Japanese and English- The section-divider marks (
***and---) are aligned - The English page’s HTML has
hreflangin both directions
hugo --quiet && echo "build ok"
hugo server -DThe URL shapes to check are as follows.
- Japanese:
/<slug>/ - English:
/en/<slug>/
Summary
If the goal is search traffic from abroad, serve crawler-readable English HTML at a separate URL instead of a client-side translation widget.
The shape I settled on this time is as follows.
| Item | Details |
|---|---|
| Source of truth | Japanese, directly under the root |
| English version | /en/ and *.en.md |
| hreflang | Bidirectional, x-default is Japanese |
| Translation scope | Only current articles get English versions; old archives stay Japanese-only |
| Language switching | Links only. No cookies or automatic redirects |
With static hosting like Cloudflare Pages, emitting both languages at build time means no extra origin logic is needed.