Loading...

Loading...

Customization

This page explains how to add your own things to the blog: custom JS/CSS, the inject_point mechanism, custom tag plugins, and how theme config is overridden. Editing code is a “developer-level” operation — read the Installation chapter first to understand the directory structure, and pick the release method per the Deployment chapter when done.

Core principle: don’t modify the theme source unless you have to. Prefer inject_point injection and config overrides — theme upgrades (submodule themes/matery/) won’t drag you into conflicts.

1. Custom JS/CSS

Purpose: append scripts and styles site-wide or per-page; the most common kind of customization.

Method 1: put files under source/ and reference them in the page (no theme source changes):

<!-- reference directly in the post/page body -->
<link rel="stylesheet" href="/js/my-custom.css">
<script src="/js/my-custom.js" defer></script>
  • Files go in source/js/, source/css/ or any directory; they’re copied as-is to the matching public/ path at build time
  • Note the skip_render section of _config.yml: some directories under source/ (e.g. nav/, docs/, live2d/) skip rendering — custom HTML in those directories stays as-is; plain .js/.css are unaffected
  • In production with CDN, static asset URLs are decided by the theme cdn section (see the Global Configuration chapter); custom files referenced directly as /js/xxx.js are served from the site’s own domain

Method 2: inject_point global injection (recommended, next section) — scripts/styles take effect on all pages without touching theme files.

2. The inject_point Mechanism

Purpose: insert HTML/JS/CSS at fixed page positions without modifying theme templates. The theme inherited an injection system from NexT; layout/layout.ejs actually uses 3 view injection points:

<!-- layout.ejs -->
<%- inject_point('bodyBegin') %>   <!-- start of <body> -->
<%- inject_point('header') %>      <!-- header position -->
<%- inject_point('bodyEnd') %>     <!-- before </body> -->

How it works: injected content is not an HTML string loaded from theme config — hexo scripts register it into a runtime registry via the theme_inject filter. scripts/events/lib/injects.js runs hexo.execFilterSync('theme_inject', injects) before generation and writes the result into theme.config.injects; the inject_point helper (scripts/helpers/engine.js) reads that registry at render time and outputs it.

Using the theme’s real implementation (scripts/filters/default-injects.js) as the template, custom injections go in any new file under the theme’s scripts/ (e.g. scripts/filters/my-injects.js):

// themes/matery/scripts/filters/my-injects.js
'use strict';

const path = require('path');

hexo.extend.filter.register('theme_inject', function(injects) {
  // file(name, file-path): register a template file (extension from the file when name has none; path relative to the hexo root)
  injects.bodyEnd.file('my-custom', path.join(hexo.theme_dir, 'layout/_partial/common/my-custom.ejs'));
  // raw(name, content-string): register HTML/JS content directly
  injects.bodyEnd.raw('my-analytics', '<script src="/js/my-custom.js" defer></script>');
}, -99); // -99 = run first, consistent with default-injects.js

The template file layout/_partial/common/my-custom.ejs is rendered as-is at the <%- inject_point('bodyEnd') %> position. injects.<point>.file(name, path, locals, options, order) — from the third argument on it’s locals/options/order; order sorts multiple injections at the same point.

  • bodyEnd is the most common injection point: global scripts, analytics code, floating widgets
  • The theme defines 12 view injection points (headEnd/header/bodyBegin/bodyEnd/footer/postMetaTop/postMarkdownBegin/postMarkdownEnd/postCopyright/postComments/pageComments/linksComments), and all 12 are called in templates: headEnd (head.ejs), footer (footer.ejs), postMetaTop/postMarkdownBegin/postMarkdownEnd/postCopyright/postComments (post-detail.ejs), bodyBegin/header/bodyEnd (layout.ejs), pageComments (bb/msg), linksComments (friends). postMetaBottom/postLeft/postRight were removed from the injection-point definitions (not “defined but not called”; comment pages use inject_point('pageComments') || partial('_partial/comments') as a fallback)
  • Style injection points (variable/mixin/style) are a separate mechanism: they register .styl file paths, injected at Stylus compile time — don’t mix them with view injection points
  • Injected content is output as-is; use defer on <script> tags (see the LCP layering principle in the Performance Optimization chapter)

3. Custom Tag Plugins

Purpose: use shortcodes in Markdown to generate complex HTML. The theme ships 14 tag plugins (note/tabs/timeline/mermaid/pdf/github-card etc.; usage in the Tag Plugins chapter) — write your own when they’re not enough.

Implementation: add a file under themes/matery/scripts/tags/ exporting a Hexo tag function:

// themes/matery/scripts/tags/mybox.js
'use strict';

function mybox(args, content) {
  const title = args.join(' ') || 'Note';
  return `<div class="my-box"><strong>${title}</strong><div class="my-box-body">${hexo.render.renderSync({ text: content, engine: 'markdown' })}</div></div>`;
}

hexo.extend.tag.register('mybox', mybox, { ends: true });

Use it in posts:

{% mybox Notes %}
Write **any Markdown** here; it renders as the card content.
{% endmybox %}
  • { ends: true } means a closing {% endmybox %} tag is required
  • content is the raw text by default; render Markdown with hexo.render.renderSync when needed (note: a Hexo internal API, only runs at build time)
  • The filename is the plugin name (mybox.js → {% mybox %}); after changes always hexo clean && hexo generate (see the cache issues in the FAQ chapter)
  • For global availability without touching theme source: putting the file in the theme scripts/ is the only way (the theme is a submodule; changes need dual-repo commits, see the Deployment chapter)

4. Theme Config Override (Template Injection Mechanism)

Purpose: theme config is generated dynamically at CI build time — userConfig/_config.tmp.yml is the template, copied to themes/matery/_config.yml at build time with placeholders replaced. Theme config changes must go in the template; editing themes/matery/_config.yml directly gets overwritten.

