Layout & Pages
This page covers the layout & pages features of the matery theme: post layout features, sidebar widgets, responsive breakpoints, galleries, search, comments, dark mode, encryption, RSS/sitemap and PWA. Each feature lists its config location and how to use it.
Config lives in two places: theme config
themes/matery/_config.yml(layout/appearance/interaction) and site config_config.yml(plugins/generators), marked per item below. Note: in this project the theme config file is overwritten by theuserConfig/_config.tmp.ymltemplate during CI builds — sync your changes to the template (see the Installation chapter).
Layout Core & Components
1. Post Layout Features
1.1 Prev/Next Navigation
Purpose: prev/next post cards at the bottom of a post, auto-picked by date order to guide continuous reading.
Enable (theme config, inside the post block):
post:
prev_next:
enable: trueEffect: two cards at the end of the post showing the adjacent posts’ titles and thumbnails, with a gradient background on hover.
1.2 Copyright Notice
Purpose: show copyright and reprint license at the bottom of a post, noting the author, original link and license.
Enable (theme config, inside the post block):
post:
copyright:
enable: true # show copyright notice
license: 'cc_by_nc_sa' # default reprint rule (sub-key of copyright, CC BY-NC-SA 4.0)The default license applies site-wide; a single post can override it with reprintPolicy in front-matter:
reprintPolicy: cc_by_nc # this post: Attribution-NonCommercialAvailable rules (9 total): cc_by, cc_by_nd, cc_by_sa, cc_by_nc, cc_by_nc_nd, cc_by_nc_sa, cc0, noreprint, pay.
Effect: a grey copyright card at the bottom of the post with author, link and license rows.
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).
Enable (theme config, inside the post block; note the current default is enable: false):
post:
outdate:
enable: true # set to true to enable
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.
1.4 TOC
Purpose: chapter navigation in the post sidebar; click to jump to the heading, current chapter highlighted while scrolling.
Enable (theme config, top-level toc section):
toc:
enable: true # global switch
headingSelector: "h2,h3,h4,h5,h6" # heading levels included in the TOC
collapseDepth: 0 # collapse depth, 0 = all collapsedDisable for a single post: add toc: false in front-matter.
Effect: sidebar TOC tree highlights on scroll on desktop; collapsed on mobile.
1.5 Advertisement Slots
Purpose: reserved ad card slots in the post sidebar (same area as TOC); multiple entries, freely toggleable.
Config (theme config, inside the post block; array, add/remove freely):
post:
advertisements:
- id: "ad-1"
text: "Recommended cheap VPS provider"
link: "https://example.com/your-aff-link"
image: "/medias_webp/cover/hexo.webp" # optional: card image
background: "#6670ff"
background_dark: "#3a4399"
colspan: 1 # card column width
enable: true # false hides itEffect: colored ad cards below the sidebar TOC, supporting light/dark background pairs.
2. Page-level Widgets
The following pages are rendered automatically by the theme generators — no config needed, they update with content:
| Page | Widget | Description |
|---|---|---|
/categories/ | Category radar | canvas radar chart showing post count distribution per category |
/tags/ | Tag cloud + word cloud | chip-style tag list; canvas word cloud with font size ∝ post count |
/archives/ | Archive timeline | vertical year/month timeline, dots + collapsible years |
/about/ | Site statistics charts | multiple canvas charts (posts/categories/tags/timeline etc.) |
/about/ | Site statistics numbers | post/category/tag counts (_partial/post/post-statis, auto-rendered) |
3. Responsive Breakpoints
Config (themes/matery/source/css/_variables/base.styl, Stylus variables):
$breakpoint-sm = 368px // extra small (phone portrait)
$breakpoint-md = 768px // tablet / phone landscape
$breakpoint-lg = 992px // desktop
$breakpoint-xl = 1200px // large desktop
$breakpoint-fun = 1400px // effects enable threshold (large screens)Layout behavior at each breakpoint:
| Breakpoint | Layout |
|---|---|
< 768px | nav collapses to hamburger menu, content single column, sidebar hidden |
768–992px | tablet layout, sidebar collapsed |
> 992px | desktop multi-column (content + sidebar + TOC) |
> 1400px | effects enabled (sakura/ripples etc.) |
The layout adapts via the Materialize grid — no manual maintenance needed.
Content Width Standardization (2026-09-07)
“Proportion + cap”: content area width: 90% + max-width tiers (readability caps).
Width tiers (_variables/base.styl, $content-max-* / $content-wide-max-*):
| Breakpoint | Standard (single-column) | Wide (multi-column) |
|---|---|---|
| FHD 993–1920px | 1125px | 1250px |
| 2K 1921–2560px | 1525px | 1750px |
| 4K 2561px+ | 1525px (cap) | 2250px |
Selection rule (mixin content-container(wide)):
- Single-column (no sidebar):
content-container()→ standard - Multi-column (sidebar/TOC present):
content-container(true)→ wide - Layouts must not hardcode max-width (always use the mixin)
wiki (multi-column) 4K content column grows from 899 to 1500px (+67%). Tested by
tools/tests/content-width.test.js(L2m).
4. Galleries
Data source (source/_data/galleries.yml, single source):
2020:
cover: /medias_webp/images/01.webp
description: New Year 2020
photos:
- url: /medias_webp/images/01.webp # form 1: full object (url/title/desc)
title: "New Year 2020"
- /medias_webp/images/02.webp # form 2: path only
- dir: /medias_webp/featureimages/ # form 3: directory, auto-scan all images
title_prefix: "Image"Create a gallery:
hexo new gallery <name>Generates source/galleries/<name>/index.md (front-matter only needs title + layout: gallery). Add password for encrypted galleries — visitors must decrypt to enter.
Effect: /galleries/ listing cards + covers; lightGallery lightbox on inner pages, image titles/descriptions shown as captions.
Feature Integration
5. Search
Config (theme config, search block, three engines):
search:
algolia:
enable: true # Algolia engine (applicationID/apiKey/indexName in theme config)
indexName: "blog"
local:
enable: true # local search: generator outputs /search.json at hexo generate
path: /search.json
field: post # search scope: post | page | all
content: true # index body content
pagefind:
enable: true # Pagefind static index (client-side search after build)
output_subdir: pagefindEffect: search box in the navbar with real-time results popup; /search.json is the local search data source; encrypted posts are automatically excluded from the index.
5.1 Three-Engine Config Keys and Default Engine
The authoritative key names come from the search block in themes/matery/_config.yml (the template userConfig/_config.tmp.yml is the same source):
| Engine | Config block | Key params | Notes |
|---|---|---|---|
| local | search.local.* | enable / path / field / content / format: striptags / preload / trigger / top_n_per_article | the theme generator outputs search.json at build time; preload/trigger are not yet wired up, so the index is in practice always fetched when the search popup first opens |
| Algolia | search.algolia.* | enable / applicationID / apiKey / indexName / content_length / excerpt_length / batch_size / hits.per_page / labels | apiKey is the front-end search key (safe to expose); the write key lives in deploy.algolia_write_key (read only during build) |
| Pagefind | search.pagefind.* | enable / output_subdir: pagefind | scans public/ at build time to produce a static index; fully client-side, no external service |
search.default(valuealgolia/local/pagefind) selects the default engine; if the configured default is not enabled the front end auto-degrades to the first enabled engine and logs aconsole.warn.- The Algolia client (
algoliasearch) is lazy-loaded: it is injected only when the search popup opens, not preloaded site-wide.
5.2 In-Page Engine Switching and Keyboard Shortcuts
(Source: themes/matery/layout/_partial/common/search.ejs, _partial/script/search.ejs)
- The search popup’s left-side engine icon
#searchEngineBtnopens a dropdown#searchEngineMenuto switch between local / algolia / pagefind; the dropdown button auto-hides when only one engine is enabled. - Results render in real time for the current engine; switching engines triggers an immediate re-search with the current input value; the active engine is persisted in
localStorage(keysearch_engine). - Ctrl+K / Cmd+K opens the search popup, Esc closes the menu; the popup uses the native
popoverand light-dismisses on outside clicks. - IME compatibility: input events skip IME composition mid-states;
compositionendfalls back to searching with the full value (shared by all three engines).
5.3 Multilingual Filtering and Unified Highlighting
(Source: themes/matery/source/js/search.js)
- Multilingual filtering: all three engines share
applyLangFilter()— results are grouped by path language prefix, and the group belonging to the user’s preferred language (localStoragekeywiki_lang, language list fromwindow.MATERY_LANG.langs) is returned first; missing translations fall back to the default language (default_lang); duplicates within a group are removed. - Unified highlighting: all three engines share the base class
_highlight()— keywords are escaped then wrapped in paired<mark class="search-keyword">tags (prevents XSS / unclosed tags); highlight HTML returned by Algolia is stripped by_safeHighlight()and re-highlighted locally. - Jump-to-page highlighting: result links append
?highlight=<keyword>; the post page’ssearch.ejstail script parses the parameter and highlights body text (skipping code blocks and links).
5.4 Pagefind Build Steps
Pagefind must scan public/ after hexo generate and before deployment to produce a static index. It runs during CI (tools/CI.sh, invoked by tools/cicd.sh):
npx --no-install pagefind --site public --output-subdir pagefind \
|| npx --yes pagefind@1.5.2 --site public --output-subdir pagefindThe locally installed version is preferred; if not present, the pinned version pagefind@1.5.2 is downloaded once (prevents npx from pulling a floating latest).
6. Comments
Config (theme config):
waline:
enable: true
serverURL: 'https://waline.17lai.site' # your Waline server
lang: zh-CN
dark: 'html[data-user-color-scheme="dark"]' # follow dark mode
login: enable
wordLimit: 1000
pageSize: 10Toggle per page via front-matter: comments: true / comments: false.
Effect: Waline comment box with Markdown/Emoji/likes/admin, auto-adapting to light/dark mode.
7. Dark Mode
Config (theme config):
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 - The manual toggle overrides the default (remembers user choice)
Effect: the [data-user-color-scheme="dark"] attribute drives the full set of CSS variables; images, charts and comment widgets all adapt.
8. Encryption
In-house HBE v4 (de-pluginized since 2026-08-09, replacing hexo-blog-encrypt): AES-256-GCM + PBKDF2 with 250k iterations, unified across posts / galleries / snippets / wikis. Front-end WebCrypto decryption; the hexo-blog-decrypt event triggers a theme refresh.
Config (the encrypt section of the theme config themes/matery/_config.yml):
encrypt:
enable: true
abstract: Something is encrypted, enter the password to view.
message: Hello, password required here.
tags: # tag-based encryption: posts with these tags encrypt automatically
- {name: 私人, password: "********"}
wrong_pass_message: Sorry, that password doesn't look right. Please try again.
autoSave: true # cache the key in localStorage, no password needed on refreshUsage (four encryption methods):
| Method | Usage | Notes |
|---|---|---|
| Tag encryption | Tag a post with one of the tags in encrypt.tags | Automatically encrypts the full post |
| Per-post encryption | password: "********" in front-matter | Full-post encryption |
| Content snippet encryption | {% encrypt password "title" "abstract" %}markdown{% endencrypt %} | Paragraph-level encryption, supports nested tags inside (2026-09-07 scheme Y) |
| Wiki-wide encryption | Add encrypt_password to {wiki}-sidebar.yml | One password for the entire wiki, shared DK across pages (2026-09-07) |
Wiki-wide encryption config (any {wiki}-sidebar.yml, e.g. source/_data/dev-sidebar.yml; unified encrypt_ prefix):
encrypt_password: "password for the entire wiki" # required
encrypt_message: "This document is password-protected" # optional: prompt text
encrypt_wpm: "Wrong password" # optional: error message
encrypt_placeholder: "Enter password" # optional: input placeholder
encrypt_button_text: "Unlock" # optional: button textEncrypts the content + right sidebar TOC; left navigation is preserved (acts as a title bar). Decrypting any page in the wiki unlocks all language versions (1-day TTL).
Effect: the post page shows a decryption UI (#hbePass); the body renders after the correct password is entered.
- Asset minification (2026-09-25): hbe encryption assets now live in
source/libs/hbe/and go through the static pipeline, sohexo-minifycompresses them (previously bypassed)
Wiki Page front-matter Checklist (2026-09-03)
Wiki pages (layout: wiki) support the same front-matter as posts. Common options:
| front-matter | Effect |
|---|---|
mermaid: true | Enable Mermaid flowcharts / sequence diagrams |
mathjax: true | Enable MathJax formulas |
toc: false | Disable the page TOC |
comment: false | Disable comments on this page |
anchorjs: false | Disable heading anchors |
treeview: true | Enable Prism treeview code-tree plugin |
autolinker: true | Enable Prism autolinker link plugin |
diffhighlight: true | Enable Prism diff highlight plugin |
echarts: true | Enable ECharts charts (requires chart containers in the body) |
password: "********" | Encrypt this page (same decryption UX as posts) |
Effect: the page shows a decryption UI (#hbePass); the body renders after the correct password is entered.
ECharts Charts (unified 2026-09-03)
- Enable: add
echarts: truein front-matter (loads libs echarts (latest, default v6.1.0) + echarts-gl 2.1.0) - Charts in posts:
{% echarts width height %}+ option JSON (width defaults to 100%, height to 400px; first arg is width, second is height) - Dark mode: auto-adapts (init with current theme, rebuilt on toggle)
- GL charts: options with grid3D/globe components work automatically (echarts-gl is loaded)
- Note: echarts 6 requires explicit
typeon xAxis (category/value)
Publishing & Standalone
9. RSS / Sitemap
Config (root _config.yml, hexo-feed + hexo-generator-sitemap):
feed: # independent switches for the three feed types
rss:
enable: true
output: "rss.xml"
atom:
enable: true
output: "atom.xml"
jsonFeed:
enable: true
output: "feed.json"
limit: 10
sitemap: # general sitemap
path: sitemap.xml
baidusitemap: # Baidu sitemap
path: baidusitemap.xmlOutput: /rss.xml, /atom.xml, /feed.json feeds; /sitemap.xml, /baidusitemap.xml for search engines.
10. PWA
Config (theme config + template):
pwa:
enable: trueuserConfig/sw.tmp.js is the Service Worker template: CI injects an offline cache version (17lai-cache-{timestamp}) and generates source/sw.js.
Effect: installable (manifest + theme color); offline caching of static assets; auto-refresh on version update.
10.1 SW Version Management Details
(Source: userConfig/sw.tmp.js, themes/matery/layout/_partial/common/head.ejs, tools/cicd.sh)
- Template → artifact: during CI,
tools/cicd.sh’sinject_pwa_sw()copiesuserConfig/sw.tmp.jstosource/sw.js, then usessedto inject placeholders; the artifact is published as/sw.jsat the site root. - Cache key / version injection: the template uses the literal
17lai-cache-{uniqueIdentifier}as the cache key (prefix17lai-cache-); CI replaces the placeholder with a build timestamp (date '+%Y%m%d%H%M%S', i.e.offline_version), producing a unique key like17lai-cache-20260913181700— a new version on every build. The same step injects{cdnUrl}/{cdnPathVersion}/{cdnPathLatest}/{urlVersion}placeholders. - Client auto-refresh (
head.ejs, registered as anasyncscript whentheme.pwa.enableis true,scope: '/'; skipped onlocalhostto avoid stale-cache hits during local development):- After registration, sends a
checkIdentifiermessage to the SW viaMessageChannel, retrieves the current cache version, and records the baseline. - On
updatefound→ new SWinstalled(afteraddAllcompletes), and oncontrollerchange(SW takes over), triggerscheckVersionAndRefresh(). - Only reloads (
window.location.reload(), delayed 500ms foractivateto finish) when the cache version changes; arefreshedguard ensures one reload per page, preventing Vercel redeploy-triggered reload loops — no popup, no manual refresh needed.
- After registration, sends a
- Activation and old-cache cleanup (SW template): the
installevent pre-cachescacheListthen callsself.skipWaiting(); theactivateevent iteratescaches.keys(), deletes old caches with the17lai-cache-prefix that don’t match the current key, re-runsaddAll, then callsself.clients.claim()to take over all pages. - Compressed backup: in release mode the uncompressed
sw.jsis backed up touserConfig/sw.js; after the build, the compressed version is backed up touserConfig/sw.min.js. In continue modesw.min.jsis reused to keep compressed artifacts consistent (falls back to the template version if missing).
11. Standalone Pages
The following pages have dedicated layout templates in the theme. Create the page under source/ (set layout in front-matter); the page body lives in the md file and the template renders it.
11.1 AV Page (layout: av)
Purpose: a media/resource-sharing page, structured like the About page — music player + video + my gallery + resource-sharing body.
Data sources (theme config):
theme.music(music player, see Interaction & Visual §9)theme.video(video player, see Interaction & Visual §10)theme.myGallery(my gallery, see Sidebar Widgets §8)page.content(page body, resource-sharing list)
⚠️ DEPRECATED (2026-09-12): the
avlayout had no practical use and has been removed
from the theme (themes/matery/layout/av.ejsdeleted;source/av/is no longer created).
The following is kept for historical reference only — do not create this page again.
Create (not yet created in this project):
hexo new page avsource/av/index.md front-matter:
---
title: AV Resources
layout: av
---11.2 Movies Page (layout: movies)
Purpose: an inspirational short-film page; the template hardcodes a Bilibili iframe player.
Data source: none — the video URL is hardcoded in the iframe src inside themes/matery/layout/movies.ejs; change the film by editing the template.
Create (already exists: source/movies/index.md):
---
title: Videos
layout: movies
---11.3 Message Board (layout: msg)
Purpose: a message board. Body (blog timeline, etc.) + letter envelope component + comments.
Comments toggle: the template injects comments via inject_point('pageComments'); enable with front-matter comment: waline (or comments: true).
Letter envelope component: _partial/social/letter renders automatically at the end of the body — envelope-open animation + message form hints (styles in letter.styl, copy via i18n letter.*).
Create (already exists: source/msg/index.md):
---
title: Message Board
layout: msg
toc: false # disable TOC (template checks theme.toc.enable && page.toc !== 'false')
comment: waline
---11.4 Friends Page (layout: friends)
Purpose: friend-link card wall (Masonry waterfall) + page body + comments.
Data source (source/_data/friends.yml, a plain array read by the template via site.data.friends):
- name: Friend name # required, card title
url: https://example.com # required, link target
avatar: /favicon.png # required, avatar
introduction: One-liner # required
title: Visit # optional, button textNotes: card background colors cycle frind-card1~frind-card10 by list order; the page body (page.content) renders below the friend list, and comments render via the linksComments injection point.
Create (already exists: source/friends/index.md):
---
title: 'For My Friends!'
layout: friends
comment: waline
---11.5 Contact Page (layout: contact — ⚠️ not implemented in the active theme, do not use)
Note:
contact.ejsdoes not exist in the active themethemes/matery/layout/(only the disabled upstream reference theme has it), solayout: contactwill not render. The following is kept for historical context only.
Purpose: a contact page — barrage interaction + comments. Page body + barrage form (text/link/speed) + comments.
Barrage: LeanCloud (AV)-backed barrager; the form shows when either valine or waline is enabled (theme.valine.enable or theme.waline.enable).
Customer service widgets: daovoice / tidio / tuxiaochao config lives in Interaction & Visual §14; this section covers the page itself.
Create (not yet created in this project):
hexo new page contactsource/contact/index.md front-matter:
---
title: Contact
layout: contact
---11.6 404 Page (layout: 404)
Purpose: not-found notice + search entry + countdown redirect to home (8 seconds).
Customize (already exists: source/404.md):
---
title: 404
layout: 404
excerpt: "Oops~,我崩溃了!找不到你想要的页面 :(" # shown as the page description (sample copy is Chinese — change it as needed)
---Notes: excerpt renders as the page description; the banner image switches daily (/medias_webp/banner/{0-6}.webp).
Appendix: Layout Feature Quick Reference
| Feature | Location | Config | Description |
|---|---|---|---|
| Prev/next nav | post bottom | theme post.prev_next | adjacent posts by date |
| Copyright | post bottom | theme post.copyright (with license sub-key) | per-post override via front-matter reprintPolicy |
| Outdated notice | post top | theme post.outdate | off by default, graded by days |
| TOC | post sidebar | theme toc | per-post off via front-matter toc: false |
| Ad slots | sidebar | theme post.advertisements | array of cards, individually toggleable |
| Category radar | /categories/ | auto-rendered | no config |
| Tag cloud/word cloud | /tags/ | auto-rendered | no config |
| Archive timeline | /archives/ | auto-rendered | no config |
| About charts | /about/ | auto-rendered | no config |
| Responsive breakpoints | global | $breakpoint-* variables in base.styl | 368/768/992/1200/1400 |
| Galleries | /galleries/ | source/_data/galleries.yml | pages via hexo new gallery |
| Search | navbar | theme search (algolia/local/pagefind) | three engines independently toggleable |
| Comments | posts/message pages etc. | theme waline | front-matter comments toggles |
| Dark mode | global | theme dark_mode | auto follows system + time window |
| Encryption | post pages | root encrypt | tag-based or front-matter password |
| RSS/Sitemap | root paths | root feed / sitemap / baidusitemap | three feeds + two sitemaps |
| PWA | global | theme pwa + userConfig/sw.tmp.js | offline cache + installable |
| Standalone pages | /movies/ /msg/ /friends/ /404 | create page under source/ + front-matter layout | four dedicated templates, see §11 (/av/ removed, /contact/ template not implemented) |