Overview

Notes on the changes I made and the issues I ran into while migrating the Hugo Stack theme used on this site from v3 to v4.
I mostly followed the official migration guide below.
Environment

- OS: Windows 11 Pro 25H2
- Hugo: v0.155.3 → v0.165.0
- hugo-theme-stack: v3.31.0 → v4.0.3
Upgrade Hugo

Stack v4 requires Hugo v0.157.0 or later. My local version was v0.155.3, so I had to upgrade. I went with the latest version at the time (v0.165.0).
Hugo binaries are available from Hugo’s GitHub releases page.
Stack requires the extended version of Hugo in both v3 and v4, so download a release with extended in its name. Extract the downloaded file and put its contents somewhere on your PATH. I’m on Windows, so I put it in C:\HugoExtended\bin and added C:\HugoExtended\bin to the PATH environment variable.
$ hugo version
hugo v0.165.0-76a5e1880ab46688155b02e99bab9be2a6134492+extended windows/amd64 BuildDate=2026-08-12T14:26:28Z VendorInfo=gohugoioThis site is hosted on Netlify, so I also had to bump Netlify’s build environment to Hugo v0.165.0.
If you set HUGO_VERSION in netlify.toml, or as an environment variable in the Netlify dashboard, change that too.
Fix the X shortcode

This one comes from upgrading Hugo rather than from the Stack theme itself: Hugo’s built-in tweet shortcode was removed in Hugo v0.156. Use the x shortcode instead.
- {{< tweet user="username" id="1234567890" >}}
+ {{< x user="username" id="1234567890" >}}
Replace .Site.Data with hugo.Data

This is another change caused by the Hugo upgrade: Hugo’s built-in .Site.Data was deprecated in Hugo v0.156.
Use hugo.Data instead.
In my case a custom shortcode I had written used .Site.Data, so I replaced it.
Temporarily move custom files aside

Going from v3 to v4, the way the theme is written and how its files are split up have both changed. So if you have layouts or assets directories that override the theme, move them aside temporarily.
One caveat: shortcodes (layouts/shortcodes in v3) must exist or the build will fail, so if you want builds to keep succeeding during the migration, move just the shortcodes to layouts/_shortcodes/.
Migrate the config files

The config files were YAML in v3, but v4 switched to TOML. Hugo converts YAML automatically, so it seems that sticking with YAML should be fine. Since I was going to review the settings anyway, I took the opportunity to switch to TOML.
hugo-theme-stack-starter is a good reference for how to write the config. The Stack theme demo works too. Judging by Stack’s Getting Started, hugo-theme-stack-starter seems to be the official template, so that’s what I used as my reference. Create a config/_default directory and put the following files in it.
config.toml

config.toml holds the basic information about the site. I changed the following from the official template.
- Added
theme = "hugo-theme-stack" - Changed
baseurlto my own site’s URL - Changed
localeanddefaultContentLanguageto Japanese, and accordingly sethasCJKLanguagetotrue
theme = "hugo-theme-stack" is needed. If you manage it as a Hugo Module, you don’t need theme.config.toml after the changes
theme = "hugo-theme-stack"
baseurl = "https://notes.nakurei.com/"
locale = "ja-JP"
title = "NakuRei's Notes"
copyright = "NakuRei"
# Theme i18n support
# See all available values at https://github.com/CaiJimmy/hugo-theme-stack/tree/master/i18n
defaultContentLanguage = "ja"
# Set hasCJKLanguage to true if DefaultContentLanguage is in [zh-cn ja ko]
# This will make .Summary and .WordCount behave correctly for CJK languages.
hasCJKLanguage = true
enableRobotsTXT = true
enableGitInfo = true
timeout = "300s"
[services.googleAnalytics]
ID = "G-XXXXXXXXXX"
[pagination]
pagerSize = 10
[frontmatter]
lastmod = ["lastmod", ":git", "date", "publishDate"]languages.toml

languages.toml holds the multilingual settings. These options are the same as languages in v3.
_languages.toml; the leading underscore is there to keep the settings from being applied. So when you actually configure this, create it as languages.toml with the underscore removed.languages.toml after the changes
[ja]
label = "日本語"
locale = "ja"
weight = 1
[ja.params]
description = "努力の積み重ねを記録しておくためのサイト。"
[ja.params.sidebar]
subtitle = "努力の積み重ねを記録しておくだけ"
[en]
label = "English"
locale = "en"
weight = 2
[en.params]
description = "A website to keep track of my efforts."
[en.params.sidebar]
subtitle = "Just keep track of my efforts."markup.toml

markup.toml holds the settings for Markdown rendering. There’s no change from what I had under markup in v3, so it ports over as-is.
markup.toml after the changes
# Markdown renderer configuration
[goldmark.renderer]
unsafe = false
[goldmark.extensions.passthrough]
enable = true
[goldmark.extensions.passthrough.delimiters]
block = [['\[', '\]'], ['$$', '$$']]
inline = [['\(', '\)']]
[tableOfContents]
endLevel = 4
ordered = true
startLevel = 2
[highlight]
noClasses = false
codeFences = true
guessSyntax = true
lineNoStart = 1
lineNos = false
lineNumbersInTable = true
tabWidth = 4menu.toml

