Switched My Blog Theme: From Hugo NexT to Self-Written Zhi

Why Switch

This blog previously used Hugo NexT, forked for custom modifications. NexT itself is a feature-rich theme, but when it comes to “customizing things yourself,” the experience wasn’t great.

The issues boiled down to a few things:

SCSS nesting hell. 101 SCSS files, three levels of directory nesting. _common/components/post, _common/components/third-party, _common/outline/sidebar… To change a style, you first had to figure out which file it was in, where variables were defined, and which scheme was overriding it. Not that it couldn’t be done, but each change meant half an hour of hunting.

Four layout schemes. Gemini, Mist, Muse, Pisces — each with its own independent CSS. I only use Gemini, but the other three schemes’ code was still there, taking up significant volume.

High JS coupling. 9 JS files, with next-boot.js as the main entry, pjax.js for page transitions without refresh, and config.js for reading theme configuration. Several files referencing each other — adding a feature meant understanding the entire startup flow.

Hard to modify. The theme was forked, and upstream updates were scary to merge — my changes were scattered across various files, resulting in endless merge conflicts. The end result was a frozen fork, patching on a fixed version.

Style mismatch. I have a studio website that follows a clean, minimalist approach — plenty of whitespace, no decoration, centered content. NexT was ported from Hexo NexT, carrying the aesthetic of the forum/portal era: high sidebar information density, excessive color options, deeply layered components. The blog and the studio are the same brand, but the two sites looked like they were made by different people — something I tolerated for too long.

In summary, the theme had tons of features, but every time I wanted to change just a few things, I had to wrestle with the entire system’s complexity.

The New Approach

I settled on a few principles:

  • No SCSS. Pure CSS + CSS Variables are enough for a theme system, no need for an extra pre-processing layer
  • No build tools. Hugo’s built-in resources.Getminifyfingerprint pipeline is sufficient; no webpack or vite needed
  • Feature toggles. Each feature gets its own flag — disable it and the corresponding CSS and JS aren’t loaded, no wasted bytes
  • Fewer files. If one CSS file can solve it, don’t split it into five

And then I got to work. The entire theme was built from scratch, with AI-assisted coding, taking approximately two weeks.

What Zhi Theme Does

The theme is called Zhi. The name comes from the middle character of my given name, with several layers of meaning: purple (zi) — the accent color of the theme is purple, clean but not cold; paper (zhi) — a blog is essentially a stack of paper; writing (zi) — where all the written content lives. “Paper” and “writing” — one carries, one expresses — exactly what a blog should be. The code is on GitHub under MIT license.

Core Metrics Comparison

NexT (fixbyown)Zhi
Total files285105
Stylesheets101 SCSS24 CSS
Stylesheet directory depth3 levels nested1 level flat
JS files913
HTML templates7938
Layout schemes4 (Gemini/Mist/Muse/Pisces)1
Build toolsRequires SCSS compilationZero build, pure Hugo Pipes

File count more than halved, CSS went from SCSS preprocessing to pure CSS, directory structure flattened. JS files actually increased by a few because NexT crammed logic into a few large files, while Zhi splits them into functionally independent small files — each JS file has a single responsibility, changing one feature doesn’t affect others.

Pure Hugo Pipes, Zero Build

One thing I particularly like is that the theme has zero npm dependencies, no package.json, no need to install node_modules.

CSS processing pipeline:

1
2
3
4
main.css (@import aggregation)
  → Hugo css.Build (source maps in dev, minified in production)
  → fingerprint
  → browser

JS processing is similar: resources.Getminifyfingerprint. All native Hugo capabilities, no build configuration needed.

This means you can use it right after installing Hugo — no environment dependency hell.

Feature Flags

Each feature can be independently toggled in config.yaml:

yaml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
params:
  features:
    codeHighlight: true
    themeSwitch: true
    mathJax: true
    mermaid: true
    lightbox: true
    search: true
    sidebar: true
    toc: true
    readingProgress: true
    backToTop: true
    analytics: false

Disabled features won’t load their corresponding CSS and JS. The implementation uses a features.html partial that serializes all flags as JSON into the data-features attribute of <body>, and JS reads this attribute to decide which modules to initialize.

For example, if your blog doesn’t use math formulas, set mathJax: false — MathJax’s JS won’t load at all, saving hundreds of KB.

Lazy Loading

