A typography-first Hugo theme for people who write — in prose, in Chinese, and in code.

English · 简体中文


ueno-hugo is the custom theme behind a personal blog. Quiet on the surface — black on white, serif body text, generous line-height — and surprisingly complete underneath: galleries and carousels, a hand-built HTML5 video player, a site-wide music player, tabbed code blocks, an in-browser code runner, client-side search, and comments. Every byte of JavaScript loads only on the pages that actually use it.

No framework. No build step beyond Hugo Pipes. No webfont download. No API keys for the embeds.

Philosophy

A handful of opinions this theme commits to:

  • Reading is the feature. Serif body type (Source Serif + Noto Serif SC), a 1.78 line-height, Chinese emphasis dots (着重号) instead of italics, oversized decorative blockquotes, a short bold centered rule. The details are tuned for mixed CJK/Latin prose — not for a landing page.
  • Restraint over decoration. Exactly one accent color: black, inverted to white in dark mode. No gradients fighting for attention. The carousel and the video player are the only places allowed a little theatrical blur.
  • Self-hosted, keyless embeds. Every embed leans on something you already have or a keyless public endpoint — YouTube/Vimeo by id, tweets, QR codes, Google Maps embed URLs, music via MetingJS, code execution via Judge0/Piston. Search runs in the browser. No external webfont by default.
  • Pay only for what a page uses. JavaScript is injected per-page through Hugo’s HasShortcode: a post without a carousel never downloads the carousel script. Everything is minified and fingerprinted.

Highlights

Typography & reading

  • Serif body type served from system/local fonts — zero webfont requests
  • CJK-aware niceties: emphasis dots instead of italics, big decorative quotes, a short centered rule, 1.78 line-height
  • CJK-accurate reading time and word count

Layout & navigation

  • Switchable article layout — a classic left sidebar or a top navbar (articleLayout)
  • Sticky table of contents with scroll-spy on wide screens
  • Mobile-first: the dual column collapses cleanly

Theme

  • Light / dark / follow-system, remembered in localStorage
  • No flash of the wrong theme — an inline script paints the mode before first render

Media — shortcodes

  • wide full-bleed image · gallery responsive grid · carousel album slider with a thumbnail strip and an ambient blurred backdrop
  • video — a hand-built HTML5 player: play/pause, scrubbable progress, buffer indicator, volume, fullscreen, keyboard shortcuts, auto-hiding controls, a dark skin matched to the theme
  • qr, gmap (keyless Maps embed), x (tweets), plus Hugo’s built-in youtube / vimeo

Code

  • macOS-terminal styled blocks (traffic-light dots, language label, copy button), light & dark
  • tabs / tab — a multi-language code switcher
  • Optional in-browser code runner via Judge0 or a self-hosted Piston — a Run button on supported languages

Sound

  • Site-wide fixed mini player (APlayer + MetingJS): platform playlists or your own audio
  • bgm — invisible single-loop background music with a floating toggle
  • aplayer — drop a standalone player anywhere in a post

Find & discuss

  • Client-side full-text search (Fuse.js) over a generated JSON index
  • Giscus comments (GitHub Discussions) — the comment box re-themes itself when you flip light/dark

SEO & syndication

  • Open Graph + Twitter Card + canonical + BlogPosting JSON-LD
  • Custom-styled RSS (XSL) that looks intentional even opened raw in a browser
  • robots.txt and sitemap

Performance

  • Per-page JS injection via HasShortcode — no carousel script on a post without one
  • Minified, fingerprinted assets through Hugo Pipes
  • Lazy images with a graceful placeholder when a source 404s

Tech stack

Static site generatorHugo (extended, ≥ 0.128)
ScriptingVanilla JS — no framework, no bundler
Vendored librariesAPlayer + MetingJS, Fuse.js
External servicesGiscus (comments), Judge0 / Piston (code runner)
HostingCloudflare Pages

Quick start

# 1. Install Hugo extended (≥ 0.128)
brew install hugo

# 2. Run the dev server
hugo server            # http://localhost:1313
hugo server -D         # include drafts

# 3. Build for production
hugo --gc --minify     # output → public/

Project structure