menu.toml holds the settings for the main menu and the social menu. I only configure the social menu on this site, but at least for that menu, the options are the same as in v3.
menu.toml after the changes
[[social]]
identifier = "github"
name = "GitHub"
url = "https://github.com/NakuRei"
[social.params]
icon = "brand-github"
[[social]]
identifier = "x"
name = "X"
url = "https://x.com/nakurei7901"
[social.params]
icon = "brand-twitter"
[[social]]
identifier = "rss"
name = "RSS"
url = "https://notes.nakurei.com/index.xml"
[social.params]
icon = "rss"
[[social]]
identifier = "zenn"
name = "Zenn"
url = "https://zenn.dev/nakurei"
[social.params]
icon = "link"module.toml

module.toml holds the Hugo Module settings. I manage the theme as a Git Submodule rather than a Hugo Module, so I didn’t create module.toml.
params.toml

params.toml holds site-wide parameters — what used to live under params in v3. The following params changed in v4.
dateFormatmoved from Go format strings likeJan 02, 2006to Hugo format strings like:date_fulland:date_mediumSortBywas addedfeaturedImageFieldwas removed: v4 always usesimagedefaultImage.opengraphwas removed
params.toml after the changes
# Pages placed under these sections will be shown on homepage and archive page.
mainSections = ["post"]
# Output page's full content in RSS.
rssFullContent = true
favicon = "img/favicon.png"
# Accepted values: "default", "lastmod"
# default = see https://gohugo.io/quick-reference/glossary/#default-sort-order
# lastmod = sort by last modified date, in descending order
SortBy = "lastmod"
[author]
name = "NakuRei"
url = "https://notes.nakurei.com/"
[footer]
since = 2022
customText = ""
[dateFormat]
published = ":date_medium"
lastUpdated = ":date_medium"
[sidebar]
compact = false
emoji = "🐢"
subtitle = ""
avatar = "img/avatar.png"
[article]
headingAnchor = true
math = false
toc = true
readingTime = true
[article.license]
enabled = true
default = "Licensed under CC BY-NC-SA 4.0"
[widgets]
homepage = [
{ type = "search" },
{ type = "categories", params = { limit = 10 } },
{ type = "tag-cloud", params = { limit = 10 } },
{ type = "archives", params = { limit = 5 } },
]
page = [
{ type = "toc" },
{ type = "categories", params = { limit = 10 } },
{ type = "tag-cloud", params = { limit = 10 } },
]
[opengraph.twitter]
site = "nakurei7901"
card = "summary_large_image"
[colorScheme]
toggle = true
default = "auto"
## Comments
[comments]
enabled = false
provider = "disqus"
# See all the available configurations at https://github.com/CaiJimmy/hugo-theme-stack/blob/master/config/_default/params.toml
# Copy the configurations you need from there and paste it here.
## Original url-card shortcode
defaultNoImage = "icons/photo-off.svg"
defaultNoLinkImage = "/icons/link-off.svg"
imageQuality = 80permalinks.toml

permalinks.toml contains the same settings that were under permalinks in v3.
permalinks.toml after the changes
post = "/post/:slug/"
page = "/:slug/"related.toml

related.toml contains the same settings that were under related in v3. There are no new options.
related.toml after the changes
# Related contents configuration
includeNewer = true
threshold = 60
toLower = false
indices = [
{ name = "tags", weight = 100 },
{ name = "categories", weight = 200 },
]Move the favicon

The favicon and avatar images lived in static/ in v3, but v4 expects them in assets/. On this site the avatar was already in assets/img/, but the favicon was in static/img/, so I moved the favicon image to assets/img/.
Fix the front matter of your pages

The front matter of articles changed between v3 and v4. What was hidden in v3 became build.list in v4. So if you were setting hidden = true, you need to change build.list to never like this.
---
- hidden: true
+ build:
+ list: never
---
If you were setting hidden = false, set build.list to always.
---
- hidden: false
+ build:
+ list: always
---
Migrate your layout customizations

In Hugo, you can override a theme’s files by placing a file with the same name in the same location. Between v3 and v4, the Stack theme changed both how the theme is written and how its files are split up. So if you customized the theme, you need to fix the location, name and content of your customized files to match v4.
Move the contents of layouts/_default/ into layouts/

With the Modern Template System introduced in Hugo v0.146.0, organizing files under a layouts/_default/ folder was deprecated and consolidated into placing them directly under layouts/.
The Stack theme followed suit: in v4 the contents of layouts/_default/ moved into layouts/.
So if you were using layouts/_default/, move it to layouts/.
In my case I had modified the theme’s layouts/_default/_markup, so I moved layouts/_default/_markup to layouts/_markup.
Move the contents of layouts/partials into layouts/_partials

This is another consequence of the Modern Template System introduced in Hugo v0.146.0.
Stack v4 follows it and changed layouts/partials to layouts/_partials.
So if you customized the theme, you need to move the contents of layouts/partials into layouts/_partials.
Change the header image height

In Stack v4 you can change the header image height in assets/scss/custom.scss1. The defaults are 150px on mobile, 200px in between, and 250px on large screens.
In v3 I had customized these to 100px on mobile, 150px in between and 150px on large screens, so I created assets/scss/custom.scss with the following.
:root {
--article-image-height: 100px;
@include respond(md) {
--article-image-height: 150px;
}
@include respond(xl) {
--article-image-height: 150px;
}
}Remove var(--zh-font-family)

In v3 I had customized the CSS on this site to change the font, but Stack v4 removed var(--zh-font-family), so my existing setting was ignored if left as-is. I removed var(--zh-font-family). Referencing an undefined CSS variable invalidates the whole property, so just deleting it brought my customization back.
References and URLs

In Stack v3 you had to override
assets/scss/partials/article.scssto change this, so personally I’m glad about this change. ↩︎