Two heavier external libraries use lazy loading:

  • MathJax 3: Only loads when page content contains $...$ or $$...$$
  • Mermaid: Only loads when a ```mermaid code block exists

Most blog posts don’t need these libraries, so most pages don’t load them.

Dark/Light Theme

Implemented with CSS Variables. theme.css defines :root (light) and [data-theme="dark"] (dark) variable sets, and all other CSS files reference colors through variables without hardcoding.

A small detail: to prevent flash of unstyled content (FOUC) during page load, an inline JS snippet in <head> sets the data-theme attribute before rendering. theme-toggle.js handles switching, with state stored in localStorage persisting across refreshes.

Default is auto, following system preference.

Code Rendering

Code blocks use Hugo’s built-in Chroma for syntax highlighting, with copy button and language label added via render hook:

python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def fibonacci(n):
    """Generate Fibonacci sequence up to n terms."""
    a, b = 0, 1
    result = []
    for _ in range(n):
        result.append(a)
        a, b = b, a + b
    return result

print(fibonacci(10))
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

One-click copy at the top-right, language name at the top-left. Monokai color scheme, line numbers enabled by default.

Formula Rendering

MathJax 3 is loaded on demand — only included when the page contains $...$ or $$...$$:

Euler’s formula: $e^{i\pi} + 1 = 0$

Bayes’ theorem:

$$ P(A|B) = \frac{P(B|A) \cdot P(A)}{P(B)} $$

Supports both inline and block-level syntax, with formula colors automatically adapting to dark mode.

Diagram Rendering

Mermaid is also loaded on demand, written directly in code blocks:

mermaid
graph LR
    A[User Request] --> B{Nginx}
    B -->|Static Assets| C[CDN]
    B -->|API Call| D[Go Service]
    D --> E[(PostgreSQL)]
    D --> F[(Redis)]
    C --> G[User Browser]

No screenshots needed, no drawing tools — write directly in Markdown. Mermaid automatically switches to dark theme in dark mode.

Other Features

  • Image lightbox: Click article images for fullscreen view
  • Local search: XML index + frontend search, no external services
  • TOC: Auto-numbering and collapsible, configurable depth
  • Reading progress bar: Thin bar at the top showing reading progress
  • Back to top: Floating button showing scroll percentage
  • Donation: WeChat/Alipay QR code popup
  • Friend links: Dedicated friend links page template
  • Archive: Timeline display of all articles
  • Creative Commons: Copyright notice at article footer
  • Shortcodes: note (info box), quote (blockquote), video (Bilibili/YouTube embed, auto-timezone switching)

Internationalization

i18n/en.toml and i18n/zh.toml — templates use {{ i18n "key" }} to reference. Adding a new language just requires adding one toml file.

How to Use

Just change one line in the blog’s config.yaml:

yaml
1
theme: hugo-themes-zhi

The theme is added as a git submodule:

bash
1
git submodule add https://github.com/mickeyzzc/hugo-theme-zhi.git themes/hugo-themes-zhi

All configuration is centralized in the site’s config.yaml; the theme’s own hugo.toml only provides defaults. For the complete configuration reference, see exampleSite/hugo.toml in the theme directory — 282 lines, each parameter annotated.

The Role of AI

Honestly, most of this theme’s code was written by AI. The architecture and design were mine — no SCSS, no build tools, feature flags, file organization — but the actual CSS and Go template code was mostly generated by AI.

The development workflow went something like this:

  1. I define a feature’s structure and interaction
  2. AI generates CSS and HTML templates
  3. I review the results and fine-tune details
  4. When CSS variable naming conflicts or JS initialization order issues arise, AI helps debug

The more complex parts were dark theme FOUC prevention and the video geo-switch logic — AI’s first attempts at these weren’t quite right, and it took a few rounds of iteration to get them working.

The entire theme, from scratch to launch-ready, took about two weeks. If I had hand-written everything, I estimate it would have taken at least two months — there are too many CSS details for one person to handle.

Summary

Switching from NexT to Zhi boils down to three core benefits:

Lower maintenance cost. Fewer files, flatter structure, changing one feature only requires touching one or two files. Previously with NexT, changing a small style meant digging through three or four directory levels to find the SCSS variable definition.

No build dependencies. Just install Hugo and you’re ready to go — no touching node_modules. Previously, local development required confirming the SCSS compiler version was correct.

Controllable features. Each feature has its own toggle — unused features can be turned off, making pages significantly lighter.

The theme is still being actively improved. Issues or suggestions are welcome at the GitHub repo.