Tutorial Membuat Blog Multilingual dengan Hugo i18n
Tutorial Membuat Blog Multilingual dengan Hugo i18n
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 = false
# Daftar 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 = 30
1.2 Struktur Content Directory
# Buat struktur folder
mkdir -p content/id/posts content/en/posts
mkdir -p content/id/categories content/en/categories
# Copy 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 konsistentranslationKey: Menghubungkan content yang sama di berbagai bahasa
2.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 -D
# Build 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 --minify
# Hasil 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.org
Step 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: ./public
9.2 Redirects untuk Root Path
File static/_redirects (untuk Netlify) atau static/_headers:
# Redirect root ke default language
/ /id/ 302 Language=id
/ /en/ 302 Language=en
Troubleshooting
Issue: “language not found”
Solusi:
# Pastikan contentDir benar
[languages.id]
contentDir = 'content/id' # Harus ada folder ini
Issue: “404 on translated pages”
Solusi:
- Pastikan
slugsama di semua bahasa - Cek
translationKeyjika digunakan - Verify file exists di content directory
Issue: “language switcher not showing”
Solusi:
{{ if .Site.IsMultiLingual }}
<!-- Language switcher di sini -->
{{ end }}
Issue: “hreflang errors”
Solusi:
- Pastikan semua halaman punya translations
- Gunakan
translationKeyuntuk menghubungkan - Check dengan Google Search Console
Best 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
Artikel Terkait
Link Postingan: https://www.tirinfo.com/tutorial-blog-multilingual-hugo-i18n/