Hugo memiliki built-in support untuk multilingual websites (i18n) yang memungkinkan Anda membuat blog dalam beberapa bahasa dengan mudah. Tutorial ini akan membahas setup lengkap dari konfigurasi hingga deployment.
Konsep Multilingual di Hugo
Bagaimana Hugo Menangani Multilingual
Hugo menggunakan content directory structure untuk memisahkan bahasa:
content/
βββ id/ # Bahasa Indonesia
β βββ posts/
β β βββ hello.md
β βββ about.md
βββ en/ # Bahasa Inggris
β βββ posts/
β β βββ hello.md
β βββ about.md
βββ _index.md # Root content
Mode Translasi
Hugo mendukung dua mode:
- Single Language Mode – Website satu bahasa (default)
- Multilingual Mode – Website banyak bahasa
Step 1: Konfigurasi Multilingual
1.1 Setup di hugo.toml
# Konfigurasi default defaultContentLanguage = 'id' defaultContentLanguageInSubdir = falseDaftar bahasa
[languages] [languages.id] languageName = 'Bahasa Indonesia' weight = 1 contentDir = 'content/id' title = 'Blog Saya'
[languages.id.params] author = 'Nama Penulis' description = 'Blog tentang teknologi dan development' [[languages.id.menu.main]] name = 'Beranda' url = '/' weight = 10 [[languages.id.menu.main]] name = 'Artikel' url = '/posts/' weight = 20 [[languages.id.menu.main]] name = 'Tentang' url = '/about/' weight = 30[languages.en]
languageName = 'English'
weight = 2
contentDir = 'content/en'
title = 'My Blog'[languages.en.params] author = 'Author Name' description = 'Blog about technology and development' [[languages.en.menu.main]] name = 'Home' url = '/' weight = 10 [[languages.en.menu.main]] name = 'Posts' url = '/posts/' weight = 20 [[languages.en.menu.main]] name = 'About' url = '/about/' weight = 301.2 Struktur Content Directory
# Buat struktur folder mkdir -p content/id/posts content/en/posts mkdir -p content/id/categories content/en/categoriesCopy content existing (jika ada)
cp content/posts/ content/id/posts/ cp content/posts/ content/en/posts/
1.3 File i18n (Translation Strings)
Buat folder
i18n/dengan file untuk setiap bahasa:i18n/id.toml:
[read_more] other = "Baca selengkapnya"[written_by] other = "Ditulis oleh"
[last_modified] other = "Terakhir diperbarui"
[categories] other = "Kategori"
[tags] other = "Tags"
[reading_time] other = "{{ .Count }} menit membaca"
[share] other = "Bagikan"
[related_posts] other = "Artikel Terkait"
[next_post] other = "Artikel Selanjutnya"
[previous_post] other = "Artikel Sebelumnya"
[table_of_contents] other = "Daftar Isi"
i18n/en.toml:
[read_more] other = "Read more"[written_by] other = "Written by"
[last_modified] other = "Last modified"
[categories] other = "Categories"
[tags] other = "Tags"
[reading_time] other = "{{ .Count }} min read"
[share] other = "Share"
[related_posts] other = "Related Posts"
[next_post] other = "Next Post"
[previous_post] other = "Previous Post"
[table_of_contents] other = "Table of Contents"
Step 2: Membuat Konten Multilingual
2.1 Konten dengan Translation Key
content/id/posts/hello.md:
--- title: "Halo Dunia" slug: "hello-world" date: 2026-02-03T10:00:00+07:00 translationKey: "hello-world-post" description: "Post pertama dalam bahasa Indonesia" categories: ["Tutorial"] tags: ["hugo", "multilingual"] ---Ini adalah post pertama saya dalam bahasa Indonesia.
content/en/posts/hello.md:
--- title: "Hello World" slug: "hello-world" date: 2026-02-03T10:00:00+07:00 translationKey: "hello-world-post" description: "My first post in English" categories: ["Tutorial"] tags: ["hugo", "multilingual"] ---This is my first post in English.
Field penting:
–slug: Harus sama di semua bahasa untuk URL yang konsisten
–translationKey: Menghubungkan content yang sama di berbagai bahasa2.2 Root Content (_index.md)
content/id/_index.md:
--- title: "Beranda" description: "Selamat datang di blog saya" ---Selamat datang di blog personal saya!
content/en/_index.md:
--- title: "Home" description: "Welcome to my blog" ---Welcome to my personal blog!
2.3 Pages yang Tidak Diterjemahkan
--- title: "Privacy Policy" description: "Our privacy policy" translated: false # Skip translation ---This page is only available in English.
Step 3: Language Switcher
3.1 Simple Language Switcher
File
layouts/partials/language-switcher.html:{{ if .Site.IsMultiLingual }} <nav class="language-switcher" aria-label="Language switcher"> <ul class="flex gap-2"> {{ range .Site.Languages }} {{ if eq .Lang $.Site.Language.Lang }} <li> <span class="font-bold text-primary-600" aria-current="true"> {{ .LanguageName }} </span> </li> {{ else }} {{ range $.Translations }} {{ if eq .Lang .Lang }} <li> <a href="{{ .Permalink }}" class="text-gray-600 hover:text-primary-600" hreflang="{{ .Lang }}" lang="{{ .Lang }}"> {{ .LanguageName }} </a> </li> {{ end }} {{ end }} {{ end }} {{ end }} </ul> </nav> {{ end }}3.2 Advanced Language Switcher dengan Dropdown
{{ if .Site.IsMultiLingual }} <div class="relative group" x-data="{ open: false }"> <button @click="open = !open" class="flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-gray-100" aria-expanded="open"> <span>{{ .Site.Language.LanguageName }}</span> <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path> </svg> </button><div x-show="open" @click.away="open = false" class="absolute right-0 mt-2 w-48 bg-white rounded-lg shadow-lg py-2 z-50"> {{ range .Site.Languages }} {{ if ne .Lang $.Site.Language.Lang }} {{ range $.Translations }} {{ if eq .Lang .Lang }} <a href="{{ .Permalink }}" class="block px-4 py-2 text-sm hover:bg-gray-100" hreflang="{{ .Lang }}"> {{ .LanguageName }} </a> {{ end }} {{ end }} {{ end }} {{ end }} </div> </div> {{ end }}
3.3 Language Switcher di Header
Update
layouts/partials/header.html:<header class="bg-white shadow-sm"> <nav class="container mx-auto px-4"> <div class="flex justify-between items-center h-16"> <!-- Logo --> <a href="{{ .Site.Home.RelPermalink }}" class="text-xl font-bold"> {{ .Site.Title }} </a><!-- Navigation --> <div class="flex items-center gap-6"> {{ range .Site.Menus.main }} <a href="{{ .URL }}" class="text-gray-600 hover:text-gray-900"> {{ .Name }} </a> {{ end }} <!-- Language Switcher --> {{ partial "language-switcher.html" . }} </div> </div></nav>
</header>Step 4: SEO untuk Multilingual
4.1 Hreflang Tags
File
layouts/partials/head.html:<!-- Hreflang untuk SEO --> {{ if .Site.IsMultiLingual }} {{ range .AllTranslations }} <link rel="alternate" hreflang="{{ .Lang }}" href="{{ .Permalink }}" /> {{ end }} <link rel="alternate" hreflang="x-default" href="{{ .Site.Home.Permalink }}" /> {{ end }}4.2 HTML Lang Attribute
File
layouts/_default/baseof.html:<!DOCTYPE html> <html lang="{{ .Site.Language.Lang }}" dir="{{ .Site.Language.LanguageDirection | default "ltr" }}"> <head> {{ partial "head.html" . }} </head> <body> {{ block "main" . }}{{ end }} </body> </html>4.3 Open Graph Meta Tags
<!-- Open Graph locale --> <meta property="og:locale" content="{{ .Site.Language.Lang }}_{{ .Site.Language.Lang | upper }}">{{ if .Site.IsMultiLingual }} {{ range .Translations }} <meta property="og:locale:alternate" content="{{ .Lang }}_{{ .Lang | upper }}"> {{ end }} {{ end }}
4.4 Sitemap.xml Multilingual
Buat
layouts/sitemap.xml:{{ printf "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?>" | safeHTML }} <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml"> {{ range .Data.Pages }} {{ if not .Draft }} <url> <loc>{{ .Permalink }}</loc> <lastmod>{{ .Lastmod.Format "2006-01-02T15:04:05-07:00" }}</lastmod> {{ if .IsTranslated }} {{ range .Translations }} <xhtml:link rel="alternate" hreflang="{{ .Lang }}" href="{{ .Permalink }}" /> {{ end }} {{ end }} <xhtml:link rel="alternate" hreflang="{{ .Lang }}" href="{{ .Permalink }}" /> </url> {{ end }} {{ end }} </urlset>Step 5: Template Modifikasi untuk i18n
5.1 Menggunakan i18n Function
<!-- Menggunakan translation strings --> <h1>{{ i18n "table_of_contents" }}</h1><!-- Dengan variabel --> <p>{{ i18n "reading_time" . }}</p>
<!-- Default value jika translation tidak ada --> <p>{{ i18n "custom_key" | default "Default text" }}</p>
5.2 List Template dengan i18n
{{ define "main" }} <div class="container mx-auto px-4 py-8"> <h1 class="text-3xl font-bold mb-8">{{ .Title }}</h1><div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> {{ range .Pages }} <article class="bg-white rounded-lg shadow-md p-6"> <h2 class="text-xl font-semibold mb-2"> <a href="{{ .RelPermalink }}">{{ .Title }}</a> </h2>
<div class="text-sm text-gray-600 mb-4"> <time datetime="{{ .Date.Format "2006-01-02" }}"> {{ .Date.Format "January 2, 2006" }} </time> <span>β’</span> <span>{{ i18n "reading_time" . }}</span> </div> <p class="text-gray-700 mb-4">{{ .Description | default .Summary | truncate 150 }}</p> <a href="{{ .RelPermalink }}" class="text-primary-600 hover:underline"> {{ i18n "read_more" }} β </a> </article> {{ end }}</div>
</div>
{{ end }}5.3 Navigation dengan i18n
<nav class="main-nav"> {{ range .Site.Menus.main }} <a href="{{ .URL }}" class="nav-link"> {{ .Name }} </a> {{ end }} </nav>Step 6: URL Structure Options
6.1 Subdirectory Structure (Recommended)
# hugo.toml [languages] [languages.id] baseURL = 'https://example.com' contentDir = 'content/id'[languages.en] baseURL = 'https://example.com/en' contentDir = 'content/en'
URLs:
– Indonesia:https://example.com/posts/hello-world/
– English:https://example.com/en/posts/hello-world/6.2 Subdomain Structure
[languages] [languages.id] baseURL = 'https://example.com' contentDir = 'content/id'[languages.en] baseURL = 'https://en.example.com' contentDir = 'content/en'
URLs:
– Indonesia:https://example.com/posts/hello-world/
– English:https://en.example.com/posts/hello-world/6.3 Domain Terpisah
[languages] [languages.id] baseURL = 'https://example.id' contentDir = 'content/id'[languages.en] baseURL = 'https://example.com' contentDir = 'content/en'
Step 7: RSS Feed Multilingual
File
layouts/_default/rss.xml:{{ printf "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?>" | safeHTML }} <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"> <channel> <title>{{ .Site.Title }} - {{ .Site.Language.LanguageName }}</title> <link>{{ .Permalink }}</link> <description>{{ .Site.Params.description }}</description> <language>{{ .Site.Language.Lang }}</language> <lastBuildDate>{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 MST" }}</lastBuildDate> <atom:link href="{{ .Permalink }}" rel="self" type="application/rss+xml" />{{ range first 20 .Site.RegularPages }} <item> <title>{{ .Title }}</title> <link>{{ .Permalink }}</link> <pubDate>{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 MST" }}</pubDate> <guid>{{ .Permalink }}</guid> <description>{{ .Description | default .Summary | html }}</description> </item> {{ end }}</channel>
</rss>Step 8: Testing dan Development
8.1 Jalankan Server Multilingual
# Build semua bahasa hugo server -DBuild bahasa spesifik
hugo server -D --language id hugo server -D --language en
Build dengan baseURL
hugo server -D --baseURL http://localhost:1313
8.2 Build untuk Production
# Build semua bahasa hugo --minifyHasil build akan ada di public/
public/id/ untuk Indonesia
public/en/ untuk English
8.3 Verify Hreflang
Gunakan tools untuk verify:
– Google Search Console
– Screaming Frog
– hreflang.orgStep 9: Deployment
9.1 Deploy ke Cloudflare Pages
# .github/workflows/deploy.yml name: Deploy Multilingual Hugo on: [push] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: peaceiris/actions-hugo@v2 with: hugo-version: 'latest' extended: true - run: hugo --minify - uses: peaceiris/actions-gh-pages@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./public9.2 Redirects untuk Root Path
File
static/_redirects(untuk Netlify) ataustatic/_headers:# Redirect root ke default language / /id/ 302 Language=id / /en/ 302 Language=enTroubleshooting
Issue: “language not found”
Solusi:
# Pastikan contentDir benar [languages.id] contentDir = 'content/id' # Harus ada folder iniIssue: “404 on translated pages”
Solusi:
– Pastikanslugsama di semua bahasa
– CektranslationKeyjika digunakan
– Verify file exists di content directoryIssue: “language switcher not showing”
Solusi:
{{ if .Site.IsMultiLingual }} <!-- Language switcher di sini --> {{ end }}Issue: “hreflang errors”
Solusi:
– Pastikan semua halaman punya translations
– GunakantranslationKeyuntuk menghubungkan
– Check dengan Google Search ConsoleBest Practices
1. Content Strategy
- Translate semua content atau berikan alternative
- Gunakan translationKey untuk maintain consistency
- Keep URL structure consistent di semua bahasa
2. SEO
- Implement hreflang tags dengan benar
- Gunakan x-default untuk fallback
- Submit sitemap per bahasa ke Google Search Console
- Use canonical URLs yang tepat
3. UX
- Tampilkan language switcher di tempat yang mudah ditemukan
- Use flags with caution (bisa politik, better use language names)
- Preserve user choice dengan localStorage atau cookies
4. Performance
- Lazy load translations jika site besar
- Use CDN untuk static assets
- Optimize images per bahasa jika berbeda
Kesimpulan
Multilingual setup di Hugo sangat powerful dengan:
β
Built-in support: Tidak perlu plugin tambahan
β
SEO-friendly: Hreflang, sitemap, meta tags
β
Flexible: Directory structure atau subdomain
β
Maintainable: Translation keys dan i18n strings
β
Fast: Static generation untuk semua bahasa
Dengan setup yang benar, Anda bisa mengelola website bilingual dengan mudah dan SEO yang optimal.
Checklist Multilingual Setup
- [ ] Konfigurasi languages di hugo.toml
- [ ] Setup content directories per bahasa
- [ ] Create i18n translation files
- [ ] Add translationKey ke content
- [ ] Implement language switcher
- [ ] Add hreflang meta tags
- [ ] Create multilingual sitemap
- [ ] Test semua URLs
- [ ] Setup redirects jika perlu
- [ ] Verify dengan Google Search Console
Ditulis oleh
Hendra Wijaya