A representative image showing the configuration for applying multilingual support and SEO settings such as hreflang and canonical to a Hugo blog

Goal

Apply multilingual support and SEO optimization to a Hugo blog to maximize search engine exposure and provide the appropriate language version to multilingual users.


SEO Configuration

SEO (Search Engine Optimization) is the work of optimizing a site’s structure and metadata so that search engines like Google can better understand the site’s content and surface it in search results.

This document summarizes the SEO settings applied to the blog.

1. Absolute URL - Hugo baseURL Configuration

  • File: hugo/hugo.toml
  • Content: baseURL = 'https://blog.plzhans.com'
  • Ensures correct absolute URLs are generated in sitemap.xml, RSS feed, Open Graph, etc.
  • Sitemap (sitemap.xml) and RSS feed (index.xml) are auto-generated by Hugo
  • hugo server (development) automatically uses localhost:1313, so no separate handling is needed

2. Automatic robots.txt Generation

  • File: hugo/hugo.toml
  • Content: enableRobotsTXT = true
  • robots.txt is auto-generated on Hugo build (allows all crawlers + includes the Sitemap URL)

3. Schema.org Structured Data (JSON-LD)

  • File: hugo/layouts/_default/single.html
  • Inserts BlogPosting JSON-LD on post pages (type != "page")
  • Included fields: headline, datePublished, dateModified, author, description, mainEntityOfPage
  • Enables rich snippets (author, date, etc.) in Google search results
  • File: src/services/NotionExportService.mjs
  • During Notion sync, the first image in the content is detected and automatically added to the front matter images field
  • Open Graph meta tags are output via Hugo’s built-in template (_internal/opengraph.html), using images as og:image

5. meta description / Twitter Card

  • File: src/services/NotionExportService.mjs
  • The “Summary” property from Notion is output as the front matter description field
  • Used by Hugo’s built-in opengraph/twitter_cards templates and the meta description in baseof.html
  • Twitter Card meta tags are output via Hugo’s built-in template (_internal/twitter_cards.html)
  • Other meta tags (author, viewport) are also provided by default from the theme

6. Canonical URL

  • File: hugo/layouts/_default/baseof.html
  • Overrides the theme’s (m10c) baseof.html to add a <link rel="canonical"> tag
  • Uses .Permalink as the canonical URL
  • Also includes multilingual hreflang tags (outputs alternate + x-default when translated pages exist)

7. Google Analytics (GA4)

  • Provided by default in the theme (m10c)
  • Can be verified via GA integration when authenticating Google Search Console

Key Elements of Multilingual SEO

HTML lang Attribute

Specifies the page language to provide language information to search engines and screen readers.

1<html lang="ko">

Informs search engines of each language’s page URL to prevent duplicate content issues.

1<link rel="alternate" hreflang="ko" href="https://blog.plzhans.com/ko/post/example/">
2<link rel="alternate" hreflang="en" href="https://blog.plzhans.com/en/post/example/">
3<link rel="alternate" hreflang="ja" href="https://blog.plzhans.com/ja/post/example/">
4<link rel="alternate" hreflang="x-default" href="https://blog.plzhans.com/ko/post/example/">

Canonical URL (Multilingual)

If each language is written as a professional translation, omitting canonical allows all language versions to be recognized as independent originals.

1<link rel="canonical" href="https://blog.plzhans.com/ko/post/example/">

Implementing Multilingual Support in Hugo

1. Verify Theme’s Multilingual Support

Check the lang attribute (themes/{theme}/layouts/_default/baseof.html)

1<!doctype html>
2<html lang=" .Site.Language.Lang ">

Check relLangURL support

Verify whether the home link preserves the language-specific URL. If unsupported, override baseof.html.

1<body>
2  <header class="app-header">
3    <a href=" .Site.Home.RelPermalink "><img class="app-header-avatar" src="..." alt="..." /></a>

2. Multilingual Configuration in hugo.toml

 1# Default content language
 2defaultContentLanguage = "ko"
 3# Include the default language in a subdirectory too (/ko/)
 4defaultContentLanguageInSubdir = true
 5
 6[languages]
 7  [languages.ko]
 8    weight = 1
 9    languageName = "한국어"
10
11  [languages.en]
12    weight = 2
13    languageName = "English"
14
15  [languages.ja]
16    weight = 3
17    languageName = "日本語"

3. Add the canonical Tag

If the theme doesn’t support it, override baseof.html.

1<link rel="canonical" href=" .Permalink " />

4. Generate hreflang Tags

Set translationKey in the content file

1---
2id: "80"
3translationKey: "80"
4slug: "80-redis-dump-vs-aof"
5title: "Redis dump vs aof"
6---

Add hreflang to baseof.html (override if the theme doesn’t support it)

1<link rel="alternate" hreflang=" .Language.Lang " href=" .Permalink " />
2<link rel="alternate" hreflang="x-default" href=" .Permalink " />

Troubleshooting

URL Duplicate Conflict

In Hugo, you should use slug instead of url to set a post’s address.

Cause

  • Setting slug automatically prepends the language prefix (/ko/, /en/, etc.)
  • Forcing url means Hugo does not automatically add the language code
  • When using url, you must directly embed the language code in the URL itself for each language, like /ko/post/example, /en/post/example
  • If the same path is specified in url without a language code, posts in different languages end up with the same URL, causing a conflict

Solution

  • Switch from url to slug
  • Set defaultContentLanguageInSubdir = true in hugo.toml so that all languages, including the default language, have a subdirectory structure

Note

  • When you only specify slug, the language code is added automatically, but if the slug itself is written in a specific language, it must be translated for each language. It’s recommended to write slugs in English.

hreflang Not Generated Even After Adding translationKey

Cause

  • The theme doesn’t support generating hreflang tags.

Solution

  • Add hreflang-related code by overriding baseof.html

References