.
├── archetypes/          # front-matter templates (default, diary)
├── content/             # your writing
│   ├── about.md
│   └── posts/  diary/  coding/  archive/  …
├── static/              # favicon, avatar, …
├── layouts/robots.txt
├── hugo.toml            # site configuration
└── themes/ueno-hugo/
    ├── assets/
    │   ├── css/         # main.css, chroma.css (dual-theme highlight)
    │   ├── js/          # theme, toc, lightbox, copy, search, carousel,
    │   │                #   video, tabs, music, runner, giscus, img-fallback
    │   └── vendor/      # aplayer, meting, fuse
    └── layouts/
        ├── _default/    # baseof, list, single, _markup/render-image
        ├── partials/    # sidebar, topnav, music-player, search, comments, …
        └── shortcodes/  # wide, gallery, carousel, video, tabs, aplayer,
                         #   bgm, gmap, qr, x

Writing

hugo new posts/my-post.md
hugo new diary/today.md      # uses the diary archetype

Front matter:

---
title: "Post title"
date: 2026-01-01T10:00:00+08:00
draft: false
subtitle: "Shown on both the list and the article"
cover: "/images/cover.jpg"     # hero image + list-card background
tags: ["tag"]
categories: ["category"]
# disableGlobalPlayer: true    # silence the site-wide music player on this page
---

Shortcodes

ShortcodeWhat it doesKey params
wideFull-bleed single imagesrc, caption
galleryResponsive image gridcols, gap, ratio — one URL per line, optional | caption
carouselAlbum slider w/ thumbnails + ambient backdropratio, autoplay, loop
videoCustom HTML5 playersrc, poster, ratio, loop, muted, autoplay, preload
tabs / tabTabbed multi-language codetab "Label"
aplayerMusic playerplatform: server/type/id · custom: url/name/artist/cover
bgmHidden looping background musicurl, volume, position
gmapKeyless Google Maps embedsrc, height, caption
qrQR code via public APItext, size, caption
xEmbedded tweeturl, or user + id
{{< carousel ratio="16/9" autoplay="3000" >}}
https://example.com/a.jpg | A caption
https://example.com/b.jpg
{{< /carousel >}}

{{< video src="https://cdn.example.com/clip.mp4" poster="/cover.jpg" >}}

Configuration

The essentials, in hugo.toml:

baseURL = "https://your-domain.example/"
title   = "Your Blog"
hasCJKLanguage = true              # accurate CJK word count

[params]
  articleLayout  = "sidebar"       # "sidebar" | "topnav"
  enableComments = true

  [params.musicPlayer]             # APlayer + MetingJS
    enable = true
    fixed  = true
    theme  = "#2980b9"
    loop   = "all"                 # all | one | none
    [params.musicPlayer.meting]    # a platform playlist…
      server = "netease"
      type   = "playlist"
      id     = "<your-playlist-id>"
    # …or self-host the audio instead:
    # [[params.musicPlayer.audio]]
    #   name = "…"; artist = "…"; url = "…"; cover = "…"

  [params.codeRunner]              # in-browser execution
    enable    = true
    provider  = "judge0"           # judge0 | piston
    endpoint  = "https://ce.judge0.com"
    languages = ["python", "rust", "go", "javascript", "…"]

  [[params.social]]
    name = "GitHub"
    url  = "https://github.com/<you>"
    icon = "github"                # github / x / rss / email / telegram / …

Comments. Giscus is wired up in layouts/partials/comments.html. Set your own data-repo, data-repo-id, data-category and data-category-id from https://giscus.app.

Deployment

Built for Cloudflare Pages (the free tier is plenty):

SettingValue
Build commandhugo --gc --minify
Output directorypublic
Environment variableHUGO_VERSION = your local Hugo version

Push to your default branch; Pages builds and ships in a minute or two.

Design note. Production uses --minify. HTML minifiers collapse standard boolean attributes (loop, autoplay, …) into valueless form — even on custom elements. So when a web component needs an actual value on such an attribute, this theme passes it via data-* and builds the element at runtime in JS, sidestepping minification entirely. Worth knowing before you add another web-component shortcode.

License

Theme code is released under the MIT License. Blog content (everything under content/) is © its author and is not covered by MIT.

Acknowledgements

Built on Hugo. Sound by APlayer + MetingJS; search by Fuse.js; comments by Giscus; code execution by Judge0.