Imported from Acris/hexo-theme-shiro (
AGENTS.md). Install upstream withnpx skills add Acris/hexo-theme-shiro. Copyright stays with the author.
AGENTS.md
Human docs: README.md / README_CN.md. Design tokens: DESIGN.md. User chat overrides this file; nearest nested AGENTS.md wins.
Project overview
Shiro (白) is a clean, minimalist, multilingual Hexo theme: Nunjucks templates, Tailwind CSS v4, optional MathJax, word count (host plugin), Pagefind search, comments, analytics, and minimal client JS for static output.
Setup commands
| Command | Purpose |
|---|---|
npm install |
Install dev dependencies |
npm run dev |
Tailwind watch (unminified source/css/style.min.css) |
npm run build |
Release assets: core CSS, optional *.min.css, browser *.min.js |
npm test |
Node 24 unit tests + real Hexo/Nunjucks render smoke test |
- Both
devandbuildreadsource/css/_tailwind.css→source/css/style.min.css. - After changing
_tailwind.css,source/css/_src/*, Tailwind utilities in templates, orsource/js/_src/*: runnpm run build(see Testing for committing outputs). - Do not hand-edit
source/css/style.min.css,source/css/*.min.css, orsource/js/*.min.js; do not delete generated CSS or the package lock without clear reason. - Prefer
npm run buildovernpm run devfor one-shot validation.
Repository map
| Path | Role |
|---|---|
layout/ |
Nunjucks: _layout.njk shell (feature gates + foot scripts; include scope does not leak {% set %}); _macro/; _partial/common/ (head, header, …), components, comments, analytics; pages |
scripts/ |
Hexo helpers/filters: thin helpers.js registrar; nunjucks.js layout-root renderer; pure logic in scripts/lib/ (html-analysis, toc, urls, seo, fonts, seal, util); also mathjax.js, images.js, pagefind.js, word_count.js |
scripts/lib/ |
Pure modules required by helpers.js / mathjax.js / images.js (and unit tests): urls, analysis, toc, seo, code-blocks, image-content, image-meta, … Side-effect free — safe if Hexo also loads nested scripts/** files. |
source/css/_tailwind.css |
Tailwind entry (@import core parts) → style.min.css |
source/css/_core/ |
Core theme CSS parts: tokens, base, components, dark, theme-toggle (imported by _tailwind.css) |
source/css/_src/ |
Feature CSS → source/css/*.min.css (code, toc, search, comments, lightgallery, giscus). Site-cascade files normally wrap rules in @layer components (match style.min.css); LightGallery and Gist overrides stay unlayered to outrank their unlayered vendor CSS, and the giscus iframe theme is also unlayered. |
source/js/_src/ |
Client sources → source/js/*.min.js (Hexo ignores _src via underscore prefix). Runtime and LightGallery are single-IIFE sources (runtime.js / lightgallery.js). |
tools/ |
build-assets.js (Tailwind + lightningcss + terser minify) |
test/ |
Unit tests (npm test) |
languages/ |
i18n YAML (keep keys aligned across locales) |
_config.yml |
Default theme config (users copy to _config.shiro.yml) |
DESIGN.md |
Design system; sync with CSS when changing colors, type, spacing, elevation, or component look |
Do not invent helpers — check scripts/helpers.js first. Implementation details live in source and tests.
Pitfalls
- No static
source/favicon.svg— generator overwrites it; seal path isSEAL_PATH_D/seal_path_d. - Font family changes: update the family list in
google_font_urls(stylesheet + preloader token). - Pagefind is not a theme dependency; host needs Pagefind 1.5.0+ when
search.enabled. Standalone generation indexes atbefore_exit; deployment indexes atdeployBefore, with process-level deduplication for combined commands. It does not index duringhexo server; there is nonpxfallback. - Word count: theme
word_count.enabledonly controls display; counting needs host hexo-word-counter. Missing plugin omits meta — does not fail generate. - Keep default LightGallery CDN versions in sync across
_config.ymlandscripts/lib/feature-gates.js(DEFAULT_LIGHTGALLERY_*). Client reads one bag object viaruntime.get('lightgallery')(css/js/themeCss/script/ integrities) — no hardcoded CDN fallback. - Runtime-injected class names are not Tailwind-scanned from client JS — put styles in feature CSS or a scanned template.
- MathJax: set
protect: falsewhen using pandoc--mathjaxorhexo-filter-mathjax. No KaTeX. - Optional
security.csp_nonceis a static theme config value (not per-request). Emitted on theme<script>tags viacsp_nonce_attr, injected once aswindow.__shiro.cspNoncein head-theme, applied byruntime.min.js(fromsource/js/_src/runtime.js) to dynamic scripts. Real CSP nonce security needs host/edge injection of the same value. Optional CDN SRI viasri_attrs/lightGallery.*_integrity/mathjax.integrity(empty = no attributes). - Shiro’s Nunjucks renderer sets
autoescape: falsefor Hexo compatibility. Escape text/attrs with helpersescape_html/escape_attr(not| safe); forhref/srcpreferhref_for/attr_url. Menutargetis allowlisted (_self|_blank|_parent|_top). - Theme
scripts/nunjucks.jsregisters Nunjucks 3 with alayout/FileSystemLoader after host plugins load; keep this renderer so templateextends/include/importresolve under real Hexo generation. Autoescape stays off for Hexo compatibility, so the escaping rule above remains mandatory. - Feature flags: pure
scripts/lib/features.js→isFeatureEnabled(also re-exported fromurls.jsfor compat). Layout usespage_feature_gates()only; helperfeature_enabledremains for child themes. Default-off: search/comments/mathjax/word_count; default-on: toc/lightGallery/progress/back_to_top/dark_mode.toggle. Word count stays meta-only (not in gates). - Page gates + CDN URLs: pure
scripts/lib/feature-gates.js→ helperpage_feature_gates()→ layout setsgatesonce; templates readgates.*(home:post_card_view_models; analytics/RSS:gates.needs*; TOC:gates.tocview-model; comments:gates.commentsClientConfigwith scheme-normalized giscus.src; CSP:csp_nonce_attr(gates.shiroCspNonce)). Foot:gates.footScripts; comments:comments/foot.njk. CDN defaults asserted bytest/defaults-sync.test.js. - Code analysis has separate gates:
needsCodeFontcovers inline or block code for Fira Code;needsCodecovers rendered block targets (pre/ highlight / Gist) and owns code CSS;needsClipboardcovers.highlighttargets and owns runtime + clipboard assets. - Home code gates analyze
excerpt_for_cardoutput, not hidden full post bodies; tag/category pages render title-only lists and must not scan excerpts. Manual excerpts and fallback truncation therefore stay in sync with rendered cards. - Categories: pure
scripts/lib/categories.js→category_index_cards()(one view-model; exclusive count/preview on index). Detail pages use Hexo’s full assignment list (superset). Home meta usespost_primary_category(deepest). Config:category_index.preview_limit. - Attribute URLs: prefer
href_for(path)/attr_url(value)over rawurl_for/versioned_urlinhref/src(Hexo nunjucks autoescape is off). - Comments readiness: prefer
gates.shiroComments+gates.commentsClientConfig. Containers:comments/index.njk; scripts:comments/foot.njkafter deferredruntime.min.js. Client config:feature_var('commentsConfig', gates.commentsClientConfig). Deferred order is runtime →comments-bootstrapinstallsruntime.comments→ provider usesruntime.comments.whenReady. Missing runtime aborts. - MathJax load policy lives in gates (
needsMathjax,mathjaxSrc, …). Helperspage_wants_mathjax/mathjax_optionsare thin aliases for tests/child themes — do not re-readtheme.mathjaxin templates. - Feature CSS minify (
tools/build-assets.js) sets Lightning CSStargetsso nesting flattens for older browsers; prefer flathtml[data-theme=dark] …selectors in_srcsources. - Client runtime and LightGallery sources are single IIFEs (
source/js/_src/runtime.js/lightgallery.js) → matching minified assets. MathJax pure logic lives inscripts/lib/mathjax/*and is re-exported bymathjax-protect.js. Runtime tests enforce the single-IIFE shapes and required API surfaces. - Lazy client features: two intentional protocols — (1) classic body scripts (
createFeatureLoader+featureReady/featureAbort): lightgallery, clipboard only; (2) external component UI (Pagefind search):loadAssetonly;whenDefinedraced with clearable timeout. Search trigger paints early underhtml.js(startsdisabled, enabled when the open handler binds; bootstrap abort hides it). Comments use deferred script order andruntime.comments. Mobile menu is a plainfootScriptsentry (js/mobile-menu.min.js) like toc/theme-toggle — no bootstrap, no bag URL, nocreateFeatureLoader.createFeatureLoaderonError(err, { permanent }): abort/timeout/missing-src permanent; network retryable. LG / clipboard only hard-stop onpermanent. Clipboard re-arm after retryable fail (delayed reload); a failed LightGallery click navigates to the original image while preserving later retries. - Collapsed mobile-menu / inline-TOC content must be
inertso hidden links leave the tab order. Head FOUC addshtml.jsso progressive UI paints correctly before deferred scripts: (1) mobile menu + inline TOC bodies collapse via CSS (data-open="false"); (2)#menuBtn,#themeToggle,#searchToggle, and.toc-toggleshow from first paint (disabled until their handler enables them). A tiny inline after each collapsed panel setsinertimmediately underhtml.js(before deferred handlers) so Tab cannot reach hidden links mid-parse. No-JS omitshtml.js(open sheets, no toggles, no inert). - The full-screen font preloader intentionally hides page content and blocks pointer/focus interaction until fonts settle. JS reveals content with
shiro-preloader-dismissed; the 6.5s CSS failsafe must reveal both the veil and page if JS is unavailable. - TOC output uses semantic nested
<ul>elements; indent.toc-list-nestedrather than flattening hierarchy into visual-onlydata-levelpadding. - Standard lazy open/warm handoff (LightGallery):
runtime.dispatchLiveOrStash/dispatchLiveOrWarm. Do not invent a parallel handoff. - Client config on
window.__shirobare keys only; read viaruntime.get(...). API iswindow.__shiro.runtimeonly (no flat__shiroRuntime). Shared escapes:runtime.escapeHtml/escapeAttr. LightGallery: bootstrap owns capture; feature installs open/warm; ready = API installed. Shared:safeNavigate/navigateFromImage/isModifiedClick. - Post-render HTML: quote-aware token/attribute boundaries live in
scripts/lib/html-scanner.js; consumers includehtml-analysis.js,toc.js,code-blocks.js, andimage-optimize.js. ReuseHTML_TOKEN_OPAQUE_ELEMENTSfor containers whose child-like text must not be scanned, then add consumer-specific skips such aspre/code.scripts/images.jsis the Hexo filter orchestrator only (exports optimizeImages / localImageSize / markCodeBlocksNotProse); do not reintroduce[^>]*tag parsing. - File-backed caches in
scripts/images.jsmust revalidate positive entries and must not permanently cache missing files/directories;hexo servercan add assets without restarting the process. - CSP nonce: layout prefers
csp_nonce_attr(gates.shiroCspNonce)(single normalize in gates). - MathJax protect placeholders are salted (
@@SHIRO_MATH_<salt>_<id>@@) so prose tokens cannot collide with a live protect pass. Filter calls derive a stable, collision-checked salt from the source path/content so renderer-generated IDs remain reproducible; do not reintroduce random build output. - Archive year groups: helper
posts_by_year/scripts/lib/archive.js(do not re-open/close year<div>s in Nunjucks loops). - Tag/category detail post lists: shared
_partial/common/paginated-posts.njk(caller importsrender_list). - Syntax highlight colors: tokens
--color-code-*intokens.css/ dark swaps indark.css; featurecode.cssconsumes tokens only (no parallel dark hex palette). - Dark mode does not invert the Tailwind slate scale. Prefer semantic tokens over
text-slate-*. UI idle / secondary text usestext-chromeonly (text-text-chrome/--color-text-chrome). Use--color-sealfor foreground accents and--color-seal-fillfor surfaces carrying--color-on-seal; there is no--color-text-mutedtoken. - Comments boot:
comments-bootstrap.jsinstallsruntime.commentsbefore the deferred provider executes. Missing runtime aborts. Providers useruntime.commentsonly; immediate load ifonNearViewportmissing. - Pagefind indexes only when theme
search.enabledis on (nothexo.config.search). - Pagefind Component UI config carries both
bundle-pathand Hexo-rootbase-url; keep result links correct for subdirectory deployments. - Home cards:
post_card_view_modelsappliesexcerpt_for_cardpolicy to the full list — no manual excerpt + fallback off orfallback.length: 0→ empty body + read-more (never full post HTML). Invalid/negativelengthfalls back to the default (200). The first rendered non-decorative image defaults to eager; later images default to lazy without overriding authoredloadingattributes. Reading templates apply the eager default after TOC rendering; the image filter leaves the first content-image policy deferred. - Footer credit is fixed English copy and has no
footer.*i18n namespace. - Local image metadata resolution checks Hexo's
post.asset_dirand the Markdown file's same-name asset folder before its source directory. - Feature CSS dark overrides use flat
html[data-theme=dark] …(not Tailwind-scanned). Core dark uses@variant darkin_core/dark.css.
Workflow rules
- Small, focused changes; preserve Hexo theme compatibility and the Shiro minimal aesthetic.
- Layout/feature changes that affect structure or agent-facing rules: update
README.md,README_CN.md, and this file. - New/changed npm deps or version bumps: update
package.jsonandpackage-lock.jsonvia npm (not hand-edited lockfiles). - New config keys: follow Config, i18n, and security (
_config.yml+ docs; safe defaults). - New user-facing strings: every file under
languages/; keys sorted alphabetically per level; group under existing namespaces (clipboard,common,gallery,nav,search,word_count, …). - Template edits: consider home, post, page, archive, tag, category (and dark mode / TOC / search / code / lightbox / MathJax when relevant).
- Avoid heavy client dependencies; prefer lazy/deferred loading.
Code style
- Nunjucks: modular macros/partials; semantic HTML; keyboard/a11y for toggles, search, copy, lightbox.
- JS: plain browser-compatible code —
'use strict', 4-space indent, single quotes; CommonJS inscripts/; DOMContentLoaded-guarded IIFEs insource/js/_src/. No ESM/TypeScript/bundlers for client scripts. - Assets: use
versioned_urlfor static theme assets. - CSS: match existing tokens and minimalist style; do not reformat unrelated code or rewrite large files without need.
- Gate scripts in
_layout.njkby page type, feature flags, and DOM needs so unused pages stay JS-free. Keep feature{% set %}in the layout parent — Nunjucks include scope does not leak sets to the parent. - Comments: short (one line when possible). Prefer clear names over essays. Deep design notes belong here or in
DESIGN.md, not in source headers. - Prefer the smallest fix that restores prior behavior; do not add dual configs, retries, or abstractions unless a real bug needs them.
Docs style
README.md/README_CN.md: user-facing setup and config. Keys, defaults, and short usage only — not implementation internals (hooks, cascade, postMessage, FOUC, etc.).AGENTS.md: agent/maintainer rules, pitfalls, architecture.DESIGN.md: visual system only.- When docs must mention behavior, one short sentence is enough; put the “why” here.
Testing and validation
Required after relevant edits (blockers if red):
- Logic under
scripts/ortest/(or behavior those tests cover) →npm test(includes a temporary real Hexo/Nunjucks generate) - CSS/JS sources →
npm run buildand include regenerated minified assets in the change set - Pure function / gate changes (MathJax protect/load, word-count display, etc.) → extend
test/in the same change set
Also:
- No separate lint/format script.
- The integration smoke test covers the default temporary host; for host-specific render checks still run
hexo clean && hexo generatein that site. - Docs-only changes: tests optional unless nearby tested behavior is described.
- Treat build/test failures, template errors, missing i18n keys, and broken config defaults as blockers.
Config, i18n, and security
- Prefer optional keys with safe defaults; missing optional keys must not throw.
- Do not remove/rename config without docs and migration notes.
- Treat copied
_config.shiro.ymlas possibly older than defaults. - Breaking: renaming/removing top-level keys in
_config.yml(see that file for the current set). - Release-coupled: default giscus theme URL embeds
hexo-theme-shiro@<version>— bump with package version in_config.yml,README.md, andREADME_CN.md. - giscus dark: one CSS with
@media (prefers-color-scheme: dark); host sets.giscus-frame { color-scheme }fromhtml[data-theme](comments.css). Paintiframe.style.colorSchemewhen the iframe mounts and whendata-themechanges — no second theme URL. - Do not commit secrets, analytics IDs, Disqus shortnames, giscus IDs, or private values.
- Treat config-rendered attributes/URLs as untrusted; be careful with external integrations (giscus, GA, LightGallery, Pagefind, CDN versions). Optional SRI hashes must be valid
sha256|384|512-…digests; invalid values are ignored.
PR, commits, and release
PR: focused; summarize user-visible changes; list npm test / npm run build / Hexo checks; note config, i18n, docs, and UI verification.
Commits: Conventional Commits — <type>(optional-scope): description; imperative, lowercase subject ≤72 chars; scopes like toc, search, readme. Breaking: ! and/or BREAKING CHANGE: footer.
Release: .npmignore excludes tests, build-only sources/tools, and maintainer docs; run npm pack --dry-run before tagging and keep runtime scripts/lib/** plus generated *.min.css / *.min.js in the package. Align package.json, package-lock.json, and every documented hexo-theme-shiro@<version> URL:
node -e "const p=require('./package.json').version,l=require('./package-lock.json'); if (l.version !== p || l.packages[''].version !== p) process.exit(1); console.log(p)"
grep -RnE 'hexo-theme-shiro@[0-9]+\.[0-9]+\.[0-9]+' _config.yml README.md README_CN.md