A card-style Hexo theme: memos timeline, blog series, gradient visuals, built-in Atom feed & OG image generation
Forked and extended from hexo-theme-butterfly 5.7.0 (Apache-2.0)
Demo: SpeechlessPanda's Blog Β· npm: hexo-theme-panda
| Feature | Description | Default |
|---|---|---|
| Memos enhancements | Per-memo Giscus comment iframes, auto-expand commented memos, standalone deep-link pages, local-search injection | on |
| Home-as-about | Home renders about/index.md; the post stream moves to /blog/ |
on (optional) |
| Latest memo card | The newest memo pinned on top of the post stream | on |
| Blog series | A _posts/<name>/ folder with index.md becomes one card on the post stream; chapters stay off the stream |
on |
| Atom feed | Custom generator mixing posts + memos, with update-notification entries | on |
| OG share images | Per-post 1200Γ630 gradient images (requires @resvg/resvg-js) |
off |
| Gradient look | Blueβpurpleβorange gradient header/footer/background (configurable, yields to your images) | on |
| Nav background band | With an image background, the fixed nav paints that image's top band, viewport-locked and aligned with #web_bg |
on |
| Link target blank | Article/memo links open in a new tab | on |
| Extra highlighting | fish & typst grammars registered out of the box | built-in |
{% hideToggle %} collapse |
Expanded blocks get a bottom collapse bar that scrolls back to the header | on |
Everything Butterfly offers (PJAX, dark mode, comment systems, search, word count, etc.) is kept intact and configured exactly the Butterfly way.
Hexo β₯ 5.3.0. Run this in the Hexo site root (not inside the theme). Package: hexo-theme-panda.
npm install hexo-theme-pandaThe renderers Panda needs (hexo-renderer-pug, hexo-renderer-stylus, hexo-util, moment-timezone) are its own dependencies, so npm/pnpm/yarn installs them automatically. Hexo 5+ loads hexo-theme-panda from node_modules.
themes/wins overnode_modules. Hexo looks forthemes/<theme>first and only falls back tonode_modules/hexo-theme-<theme>. If you previously cloned the theme, deletethemes/panda/β otherwise the stale copy keeps loading andnpm update hexo-theme-pandasilently does nothing. The build prints a warning when this happens.
Upgrading an existing install: see π Upgrading.
Only if you want to hack on the theme itself.
git clone https://github.com/SpeechlessPanda/hexo-theme-panda.git themes/pandaEnable the theme in the site _config.yml:
theme: pandaNew site from scratch:
npm install -g hexo-cli
hexo init my-blog && cd my-blog
npm install hexo-theme-pandaThen set theme: panda as above.
Two commands, and both of them matter:
npm update hexo-theme-panda
hexo clean && hexo generatenpm updateonly swaps files innode_modules. It rebuilds nothing, so apublic/written by the previous theme keeps rendering exactly what it had β including the old version in the footer.hexo cleanbeforehexo generate. An upgrade can drop or rename files undersource/;hexo cleandeletespublic/anddb.jsonso nothing from the old theme survives. Runninghexo generatealone after a theme upgrade is the usual cause of a site that looks half-updated.
Swap in pnpm update / yarn upgrade if that is your package manager.
If npm update says Already up to date while a newer release exists on npm, you are resolving through a registry mirror that has not caught up yet. Pin the version to get past it: npm install hexo-theme-panda@latest (pnpm add / yarn up behave the same way).
GitHub Actions, Vercel, Netlify, Cloudflare Pages and friends build from the committed lockfile, not from your laptop. After the two commands above, commit the result and push:
git add package.json package-lock.json # or pnpm-lock.yaml / yarn.lock
git commit -m "chore(deps): bump Panda theme"
git pushSkipping this is the single most common "I upgraded but the footer still shows the old version" report: your local public/ runs the new theme, while the deployed site keeps rendering whatever the lockfile in the repository still pins. The build now prints the version it used, so the CI log tells you which theme produced the deployment.
| Where | What you should see |
|---|---|
hexo g output |
[panda] theme X.Y.Z Β· Hexo A.B.C Β· N files generated (plus the banner at startup) |
| CI log | the same [panda] theme X.Y.Z line on the run that deployed |
| Any page footer | Panda X.Y.Z next to the Hexo version |
| Page source | theme assets carrying ?v=X.Y.Z |
If the [panda] line, the footer and the ?v= disagree, the page you are reading was not built by the theme you have installed β regenerate, then redeploy.
All of these run in the site root. Full CLI: Hexo commands.
| Command | What it does |
|---|---|
hexo new "My post" |
New article β source/_posts/My-post.md |
hexo new series "Intro to X" |
New series β source/_posts/Intro to X/index.md (title is the folder name, not slugized) |
hexo new --series "Intro to X" "Chapter 1" |
New chapter β source/_posts/Intro to X/Chapter 1.md (creates index.md if missing) |
hexo new page about |
New page β source/about/index.md |
hexo server / hexo s |
Local preview at http://localhost:4000/ |
hexo s --draft |
Preview including drafts |
hexo generate / hexo g |
Build static files into public/ |
hexo deploy / hexo d |
Publish public/ with your deployer |
hexo g -d |
Generate, then deploy |
hexo clean |
Delete db.json and public/ |
hexo clean && hexo g -d |
Full republish |
hexo clean && hexo s |
Stale theme/config? wipe cache, then preview |
Memos (η’η’εΏ΅) are not created with hexo new. Edit source/_data/shuoshuo.yml, then hexo s or hexo g.
hexo deploy needs a deployer plugin. GitHub Pages example:
npm install hexo-deployer-git# site _config.yml
deploy:
type: git
repo: git@github.com:<user>/<user>.github.io.git
branch: mainhexo clean && hexo g -dPanda writes public/atom.xml itself. Do not also enable hexo-generator-feed (both would claim the same path).
Same convention as Butterfly: don't edit the packaged _config.yml (node_modules/hexo-theme-panda/_config.yml or themes/panda/_config.yml). Create _config.panda.yml in your site root and put your overrides there (Hexo deep-merges, overlay wins).
The fully-commented default config lives in _config.yml.
flowchart LR
overlay["_config.panda.yml"] --> hexo[Hexo]
theme["_config.yml"] --> init["scripts/events/init.js"]
init --> hexo
hexo --> layouts["layout/*.pug"]
hexo --> memos["scripts/panda/memos.js"]
hexo --> feed["scripts/panda/feed.js"]
hexo --> series["scripts/panda/blog-series.js"]
hexo --> og["scripts/panda/og-image.js"]
- Hexo β₯ 5.3.0 (8.x works; this is what the demo runs)
- Node.js β₯ 18
- Renderers:
hexo-renderer-pugandhexo-renderer-stylus(install them if your site does not already have them)
Optional extras, only if you turn the matching feature on:
| Feature | Extra install |
|---|---|
OG share images (og_image.enable) |
npm install @resvg/resvg-js |
| Local search injection | Butterfly-style search.use: local_search plus a search data file |
| Memo comment counts | GH_DISCUSSION_TOKEN in the build environment |
# _config.panda.yml
home_about:
enable: true # falls back to the post list when the source page is missing
source: about/index.mdMove the post stream to /blog/ and bind the home page:
# site _config.yml
index_generator:
path: /blog---
title: About
layout: home
---(source/index.md)
/ keeps the full-screen hero (title + typewriter). /blog/ uses the same short header as About / Links (#page-site-info, i18n page.articles), not the homepage banner.
- Create
source/memos/index.md:
---
title: Memos
type: shuoshuo
---- Write entries in
source/_data/shuoshuo.yml:
- date: 2026-09-10 12:00 # site timezone (_config.yml `timezone`)
content: Supports **markdown** and [links](https://example.com)
tags: [life] # optional
key: my-custom-key # optional; derived from date otherwise# _config.panda.yml
memos:
enable: true
path: /memos/ # page path (shared by card link, standalone pages, feed)
data_key: shuoshuo # source/_data/<data_key>.yml
latest_card: true # "latest memo" card on the post stream
standalone_pages: true # one deep-linkable page per memo
search_injection: true # inject into local search (needs search.use: local_search)
comment_count: true # build-time Giscus counts -> auto-expand commented memosWhile the data file is missing or empty, the theme shows one built-in sample memo ("Hello, it's my blog!") on the timeline, the latest-memo card and the feed, so a fresh site demonstrates the format end to end. It disappears everywhere the moment you write your first real entry, and it is never persisted into your db.json.
Comments use Giscus (comments.use: Giscus + giscus.*, same as Butterfly). Each memo gets its own discussion via the theme's multi-iframe giscus /widget integration.
comment_countneedsGH_DISCUSSION_TOKEN(a GitHub token with discussions read access) in the build environment. Without it, the step is skipped silently and comment areas stay collapsed.
A series is a one-level folder under source/_posts, not Butterfly's {% series %} tag (series.enable stays off).
| Path | Role |
|---|---|
source/_posts/hello.md |
Independent post, shown on the post stream |
source/_posts/Intro to X/index.md |
Series metadata only (title + description keys required; description may be empty). Not a post, not in the feed |
source/_posts/Intro to X/Chapter 1.md |
Chapter: listed on the series page, hidden from the post stream, still an RSS entry |
index.md example:
---
title: Intro to X
description: From zero to a working setup
cover: /img/series-intro.jpg
---cover is the series thumbnail (same idea as a post cover). It appears on the /blog/ series card and as the series page header. It does not inherit from chapters.
cover value |
Effect |
|---|---|
/img/foo.jpg, https://β¦/foo.png |
Image. Local files go in source/ so the URL matches (source/img/foo.jpg β /img/foo.jpg). Also gif / svg / webp / avif |
linear-gradient(135deg, #6ec6ff, #ffd2a8) or #6ec6ff |
CSS background, not an <img> |
empty / omit / false |
No card image; series page header falls back to default_top_img |
The post stream mixes independent posts and series cards, newest first. A series card's date is its latest chapter. Empty series (index only) are omitted. Drag existing posts into the folder β no extra front-matter β and they leave the stream on the next hexo g / hexo s. CLI: hexo new series "Intro to X" and hexo new --series "Intro to X" "Chapter 1" (see Daily commands).
# _config.panda.yml
blog_series:
enable: true
path: /series/ # landing page URL prefixLanding pages: /series/<folder>/. Chapter permalinks stay whatever your site permalink already is. In-series prev/next only walks sibling chapters. Comments on the series page follow the site comment system; set comments: false on index.md to turn them off.
Deeper than one folder (_posts/a/b/c.md) is ignored.
# _config.panda.yml
feed:
enable: true # on by default; do not run hexo-generator-feed together
path: atom.xml
post_limit: 20
excerpt_limit: 140
update_notify_hours: 24 # re-push posts with explicit front-matter `updated:` >24h after `date:`
include_memos: trueEntry identity lives entirely in the URL path (never query/fragment), which every reader preserves; memo entries link to their standalone pages and update entries to auto-generated redirect stubs. Entry ids derive from file content, never from file mtime (which is just checkout time on CI): rebuilding after any unrelated change β e.g. adding a memo β never re-pushes old posts. New posts scaffold with updated: false and sticky: false; to re-push a revised post, flip updated to a real date (updated: 2026-09-28 21:00:00) more than update_notify_hours after date: β false/null/empty all mean "never revised" and are ignored. The stub id is a content hash, so every further edit notifies exactly once more, and ids stop moving the moment you stop editing. Series chapters are ordinary posts: they scaffold the same keys and re-push the same way. Memo dates accept YYYY-MM-DD HH:mm (also ISO T or date-only); a memo with an unparseable date is skipped with a build warning. Series index.md is not a post and never appears in the feed. A series card on the post stream is dated by its latest chapter, so a new chapter also moves the card.
npm install @resvg/resvg-js # once, in your site# _config.panda.yml
og_image:
enable: true
width: 1200
height: 630The LXGW WenKai font (SIL OFL 1.1) is downloaded once into fonts/ on first build. Each post gets og-images/<slug>.png wired into og:image.
On by default. Setting Butterfly's background, default_top_img or footer_img makes the matching gradient area yield to your image automatically. To recolor:
# _config.panda.yml
gradient:
header_light: linear-gradient(135deg, ...)
header_dark: linear-gradient(135deg, ...)
web_bg_light: linear-gradient(180deg, ...)
web_bg_dark: linear-gradient(180deg, ...)
# plus header_mask_* / nav_* β see the default config commentsDisable entirely: gradient.enable: false.
Butterfly's background paints the page background (#web_bg). Panda adds one thing on top: when that value is an image, the fixed nav bar paints the top band of the same image, locked to the viewport. It no longer scrolls away, and it no longer falls back to a flat gradient bar.
# _config.panda.yml
background: /img/bg.webp # an array works too β a random pick on each loadWhy it lines up: #web_bg is a viewport-sized box with background-size: cover + background-position: center. Painting that same cover onto the 60px #nav box zooms into the photo's middle. The scrolled bar instead uses a viewport-sized #nav::before (100vwΓ100vh, cover, center, clipped to 60px) so it shows the top strip of the rendered page background β same pixels, no seam, no scroll.
Legibility: the band carries a scrim (gradient.nav_band_mask_light/dark) under white / #eaf4ff nav text. Measured against a mid-tone photo: 5.0:1 in light mode, 5.2:1 in dark mode. Unusually light or dark image? Retune:
# _config.panda.yml
nav:
background_band: true # false keeps the gradient nav bar
band_text_light: '#ffffff'
band_text_dark: '#eaf4ff'
gradient:
nav_band_mask_light: 'linear-gradient(180deg, rgba(12, 24, 44, 0.6) 0%, rgba(12, 24, 44, 0.42) 100%)'
nav_band_mask_dark: 'linear-gradient(180deg, rgba(0, 0, 0, 0.58) 0%, rgba(0, 0, 0, 0.42) 100%)'Edge cases:
- A color or a raw CSS gradient in
backgroundhas no "top band", so the nav keeps the normal gradient bar. - A random-array
backgroundkeeps the nav on the image#web_bgactually picked (synced at runtime, PJAX included). - iOS Safari still mishandles a viewport-locked image in the 60px bar. There the theme drops the image layer and keeps the scrim + blur.
# _config.panda.yml
link_target_blank: true # article/memo links open in a new tab (default on)fish / typst code highlighting works out of the box.
{% hideToggle Title %}β¦{% endhideToggle %} is Butterfly's tag. Panda adds a bottom collapse bar (i18n: en Collapse / zh-CN ζΆθ΅· / zh-TW ζΆε / ja ιγγ / ko μ κΈ°). Clicking it closes the block and scrolls back to the header under the fixed nav. Asset URLs carry ?v=<theme version> so a patch like 1.1.1 busts cached main.js / index.css.
- Replace
themes/butterflywith this theme (or swap the npm package) and settheme: panda - Rename
_config.butterfly.ymlto_config.panda.yml - Butterfly's legacy
_data/butterfly.ymloverride is not supported (Panda warns on startup) - Every other config key is compatible
Apache-2.0. Panda is derived from hexo-theme-butterfly by Jerry (Apache-2.0); see NOTICE for attribution and the list of changes. Modified files carry a notice in their header comments. The OG font LXGW WenKai is SIL OFL 1.1.
Releases: CHANGELOG.md.