This article is both a complete usage tutorial for the matery theme’s layout and page features and an automated testing playground.
Coverage: post layout features, sidebar widgets, responsive breakpoints, galleries, search, comments, dark mode, encryption, RSS/sitemap, PWA.
Automated tests (tools/tests/release-test.sh) assert the rendering markers labeled below.
1. Post Layout Features
1.1 Prev/Next Navigation
Purpose: Display prev/next post cards at the bottom of a post to guide continuous reading.
Configuration (theme _config.yml):
# Prev/next navigation at the bottom of a post (auto-sorted by date)
prenext:
enable: trueEffect: Two cards at the end of the post showing adjacent posts’ titles + thumbnails, with a gradient background on hover.
Test assertion: The post page contains the
#prenext-postscontainer.
1.2 Copyright Notice
Purpose: Show copyright and license information at the bottom of a post, noting the author and reprint rules.
Configuration (theme _config.yml):
post:
copyright: true # Show copyright notice
license: This article is licensed under CC BY-NC-SA 4.0Effect: A grey copyright card at the bottom of the post with author, link, and license rows.
Test assertion: The post page contains a copyright notice container (
#copyrightor.post-copyright).
1.3 Outdated Content Notice
Purpose: Show a “content may be outdated” notice at the top of old posts, graded by days (warning/error styles).
Configuration (theme _config.yml):
post:
outdate:
enable: true
warning_day: 365 # Show warning-style note after 365 days
error_day: 3650 # Show error-style note after 3650 daysEffect: An outdated notice note at the top of the post, more prominent the older the post.
Test assertion: The post page contains an outdated notice element (
outdate-related class).
1.4 TOC
Purpose: Display chapter navigation in the right sidebar (or sidebar); click to jump to the corresponding heading, with current chapter highlighted while scrolling.
Configuration (theme _config.yml, top-level toc):
# TOC (top-level config, shared by every layout that has a TOC: post / wiki / msg / bb)
toc:
enable: true
headingSelector: "h2,h3,h4,h5,h6" # heading levels included in the TOC
collapseDepth: 0 # collapse depth (0 = all collapsed)Effect: Desktop: floating TOC tree on the right, highlights on scroll; Mobile: collapsed into the sidebar.
2026-09-25 update: the config moved from
post.tocto top-leveltoc, and the wiki page now shares the same config as the post page (previously wiki hardcodedh1, h2, h3and ignored the config entirely). The misleadingshowToggleBtn(it only gated whether the button’s click handler was registered — not a visibility switch) and the unimplementedplacement(dead config) were removed.
Test assertion: The post page contains the
#toc-contentTOC container.
1.5 Advertisement Slots
Purpose: Reserved ad card slots below the TOC (card format).
Configuration: The ad card is rendered by _partial/widget/advertisement.ejs, with content maintained in the template (post 2026-09-08 directory restructuring position).
Effect: Ad card (.tgCard) displayed below the sidebar TOC.
Test assertion: The post page contains the
.tgCardad container.
1.6 Word Count & Reading Time
Purpose: the post header article-info shows the per-post word count and estimated reading time; the footer shows the site-wide total.
Configuration (theme _config.yml, post section):
post:
wordcount:
enable: true # show per-post word count
min2read:
enable: true # show reading time
awl: 1 # average word length: 1 char = 1 word for CJK
wpm: 300 # words per minute: 250-350 suits CJK technical contentImplementation (themes/matery/scripts/helpers/wordcount.js): three built-in helpers replace the former hexo-wordcount plugin — same names and signatures, so no template changed:
| Helper | Purpose |
|---|---|
wordcount(page) | per-post count (CJK characters + English words) |
min2read(page) | reading time in minutes = floor(count / (awl × wpm)) + 1 |
totalcount(site) | site-wide total (posts + wiki pages; i18n duplicates de-duplicated; encrypted posts counted by plaintext post.origin) |
Effect: article-info shows “word count · reading time”; the footer shows the site-wide total (wiki pages and encrypted posts included).
Test assertion: posts expose the
article-infoword-count / reading-time markers; the three helpers’ behaviour is covered bytools/tests/unit/helpers.test.js.
⚠️ Historical bug (fixed 2026-09-22): the old default
awl: 2 / wpm: 60equals 120 chars/min — tuned for English — so a 520-character Chinese post showed 5 min; with the newawl: 1 / wpm: 300the same post shows 2 min.
1.7 Update Time (“Updated on”)
Purpose: the post header shows when the article was last modified (Updated on), distinct from Published on.
Data source (fixed 2026-09-26): the source file’s git last-commit time — neither the file mtime nor the publish date.
| Candidate | Why not |
|---|---|
file mtime | reset by git checkout/clone or bulk scripts (measured: all 142 _posts mtimes fell in the same second) → every post shows the same day |
date (publish time) | cannot reflect edits; yields “Updated on == Published on” |
| git commit time ✅ | frozen at commit time, immune to any working-tree operation |
Implementation: scripts/filters/post-updated.js overwrites post.updated in before_generate (before every generator) using the git time from scripts/utils/git-time.js; an explicit updated: in front-matter takes precedence.
Config (root _config.yml):
updated_option: 'date' # fallback only — 'empty' makes the third-party sitemap template crash on post.updated.toDate()Effect: the post shows “Updated on …” (different per post); posts never edited are hidden by the compare_date(page.date, page.updated) guard.
Test assertion:
tools/tests/unit/unit-updated.test.js(4 cases: CJK path hits / missing returns null / overwritten as moment / author value preserved); the post page containsdatetime="…" update.
2. Sidebar Widgets
2.1 Category Radar
Purpose: The /categories/ page uses a canvas radar chart to display post count distribution across categories.
Effect: Canvas radar chart at the top of the categories page, one dimension per category.
Test assertion:
/categories/contains a<canvas>element.
2.2 Tag Cloud
Purpose: The /tags/ page displays all tags in chip style; click to navigate to the corresponding tag page.
Effect: Colorful rounded chips on the tags page, highlighted on hover.
Test assertion:
/tags/contains.tag-chips .chip.
2.3 Word Cloud
Purpose: The /tags/ page uses an ECharts word cloud (2026-09-05 custom spiral → echarts-wordcloud@2, fixing 30s timeout); tag size/color based on weight.
Effect: ECharts word cloud on the tags page (continuous font size range [14,56] ∝ post count; container height auto-calculated as max(theme.wordcloud.minHeight=500, n×theme.wordcloud.heightPerWord=2.2) — zero-config auto-calculation; title Tag Word Cloud i18n; light/dark mode via Matery.echarts.init auto-switch).
Test assertion:
/tags/contains#tag-wordcloudcontainer + echarts loaded and only 1 instance + light/dark switch dispose→reinit (tools/tests/wordcloud.test.jsL2g).
2.4 Archive Timeline
Purpose: The /archives/ page archives all posts in a year/month timeline.
Effect: Vertical timeline with dots + collapsible years.
Test assertion:
/archives/contains#cd-timeline.
2.5 About Page Statistics (postCharts)
Purpose: The /about/ page uses three ECharts charts to display site statistics (post trend / category distribution / tag distribution).
Effect: Three groups of canvas charts on the about page (#posts-chart / #categories-chart / #tags-chart), auto-adapting to light/dark mode.
Test assertion:
/about/contains the#postChartscontainer + multiple<canvas>elements.
2.6 ArtiTalk (Optional, Disabled by Default)
Purpose: Embed the ArtiTalk microblog component on the /about/ page (a LeanCloud-based personal status wall for posting short updates).
Configuration (theme _config.yml):
artitalk:
enable: false # Disabled by default
appId: # LeanCloud App ID
appKey: # LeanCloud App KeyEffect: Renders a status list on /about/, requires LeanCloud backend. Disabled by default; enable by filling in appId/appKey.
Test assertion: Does not render in the default state.
2.7 My Projects (myProjects)
Purpose: Display a card list of personal projects on the /about/ page (project name / icon / description / link).
Configuration (theme _config.yml):
myProjects:
enable: true
data:
ProjectName:
icon: fab fa-linux
iconBackground: 'linear-gradient(...)'
url: https://example.com
desc: Project descriptionEffect: Three-column project cards on the about page, with icons + gradient background + title + description.
Test assertion:
/about/contains the.my-projectscontainer.
2.8 My Skills (mySkills)
Purpose: Display skill progress bars on the /about/ page (skill name / percentage / color gradient).
Configuration (theme _config.yml):
mySkills:
enable: true
data:
C:
background: 'linear-gradient(to right, #2196F3 0%, #42A5F5 100%)'
percent: 90%Effect: Two-column skill bars on the about page, with colorful gradient backgrounds + percentage numbers.
Test assertion:
/about/contains the.my-skillscontainer.
2.9 Archive Calendar (postCalendar)
Purpose: Display a calendar heatmap on the /archives/ page showing monthly post publication counts.
Configuration (theme _config.yml):
postCalendar: true # Show post calendar widget on the archives pageEffect: Calendar component (#post-calendar) on the archives page; color intensity represents post count.
Test assertion:
/archives/contains the#post-calendarcontainer.
2.10 Sidebar Advertisement (advertisement)
Purpose: Display ad cards (text / image / link) below the TOC on post pages.
Configuration (theme _config.yml):
post:
advertisements:
- id: "ad-1"
text: "Ad copy"
link: "https://example.com"
background: "#ff6666"
background_dark: "#8b3a3a"
enable: trueEffect: Renders .tgCard ad cards below the TOC; supports dual color values for dark mode. Outputs nothing when no ads are enabled.
Test assertion: Post page contains the
.tgCardad container.
3. Responsive Breakpoints
Configuration (themes/matery/source/css/_variables/base.styl)
$breakpoint-sm: 368px // Extra small (phone portrait)
$breakpoint-md: 768px // Tablet / phone landscape
$breakpoint-lg: 992px // Desktop
$breakpoint-fun: 1400px // Effects enable threshold (large screens)Layout at Each Breakpoint
| Breakpoint | Layout |
|---|---|
<768px | Nav collapses to hamburger menu, content single column |
768–992px | Tablet layout, sidebar collapsed |
>992px | Desktop three-column (content + sidebar + TOC) |
>1400px | Effects enabled (sakura/ripples, etc.) |
Implementation effect: Bootstrap/Materialize grid auto-adapts; sidebar auto-hides on narrow screens; TOC collapses on mobile.
Test assertion:
<768pxnav collapses;>992pxdesktop three-column;>1400pxeffects enabled.
4. Content Width Unification (Added 2026-09-07)
Design Principle
“Percentage + Max-width”: Content area width: 90% (relative to screen) + max-width steps (readability limit). Not pure 90% — at 4K, 90% = 3456px is unreadable.
Width Steps (_variables/base.styl)
| Breakpoint | Standard (single-column) | Wide (multi-column) |
|---|---|---|
| FHD 993-1920px | 1125px | 1250px |
| 2K 1921-2560px | 1525px | 1750px |
| 4K 2561px+ | 1525px (capped) | 2250px |
Selection Rules (mixin content-container(wide))
- Single-column (content without sidebar):
content-container()→ standard width - Multi-column (content + sidebar/TOC):
content-container(true)→ wide width - No hand-written max-width in layout (unified via mixin)
Implementation effect: Wiki (multi-column) 4K content column 899px → 1500px (+67%); same width steps as post-container.
Automated testing: tools/tests/content-width.test.js (L2m) covers: W1 4K wiki==post-container / W3 FHD / W4 2K / W5 single-column capped at 1525px.
FA Resource Version Stamp ({urlVersion})
Purpose: Ensure browsers/CDN fetch new resources after a version release, avoiding stale caches.
Mechanism: Resource URLs carry a version stamp placeholder {urlVersion} (e.g., /libs/awesome/webfonts/fa-solid-900.woff2?v=13.13). During build, tools/cicd.sh sed-replaces it with the actual version number (from tools/.VERSION).
Affected resources (userConfig/_config.tmp.yml, static_prefix.libs section):
| Resource | Config Key | Example |
|---|---|---|
| FA WebFont | faBrands/faRegular/faSolid | /libs/awesome/webfonts/fa-solid-900.woff2{urlVersion} |
| Theme CSS | matery/mycss/main | /css/main.css{urlVersion} |
| Highlight CSS | highlight/highlightDark | /css/highlight.css{urlVersion} |
No manual action required: Version is injected automatically by CI. Local preview (hexo server) does not replace the placeholder; resources have no version suffix (functionality unaffected).
⚠️ Do not manually edit the
{urlVersion}placeholders inthemes/matery/_config.yml— that file is a build artifact overwritten byuserConfig/_config.tmp.ymlduring CI.
5. Multilingual Fallback Mechanism (Added 2026-09-08, Revised 2026-09-10)
Language Configuration (preference section)
default_lang: Default language (no prefix path), shared by all componentsfallback_lang: Fallback intermediate layer — when the requested language doesn’t exist, fall back to this language firstlang_meta: Language capability table (9 languages) +enabledswitch (disabled languages don’t generate fallback pages / don’t appear in dropdown)
Fallback Chain (Three Layers)
Requested language exists? → Use it
→ Doesn't exist → fallback_lang exists? → Fall back to fallback_lang
→ Doesn't exist → default_lang exists? → Fall back to default_lang
→ Doesn't exist → Any available languageBehavior by Page Type (Revised 2026-09-10)
| Page Type | Multilingual Version | Notice Banner |
|---|---|---|
| Post | ✅ If translation exists, use it; if not, generate via fallback chain | ✅ Show “xx doesn’t exist, displaying yy language version” |
| Wiki page | ✅ Same as above | ✅ Same as above |
| Aggregation page (tag/category/archive) | ✅ Generate per enabled languages | ❌ Generate only, no notice |
| Standalone page (about/musics/movies/friends/msg/bb/galleries etc.) | ✅ Generates /{lang}/{page}/ (UI text goes through languages/{lang}.yml i18n) | ❌ None |
| Homepage | ✅ /en/, /en/page/N/ | ❌ None |
Standalone pages (including gallery sub-pages and historical pages with
layout:'post'in the pages collection, such as dash) are generated bystandalone-page-generator.jsusing an exclusion method (layout ∉ {wiki, tags, categories, 404} → generate; new standalone pages are automatically covered without registering to a list).
Real Translation Mapping (post / wiki)
Translation files share the same filename as the default language, placed in the language subdirectory:
| Type | Translation Source Path | front-matter |
|---|---|---|
| Post | source/{lang}/_posts/{same-name}.md | Keep the same abbrlink + add lang: en |
| Wiki | source/{lang}/docs/{same-name}.md | Same (wiki pages have no abbrlink, mapped by same-name path) |
Chinese-English association has two modes: with abbrlink plugin → same abbrlink associates; without plugin → same-name file association (with filesystem fallback check). With real translation → generates /{lang}/posts/{abbrlink}/ real content page; without translation → fallback chain generates.
Note: Hexo natively only recognizes
source/_posts/(en/_posts/is ignored); article translations are generated by the theme’si18n-post-generator.js.
Title i18n (page_title_i18n, 2026-09-10)
<title> and the cover headline (bg-cover-content.ejs) share page_title_i18n from scripts/helpers/page-title.js: tags/categories/about map by title, the rest map by layout to i18n keys; when it returns null the caller falls back to the original title — multilingual page titles switch with the language.
Client-Side Language Strategy (Scheme A: site-wide follow, 2026-09-10)
| Scenario | Behavior |
|---|---|
| First visit | No local preference and browser language matches an enabled language → popup asking to switch; if accepted, remember and redirect; if rejected, remember default language (won’t ask again) |
| Panel switch | Language dropdown selection → write local preference + redirect to corresponding language version |
| Page load / refresh | Has local preference → load corresponding language version directly (head.ejs inline redirect, before render with no flash); crawlers skipped (SEO); no multilingual version path skipped (prevent 404) |
| Panel display | Language dropdown shows local preference (localStorage.wiki_lang) |
Added 2026-09-27 (i18n batch)
| Item | Change |
|---|---|
| 404 page | Switched to client-side language detection (single file) — copy is chosen dynamically from the browser language / local preference; no longer statically generated per path (/en/404.html etc. retired) |
| archives aggregate page | Unified pagination across all languages — no longer diverges per language |
| Encrypted page language | Encryption prompt / button copy follows the page language (hbe-bundle fallback copy goes through MATERY_I18N, E7) |
| Runtime copy | Search / dialog / codeLang runtime copy is i18n-ised (P0/P1) |
| In-content links | url_for_lang points to the matching language version (E2) |
Test coverage:
lang-client-strategy.test.js(client language strategy, 7 assertions, L11);i18n.test.js(MATERY_I18N injection + button copy, L11)
Language Prefix Rules (url_for_lang)
Resources (css/js/libs/medias/images/fonts etc.) and static files (xml/txt/json etc.) do not get language prefixes; all other content paths always get the prefix. Exemption rules come from a single source: scripts/utils/url-lang-blacklist.js. Standalone page navigation thus carries the /en/ prefix pointing to the corresponding language version.
Implementation Effect
Visiting /de/docs/setup/ (de enabled, no translation): if en translation exists → content uses en + notice banner “de doesn’t exist, displaying en”; if no en → content uses zh-cn + notice banner. Standalone page navigation (url_for_lang) doesn’t add language prefix, always points to default language version.
Automated testing: tools/tests/lang-fallback.test.js (L13) covers fallback chain 4 levels + configuration compatibility/filtering/validation (13 assertions); tools/tests/release-test.sh L2x covers MATERY_LANG injection / fallback pages / 9 standalone pages multilingual (including gallery sub-pages and dash) 200 / aggregation page multilingual; tools/tests/unit-standalone.test.js (L13, 22 assertions) covers standalone page exclusion judgment + language prefix exemption; tools/tests/lang-client-strategy.test.js (L11, 7 assertions) covers client-side language strategy (site-wide follow / crawler skip / panel display).
6. Wiki Multilingual (docs/api/tutorial sub-sites, revised 2026-09-10)
Purpose
Wiki documentation sub-sites (docs/api/tutorial) with three-column layout + Chinese/English bilingual + fallback mechanism, sharing the same multilingual system as blog posts.
Core Conventions (required front-matter)
---
layout: wiki # Required, otherwise the three-column layout is not rendered
wiki: tutorial # Required, sub-site name (docs/api/tutorial)
---Directory Structure
| Language | Path |
|---|---|
| Chinese (default) | source/{wiki}/ (no prefix) |
| English | source/en/{wiki}/ (/en/ prefix) |
Fallback Mechanism
No English version → /en/... falls back to showing Chinese + top notice banner (“no en version”) + noindex.
Sidebar (source/_data/{wiki}-sidebar.yml)
# source/_data/docs-sidebar.yml
docs:
- key: guide
children:
- key: writing
title: Writing Guide- Entry keys are English snake_case, display text goes through
languages/*.ymltranslations - Whole-wiki encryption: configure
encrypt_passwordat the top of the sidebar (unifiedencrypt_prefix: message/placeholder/button_text optional) → all pages under that wiki are encrypted, enter the password to decrypt and render; after decryption the derived key (dk) is stored in localStorage (wiki.<wiki name>, TTL 1 day), other pages of the same wiki auto-decrypt without re-entering;encrypt_-prefixed config keys are skipped during sidebar rendering (not shown as table of contents items)
Internal Links
Absolute paths + .html (/tutorial/guide/writing.html), language prefix added automatically.
Section Numbering
Wiki pages with hand-written numbering must set closeAutoTocNum: true (same as the blog, prevents double numbering from CSS auto-numbering).
Adding a New Wiki Page
- Write Chinese
source/{wiki}/xxx.md(front-matterlayout: wiki+wiki: <sub-site>) - Add English
source/en/{wiki}/xxx.md - Update
source/_data/{wiki}-sidebar.yml - Add translation keys for 9 languages (
languages/*.yml) hexo cleanrebuild
Wiki Tag-Component Styles & Layout Width
Wiki pages have additional layout considerations for tag components (note / ::: containers / tabs):
Style scoping: Tag component CSS is entirely scoped under .post-container descendant selectors. The wiki <main> must carry both post-container and wiki-main classes for components to receive styles. If new tag component CSS is added without a .post-container prefix, it will be invisible on wiki pages.
Layout width: Wiki <main> requires explicit width: 100% (_base/media.styl .wiki-main { width: 100% }), otherwise the flex item’s auto margin triggers shrink-to-fit, causing inconsistent widths across wiki pages (pages with less content become narrower). max-width is controlled by the global .post-container rule, keeping all wiki pages at uniform width.
Test verification:
readmode-integration.test.jsasserts wiki<main>containspost-containerclass and.wiki-layoutwidth is consistent across wiki pages.
Automated Testing
tools/tests/release-test.sh L2b: en page 200 + fallback notice banner + comment path normalization + default language no prefix; L2y: wiki encrypted page decryption + sidebar has no encrypt_ junk titles.
7. Gallery Feature
Data Source (source/_data/galleries.yml)
Album Name:
photos: Single image object | string path | directory path # Three forms
cover: Cover image
description: DescriptionPage Structure
/galleries/— Album list page (cards + covers)/galleries/<name>/— Single album page (lightbox + caption)- Encrypted album: Add
passwordto front-matter, requires decryption to enter
Implementation Effect
List cards + inner page lightGallery lightbox browsing; encrypted albums display after decryption.
Test assertions:
/galleries/album list/galleries/2020/lightbox + caption/galleries/private/encrypted decryption UI
8. Search Feature
Configuration (root _config.yml search block)
Three engines: local (local search.json) + algolia (Algolia API) + pagefind (Pagefind index).
Interaction
- Popup: Opens on click/focus of the navbar search box;
Ctrl+K(Mac⌘+K) keyboard shortcut opens it - Engine switching: Dropdown inside the popup switches between local / Algolia / Pagefind
- Multi-language filtering: Filters by language prefix + deduplicates via fallback (
applyLangFilter) - Unified highlighting: All three engines inherit the same base class, sharing
_highlight/_escapeHtml(consistent result highlighting)
Implementation Effect
- Navbar search box (
#searchInput) - Search popup with real-time results
/search.jsonis the local search data source
Test assertion: Page contains
#searchInput;/search.jsonis accessible; popup can be opened.
9. Comment System
Configuration (root _config.yml)
comment: waline # Comment engine
waline:
serverURL: https://waline.17lai.site
...Page front-matter control: comment: waline (post/message/friends/timeline pages).
Implementation Effect
Waline comment box (Markdown/Emoji/likes/admin backend).
Test assertion: Post/msg/friends/bb pages contain
#comments; waline initialization script exists.
10. Dark Mode
Configuration (theme _config.yml)
dark_mode:
enable: true
default: auto # auto | light | dark- auto: First follows
prefers-color-scheme, then auto-enters dark between 18:00–6:00 local time - Toggle button: Manual switch overrides default mode
Implementation Effect
[data-user-color-scheme="dark"] attribute drives full CSS variable switching; images/charts adapt automatically.
Test assertion: CSS contains
[data-user-color-scheme="dark"]rules; toggle button can switch light/dark.
11. Encrypted Posts
Configuration (root _config.yml)
encrypt:
tags:
- Private # Encrypt by tag
default_abstract: This post is encrypted
...Posts only need the encryption tag; readers must enter a password to view.
Implementation Effect
Post page displays decryption UI (#hbePass); content renders after correct password entry.
Test assertion: Tag-encrypted post contains
#hbePassdecryption UI.
Unified Refresh After Decryption (Added 2026-09-09)
After encrypted content is decrypted, components in the content need re-initialization (before decryption it’s ciphertext, DOM lacks these elements):
| Component | Action After Decryption |
|---|---|
| Code blocks (code-area) | codeWidget() scans new pre elements to generate toolbar (language label/copy/collapse/fullscreen) |
| Image lightbox | articleInit() wraps img → .img-item + lightGallery binding (shared for post/wiki) |
| Mermaid diagrams | events.refresh() triggers refresh callback to re-render (data-processed idempotent) |
| Layout (AOS/Masonry) | boot.refresh() → refreshLayout re-arranges |
Unified trigger point: hbe-bundle onSuccess (all encryption scenarios: post/gallery/wiki/snippet shared). Each listener retains only its unique handling:
- Post: tocbot reinit (post TOC)
- Wiki: TOC encryption state removal + tocbot reinit
- Gallery: initGallery()
Idempotent protection: codeWidget data-code-processed, mermaid data-processed, articleInit destroys old instance and rebuilds — repeated calls don’t produce duplicate elements.
Test assertion: After decryption,
.code-toolbarcount equals.code-areacount; image wrapping.img-item+ containerlg-uidattribute exists.
12. RSS / Sitemap
Configuration (root _config.yml)
feed: # hexo-feed generation
type: atom
path: atom.xml
sitemap: # hexo-generator-sitemap
path: sitemap.xmlOutput
/rss.xml,/atom.xml,/feed.json— Subscription feeds/sitemap.xml,/baidusitemap.xml— Search engine crawling
Test assertion:
/rss.xml,/atom.xml,/feed.json,/sitemap.xmlare all accessible.
13. PWA
Configuration (root _config.yml + userConfig/sw.tmp.js)
pwa:
enable: true
manifest: ... # Name/icons/theme colorsw.tmp.js is the Service Worker template; CI injects offline cache version 17lai-cache-{timestamp} to generate source/sw.js.
Implementation Effect
- Installable (manifest)
- Offline caching of static assets
- Auto-refresh on version update
Test assertion:
/sw.jsis accessible, SW registration works (localhost exempted).
SW Version Comparison & Auto-Refresh
The CI-injected cache key 17lai-cache-{timestamp} is treated as a new version whenever it changes. When registering the SW, updatefound (new SW installed) and controllerchange are monitored; the old and new cacheStorageKey are compared: if changed, the page auto-refreshes once (no prompt, the user doesn’t need to manually refresh). sw.tmp.js uses skipWaiting on install + clients.claim() on activation to take over immediately, and cleans up old 17lai-cache-* caches.
14. SEO Structured Data (JSON-LD + Open Graph)
Purpose
Provide structured metadata for search engines and social platforms: JSON-LD rich results + Open Graph / Twitter Card share previews.
Configuration (layout/_partial/common/structured-data.ejs + head.ejs)
No standalone toggle; output by head.ejs:
- JSON-LD
@graphsingle script:WebSite(with SearchAction search box) +Person(author) +Organization(site); post pages addBlogPosting @idcross-references (publisher/author point to#organization/#person), descriptions truncated afterstrip_html+ 210 characters- OG/Twitter:
og:type/title/url/site_name/description/image+twitter:card=summary_large_image
Implementation Effect
- Page contains
<script type="application/ld+json">rich result data - Sharing to social platforms shows title/description/cover image preview
Test assertion: Page contains
application/ld+json; post page containsBlogPosting; containsog:type/twitter:cardtags.
15. Design Tokens & CSS Variable Architecture
Purpose
Unify design tokens (border-radius / shadows / transitions / spacing / font sizes) through a three-layer delivery chain to CSS custom properties for site-wide reference, avoiding hardcoded values.
Configuration (theme _config.yml design section)
design:
radius_sm: 3px # Buttons / small labels
radius_md: 8px # Cards / notes
radius_lg: 12px # Modals
shadow_md: "0 2px 5px 0 rgba(0,0,0,0.16)"
shadow_lg: "0 5px 11px 0 rgba(0,0,0,0.18)"
transition_fast: 0.2s
space_1: 4px # 4px base increment
font_size_md: 1.125rem # rem-based, scales with --font-scaleThree-Layer Delivery Chain
_config.yml design.* → theme-config() → Stylus variables → :root CSS custom properties → components var(--*)- Scheme layer:
source/css/_schemes/matery.styl(aligned with NexT_schemes/mechanism, extensible for future schemes) - Font size: root font size
calc(var(--font-size-base) * var(--font-scale)), preference panel adjusts--font-scalefor site-wide rem proportional scaling
Implementation Effect
Components reference tokens uniformly (consistent border-radius / shadows / spacing); changing one value in the design section applies site-wide.
Test assertion: CSS contains
--font-scalerule and root font sizecalc(... * var(--font-scale)).
⚠️ Known Pitfalls
Pitfall 1: Double Section Numbering
Symptom: Headings render as “1. 1. Overview” (two numbers).
Cause: Theme CSS auto-adds numbers to h2–h6 by default; hand-written numbers (e.g. ## 1. Overview) stack on top.
Fix: Pages with hand-written numbering add closeAutoTocNum: true in front-matter; pages without hand-written numbering must not add this field.
This article already correctly sets
closeAutoTocNum: true(front-matter line 19).
Pitfall 2: Encryption ↔ TOC Conflict
Symptom: When a wiki is encrypted as a whole, the TOC is hidden by design.
Cause: In encryption mode the TOC component is skipped (intended behavior).
Fix: Test assertions must not hard-code “TOC must exist”. Encrypted wiki pages not showing TOC is normal behavior, not a bug.
Pitfall 3: Wiki English Pages Missing lang: en
Symptom: Sidebar links point to /docs/ instead of /en/docs/.
Cause: wiki_sidebar uses page.lang || default_lang; omitting lang: en falls back to the default language path.
Fix: All pages under source/en/tutorial/ must include lang: en. Verify: for f in source/en/tutorial/**/*.md; do grep -L 'lang: en' "$f"; done.
Pitfall 4: _config.tmp.yml Comment Lines Must Be Preserved
Symptom: Attempted to delete comment lines to slim the file, was vetoed and fully reverted.
Cause: User explicitly declared comment lines are useful reserves (may be restored in the future).
Fix: Never delete or modify comment lines in userConfig/_config.tmp.yml.
Pitfall 5: Config Changes Require Clean Build
Symptom: Changed configuration but the page didn’t update.
Cause: Hexo’s caching mechanism doesn’t auto-detect configuration changes.
Fix: After changing config or source/_data/*, you must run npm run clean && npm run build. Build logs showing INFO Deleted database. and INFO Deleted public folder. confirm clean ran successfully.
Pitfall 6: source/_data/* Incremental Cache Misjudgment
Symptom: After modifying source/_data/friends.yml and other data files, the page is blank or data doesn’t update.
Cause: Hexo’s incremental cache incorrectly judges “cached, unchanged” (Cache hash updated but Data model not written).
Fix:
- Local:
rm -f db.json && npm run build - Container:
tools/cicd.sh -d(includes clean + generate + config injection) - In-container
pm2 restartis not enough: pm2 caches old db.json content in memory
Appendix: Layout Feature Quick Reference
| Feature | Location | Key Configuration | Automated Assertion Marker |
|---|---|---|---|
| Prev/next | Post bottom | prenext | #prenext-posts |
| Copyright | Post bottom | post.copyright | #copyright |
| Outdated notice | Post top | post.outdate | outdate |
| TOC | Right sidebar | toc | #toc-content |
| Ad slot | Below TOC | post.advertisements | .tgCard |
| Category radar | /categories/ | Auto | canvas (Matery.echarts light/dark switch) |
| Tag cloud/word cloud | /tags/ | Auto | .tag-chips / #tag-wordcloud |
| ArtiTalk | /about/ | artitalk.enable: false (disabled by default) | LeanCloud backend |
| My Projects | /about/ | myProjects.enable: true | .my-projects |
| My Skills | /about/ | mySkills.enable: true | .my-skills |
| Archive calendar | /archives/ | postCalendar: true | #post-calendar |
| Online music | /musics/ | musics + failover | .aplayer (tools/tests/musics-integration.test.js L2i) |
| Archive timeline | /archives/ | Auto | #cd-timeline |
| About page charts | /about/ | Auto | canvas |
| Galleries | /galleries/ | galleries.yml | Lightbox + encryption UI |
| Search | Navbar | search | #searchInput / search.json |
| Comments | Post/msg etc. | comment: waline | #comments |
| Dark mode | Global | dark_mode | data-user-color-scheme |
| Encryption | Tag-driven | encrypt.tags | #hbePass |
| RSS/Sitemap | Root paths | feed / sitemap | /rss.xml etc. |
| PWA | Global | pwa + sw.tmp.js | /sw.js |
| SEO | <head> | structured-data.ejs + head.ejs | application/ld+json / og:type |
| Design tokens | Global | design section + _schemes/ | --font-scale |
References
系列教程
Hexo 主题功能测试系列
- Hexo主题功能测试-布局与页面篇 —— 页面布局、导航、侧边栏、卡片等结构功能
- Hexo主题功能测试-交互视觉篇 —— 代码高亮、图片灯箱、动画、明暗切换等交互
- Hexo主题功能测试-内容tag篇 —— note / tabs / timeline / label / button 等 tag 插件
- Hexo主题功能测试-Markdown语法篇 —— Markdown 渲染与扩展语法
- Hexo主题功能测试-自动化测试篇 —— 主题功能的自动化测试方案