Template placeholders (replaced by tools/cicd.sh via sed):

PlaceholderReplaced withExample
{cdnPathVersion}jsDelivr versioned pathhttps://cdn.jsdelivr.net/gh/appotry/hexo@1.1
{cdnPathLatest}latest CDN pathhttps://cdn.jsdelivr.net/gh/appotry/hexo@latest
{urlVersion}file version number?v={urlVersion}
{cdnUrl} / {mediaUrl} / {resUrl}CDN domainshttps://cfblog.17lai.site

Full chain for a custom config item (example: adding a “my switch”):

  1. Template userConfig/_config.tmp.yml add the key:
    myFeature:
      enable: true
      text: Hello
  2. Read in Stylus (under themes/matery/source/css/):
    $my-feature-color = theme-config("myFeature.text", "default value")
  3. Read in templates (EJS under layout/):
    <% if (theme.myFeature.enable) { %> <div><%= theme.myFeature.text %></div> <% } %>
  4. Release: after config changes run tools/cicd.sh -r "feature" to bump the version (config counts as code-side changes; see the release flow selection in the Deployment chapter)

Note: the tools/cicd.sh build pipeline injects config (sed-replaces placeholders in the userConfig/_config.tmp.yml template), processes image URLs (imgurl.sh --weserv2githubpage), and generates the PWA Service Worker (userConfig/sw.tmp.js → source/sw.js); grep before editing the template to confirm key names aren’t used by build scripts.

5. Custom File Config Keys (custom_file_path / custom_js / custom_css)

Purpose: specify custom JS/CSS file paths directly in the theme config, auto-loaded site-wide — simpler than §1 (in-page references) and §2 (inject points); ideal for “just add a script/style”.

Config (theme config):

# Specify custom .js file paths, list supported; paths are relative to the source directory
# e.g. /js/custom.js corresponds to source/js/custom.js
custom_js:
  - /js/custom.js

# Specify custom .css file paths, same usage as custom_js
custom_css:
  - /css/custom.css

# Custom file path (Next theme inject reference, empty in this project)
custom_file_path:

Notes:

  • Place files under source/js/, source/css/ etc.; they are copied as-is to public/ at build time
  • custom_js/custom_css are lists — configure multiple files, loaded in order
  • custom_file_path references Next theme default-injects.js; unused in this project, leave empty

6. Encryption Architecture (HBE v4)

Purpose: theme-built-in encryption (developed in-house 2026-08-09, based on hexo-blog-encrypt v4, wire format format: '4'), covering three encryption surfaces:

#SurfaceDeclarationImplementation
aEntire postfront-matter password, or post tag matching encrypt.tagsscripts/encrypt/index.js after_post_render filter (priority 1000)
bInline snippetMarkdown {% encrypt password "prompt title" "content summary" %}…{% endencrypt %}scripts/tags/encrypt-tag.js stages + same filter (priority 2000) second pass
cWiki / gallery pageWiki sidebar encrypt_password (see the Multilingual & Wiki System page §10); gallery uses encryptHtml helperlayout/wiki.ejs, gallery.ejs

Encryption mechanism (server-side scripts/encrypt/crypto.js and client-side source/libs/hbe/hbe-bundle.js (WebCrypto) must use matching parameters):

  • Key derivation: PBKDF2-SHA256, iteration count set by encrypt.kdf.iterations
  • Symmetric encryption: AES-256-GCM; 32-byte random salt + 12-byte random nonce; ciphertext = ciphertext || authTag(16B)
  • When stableSalt: true, the salt is derived from the post permalink (stableSaltFromPermalink), so repeated builds produce the same salt and cached decryptions stay valid; the nonce remains random each time to avoid nonce reuse

Config keys (theme encrypt.*, authoritative in the template userConfig/_config.tmp.yml; post front-matter can override password/abstract/message/theme/wrong_pass_message/silent/autoSave/stableSalt/kdf):

KeyDescription
encrypt.enableMaster encryption switch
encrypt.abstractPlaceholder summary for encrypted post listings (also overrides excerpt to prevent content leakage)
encrypt.message / encrypt.wrong_pass_messagePrompt / wrong-password message
encrypt.tags{name, password} list; posts whose tags match are encrypted automatically
encrypt.autoSaveWrites the derived key to localStorage, skipping the password on refresh (off by default)
encrypt.stableSaltStable PBKDF2 salt across clean builds (default false)
encrypt.kdf.iterationsPBKDF2 iteration count; floor 100000, recommended ≥ 600000, default 250000

Multi-container storageKey: encrypted snippets carry a data-storage-key attribute; hbe-bundle.js uses storageKey || location.pathname as the localStorage cache key (prefix hbe.v4.), so multiple encrypted containers on the same page don’t collide — snippets use segment.<path>#<id>, wiki uses wiki.<name>, whole-post leaves it empty (falls back to the page path).

Search engine exclusion (all three engines skip encrypted content): the local search scripts/generators/local-search.js skips post.encrypt === true; Algolia scripts/generators/algolia-search.js returns null for record.encrypt === true; Pagefind is excluded by layout/_partial/post/post-detail.ejs which omits data-pagefind-body when page.encrypt is true.

Appendix: Customization Quick Reference

NeedMethodFile location
Per-page scripts/styles<link>/<script> in Markdownsource/js/, source/css/
Site-wide scripts/stylesinject_point injectionthemes/matery/scripts/filters/
New shortcodestag pluginthemes/matery/scripts/tags/
Theme config itemstemplate key + read in Stylus/EJSuserConfig/_config.tmp.yml
Global HTML fragmentsinjection points or _partial/ templatesconfig or themes/matery/layout/_partial/
Comments
Data Loading...