ueno hugo
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
widefull-bleed image ·galleryresponsive grid ·carouselalbum slider with a thumbnail strip and an ambient blurred backdropvideo— a hand-built HTML5 player: play/pause, scrubbable progress, buffer indicator, volume, fullscreen, keyboard shortcuts, auto-hiding controls, a dark skin matched to the themeqr,gmap(keyless Maps embed),x(tweets), plus Hugo’s built-inyoutube/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 toggleaplayer— 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 +
BlogPostingJSON-LD - Custom-styled RSS (XSL) that looks intentional even opened raw in a browser
robots.txtand 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 generator | Hugo (extended, ≥ 0.128) |
| Scripting | Vanilla JS — no framework, no bundler |
| Vendored libraries | APlayer + MetingJS, Fuse.js |
| External services | Giscus (comments), Judge0 / Piston (code runner) |
| Hosting | Cloudflare 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
| Shortcode | What it does | Key params |
|---|---|---|
wide | Full-bleed single image | src, caption |
gallery | Responsive image grid | cols, gap, ratio — one URL per line, optional | caption |
carousel | Album slider w/ thumbnails + ambient backdrop | ratio, autoplay, loop |
video | Custom HTML5 player | src, poster, ratio, loop, muted, autoplay, preload |
tabs / tab | Tabbed multi-language code | tab "Label" |
aplayer | Music player | platform: server/type/id · custom: url/name/artist/cover |
bgm | Hidden looping background music | url, volume, position |
gmap | Keyless Google Maps embed | src, height, caption |
qr | QR code via public API | text, size, caption |
x | Embedded tweet | url, 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):
| Setting | Value |
|---|---|
| Build command | hugo --gc --minify |
| Output directory | public |
| Environment variable | HUGO_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 viadata-*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.