Customizing My Blog Theme

The theme is hugo-cactus-dark, a dark theme, clean and simple. Good enough for a blog.

But the original theme only had basic features. I needed bilingual switching, Giscus comments, page analytics, structured data — none of these were included. So I forked it and overrode the templates I needed in layouts/.

What Was Overridden

Templates in layouts/ have higher priority than the same files in the theme. I only overrode what I needed to change.

One annoying thing: Hugo caches templates. Sometimes after editing layouts/ files, the browser still shows the old version. Not Hugo’s fault, it’s browser cache. Use incognito mode or clear cache.

head.html

The most heavily modified. Original only output basic meta tags and style references. Added a bunch of things:

<!-- Search engine verification -->
<meta name="360-site-verification" content="xxx" />
<meta name="sogou_site_verification" content="xxx" />

<!-- Bilingual hreflang -->
{{ if .IsTranslated }}
{{ range .Translations }}
<link rel="alternate" hreflang="{{ .Lang }}" href="{{ .Permalink }}">
{{ end }}
<link rel="alternate" hreflang="{{ .Lang }}" href="{{ .Permalink }}">
{{ end }}

<!-- Open Graph -->
<meta property="og:locale" content="{{ if eq .Lang "zh" }}zh_CN{{ else }}en_US{{ end }}">
<meta property="og:title" content="{{ .Title }}">
<meta property="og:image" content="{{ .Params.image | absURL }}">
<meta property="og:description" content="{{ .Summary | truncate 200 }}">

<!-- Canonical -->
<link rel="canonical" href="{{ .Permalink }}">

<!-- Structured data -->
{{ if .IsPage }}
<script type="application/ld+json">
{
    "@context": "https://schema.org",
    "@type": "Article",
    "headline": "{{ .Title }}",
    "datePublished": "{{ .Date.Format "2006-01-02T15:04:05Z07:00" }}",
    "dateModified": "{{ .Lastmod.Format "2006-01-02T15:04:05Z07:00" }}",
    "author": { "@type": "Person", "name": "{{ .Site.Params.author }}" }
}
</script>
{{ end }}

Why Open Graph? When sharing articles on social media, preview cards need these meta tags. Without them, you just get a bare URL.

Structured data is for Google’s rich results in search. Articles show publish date, author, etc.

Added a language switcher to the navigation bar. Logic: if the current article has a translation, link directly to it. If not, link to the other language’s homepage.

<li class="lang-switcher has-dropdown">
  <a href="#">{{ .Site.Language.LanguageName }}</a>
  <ul class="dropdown">
    {{ range $.Site.Home.AllTranslations }}
    <li>
      <a href="{{ .Permalink }}">{{ .Language.LanguageName }}</a>
    </li>
    {{ end }}
    {{ if .IsTranslated }}
    {{ range .Translations }}
    <li>
      <a href="{{ .Permalink }}">{{ .Language.LanguageName }}</a>
    </li>
    {{ end }}
    {{ end }}
  </ul>
</li>

comments.html

The original theme supported Disqus and Valine. Replaced both with Giscus.

Giscus is based on GitHub Discussions. Visitors can comment after logging in with their GitHub account. No third-party service needed, data lives in the repo’s Discussions.

How to configure it, what the flow looks like — wrote a separate article: How I Added Comments to My Blog.

First time setting it up, there’s a gotcha: data-repo-id and data-category-id are not something you guess. Go to https://giscus.app, enter your repo name, and it generates them for you. I thought repo-id was just the repo name at first, wasted a while on that.

single.html

Article page template. Added article link output for easy referencing. Tags use relLangURL to ensure they point to the current language.

<div class="content" itemprop="articleBody">
  {{ .Content }}
  <h2>{{ i18n "articleLink" }}</h2>
  <a href='{{ .RelPermalink }}'>{{ .RelPermalink }}</a>
</div>

js.html

Scripts loaded at the bottom of the page. Added these:

  • highlight.js: code highlighting, only on article pages
  • Busuanzi: page view counter
  • Service Worker registration: only on production domain
  • Google Analytics: traffic stats

Service Worker registration code:

if ('serviceWorker' in navigator && location.hostname === 'time-friend.com') {
  window.addEventListener('load', function() {
    navigator.serviceWorker.register('/sw.js');
  });
}

Hostname check prevents SW registration in development, avoiding cache interference.

Other Overrides

  • footer.html: i18n-based copyright output
  • post-list.html: homepage article list, filters out about page
  • latest-posts.html: related articles at bottom, excludes current article
  • tagcloud.html: tag cloud with font size based on article count
  • terms.html: tag/category listing with article counts

Custom CSS

Added some styles in static/css/custom.css:

/* Chinese font priority */
body {
  font-family: 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
}

/* Language switcher dropdown */
.has-dropdown:hover .dropdown {
  display: block;
}

Mainly just the Chinese font stack and dropdown menu styles. Didn’t change much, the original theme’s styles were good enough.

Performance

Template overrides have zero performance impact. Hugo compiles templates to static HTML at build time. Override or not, runtime is the same.

One thing to note: all JS is in js.html, loaded at the bottom of the page, doesn’t block rendering. highlight.js only loads on article pages, not on homepage or listing pages.

Article Link:

/en/archive/blog-architecture-theme-customization/

# Related Articles