This guide is for the people who work on Hexo or extend it: plugin authors, theme developers, contributors to Hexo core, and architects evaluating Hexo as a static-site engine. It documents Hexo's internal architecture, the processing pipeline, the data model, every extension point, the complete configuration surface, security considerations, and the contribution workflow.
It is the companion to the User Guide (which covers writing content and running the CLI). Where the User Guide shows what to do, this guide explains how Hexo works and how to build on it.
Note
This document describes the
hexocore package — the framework itself, not a site built with it. The repository is written in TypeScript and compiled todist/for publication. All file paths below refer to thelib/source tree.
1. Executive Summary
Hexo is a Node.js static-site framework. Given a folder of Markdown source and a theme, it produces a complete static website. The core is deliberately small and almost everything user-visible — Markdown rendering, listing-page generation, deployment — is delegated to plugins, while presentation is delegated to themes.
| Attribute | Value |
|---|---|
| Package | hexo |
| Version | 8.1.2 |
| Language | TypeScript (compiled to dist/) |
| Runtime | Node.js >=20.19.0 |
| License | MIT |
| Entry binary | bin/hexo (the launcher itself is the hexo-cli package) |
| Module shape | CommonJS, export = style |
| Database | warehouse (in-memory JSON document store, persisted to db.json) |
| Template engine | nunjucks (for tag plugins and .njk views) |
| Core source | ~22 high-value modules under lib/, ~508 KB of source |
Architectural pillars
- The
Hexoobject — a singleEventEmitterinstance that holds all configuration, the database, the router, and eleven extension registries. Everything hangs off this context. - Boxes and processors — the
source/folder and the active theme are each aBox. A box reads files, hashes them against a cache, and runs processors that turn files into database documents. - The warehouse database — processed content becomes
Post,Page,Asset,Category,Tag(etc.) documents, queryable through a Mongo-like API and exposed to themes assite.*locals. - Generators → routes → renderers — generators turn documents into route definitions; the router holds the URL→content map; renderers produce the final bytes written to
public/. - Filters and events — pipeline hooks (filters) and lifecycle signals (events) let plugins intervene at every stage.
Important
Hexo core ships no Markdown renderer and no index/archive/category/tag generators. Those are separate plugins (
hexo-renderer-marked,hexo-generator-*) that a scaffolded site installs. The core's built-in renderers handle only passthrough (HTML/CSS/JS), JSON, YAML, and Nunjucks; the core's built-in generators handle onlyasset,page, andpostroutes. This is by design — the core is the engine, not the car.
2. Architecture
2.1 High-level data flow
┌──────────────────────────────────────┐
_config.yml ─────────► │ Hexo (context) │
plugins (hexo-*) ───► │ config · database · router · log │
scripts/, theme/ ───► │ extend = { console, deployer, │
│ filter, generator, helper, │
│ highlight, injector, migrator, │
│ processor, renderer, tag } │
└───────────────┬──────────────────────┘
│ hexo.load() / watch()
┌───────────────────────┼───────────────────────┐
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ Source Box │ │ Theme Box │
│ source/ │ │ themes/<name>/ │
│ → processors: │ │ → processors: │
│ asset,data, │ │ config,i18n, │
│ post │ │ source,view │
└────────┬─────────┘ └────────┬─────────┘
│ insert/update/remove │ register views,
▼ ▼ i18n, theme config
┌─────────────────────────────────────────────────────────────────┐
│ warehouse database (db.json) │
│ Post · Page · Asset · PostAsset · Category · Tag · Data · Cache │
└────────────────────────────┬────────────────────────────────────┘
│ exposed as site.* locals
▼
┌─────────────────────────────────────────────────────────────────┐
│ Generators (asset, page, post, + plugins) │
│ documents → { path, layout, data } route definitions │
└────────────────────────────┬────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────┐
│ Router (path → data | render-thunk) │
└────────────────────────────┬────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────┐
│ Render: theme View → renderer (njk) → injector → after_render │
│ filter → write bytes to public/<path> (SHA-1 dedup via Cache) │
└─────────────────────────────────────────────────────────────────┘
2.2 Source layout
| Path | Responsibility |
|---|---|
lib/hexo/ |
The Hexo class, config/plugin/theme loaders, Post, Render, Router, Scaffold, Source, Locals, config validation |
lib/box/ |
Box (file scanner + watcher) and File (a single source file) |
lib/extend/ |
The eleven extension registries (Console, Deployer, Filter, Generator, Helper, Injector, Migrator, Processor, Renderer, SyntaxHighlight, Tag) |
lib/models/ |
warehouse schemas: Asset, Cache, Category, Data, Page, Post, PostAsset, PostCategory, PostTag, Tag, plus the BinaryRelationIndex |
lib/plugins/console/ |
Built-in CLI commands |
lib/plugins/processor/ |
Built-in source processors (asset, data, post) |
lib/plugins/generator/ |
Built-in generators (asset, page, post) |
lib/plugins/renderer/ |
Built-in renderers (plain, json, yaml, nunjucks) |
lib/plugins/filter/ |
Built-in filters (permalink, post path, lifecycle filters) |
lib/plugins/helper/ |
Built-in theme helpers |
lib/plugins/tag/ |
Built-in tag plugins |
lib/plugins/highlight/ |
highlight.js and PrismJS adapters |
lib/plugins/injector/ |
The HTML injection plugin |
lib/theme/ |
Theme (a Box), View, and theme processors |
lib/types.ts |
The TypeScript surface: schemas, LocalsType, generator return types |
2.3 The Hexo object
The heart of the system is the Hexo class (lib/hexo/index.ts), a subclass of Node's EventEmitter. One instance is created per site and threaded through every plugin as this (or the global hexo).
Key properties:
| Property | Type | What it holds |
|---|---|---|
config |
object | The merged _config.yml (starts from default_config) |
theme_config |
object | The theme's own config (from _config.<theme>.yml) |
env |
object | Runtime environment: args, debug, safe, silent, env, version, cmd, init |
extend |
object | The eleven registries (see §5) |
database |
warehouse.Database |
The document store (db.json) |
route |
Router |
The URL→content map |
render |
Render |
The rendering facade over the renderer registry |
post |
Post |
Post creation/publish/render helper |
scaffold |
Scaffold |
Scaffold (template) manager |
source |
Source (Box) |
The source/ box |
theme |
Theme (Box) |
The active theme box |
locals |
Locals |
Site-wide template variables (posts, pages, categories, tags, data) |
log |
logger | hexo-log instance |
Directory anchors are computed in the constructor: base_dir, public_dir, source_dir, plugin_dir (node_modules/), script_dir (scripts/), scaffold_dir, theme_dir, theme_script_dir.
Key methods:
| Method | Purpose |
|---|---|
init() |
Load internal plugins, then update_package → load_config → load_theme_config → load_plugins; emits ready |
load(cb) |
Load the database, process source + theme boxes, then _generate() once |
watch(cb) |
Like load, but keeps watching for file changes and regenerates (debounced 100 ms) |
call(name, args, cb) |
Invoke a console command programmatically |
model(name, schema?) |
Get or define a warehouse model |
render / route / post |
Facades described above |
execFilter(type, data, opts) / execFilterSync(...) |
Run a filter chain |
exit(err) |
Run before_exit filters and emit exit (saves the database) |
loadPlugin(path) |
Load a plugin/script file in a sandboxed function scope |
2.4 Initialization sequence
hexo.init()
├─ require internal plugins: console, filter, generator, helper,
│ highlight, injector, processor, renderer, tag (register into hexo.extend)
└─ Promise.each:
update_package → bump `hexo.version` in the site's package.json; sets env.init
load_config → read _config.yml (merge over defaults), validate, resolve theme dir
load_theme_config → read _config.<theme>.yml, merge into config.theme_config
load_plugins → load every hexo-* dependency + scripts/, theme scripts/
→ execFilter('after_init')
→ emit('ready')
Note
In safe mode (
--safe),load_pluginsreturns early and no external plugins or scripts load. This is the diagnostic baseline — only Hexo's built-in plugins run.
3. The Box & Processing Pipeline
3.1 Boxes
A Box (lib/box/index.ts) is an EventEmitter that owns a base directory, a list of processors, and (optionally) a file watcher. Hexo has two boxes:
- Source box (
lib/hexo/source.ts) — base =source_dir, processors from the processor registry (asset,data,post). - Theme box (
lib/theme/index.ts) — base =theme_dir, processors =config,i18n,source,view.
3.2 File scanning and change detection
Box.process()
├─ stat(base) — skip if not a directory
├─ _readDir(base): walk recursively (honoring `ignore` patterns)
│ for each file → Cache.compareFile(id, hashFn, statFn)
│ → type = 'create' | 'update' | 'skip' (by mtime, then SHA-1)
│ → _processFile(type, path)
└─ files present in Cache but absent on disk → _processFile('delete', path)
Each file becomes a File object (lib/box/file.ts) carrying source (absolute path), path (box-relative, forward-slashed), params (pattern-match data), and type (create/update/skip/delete). A box's _File subclass adds render()/renderSync().
_processFile emits processBefore, runs every processor whose pattern matches the path, then emits processAfter. Watching (Box.watch()) wires add/change/unlink/addDir events to the same _processFile machinery.
Tip
The
Cachemodel stores{ _id, hash, modified }per file.compareFilefirst checks mtime (cheap), and only computes a SHA-1 hash if the mtime changed — this is why repeat builds are fast. The same hashing protects the output: the generator skips writing apublic/file whose content hash is unchanged.
3.3 Built-in processors
| Processor | Pattern | Produces |
|---|---|---|
post |
_posts/*, _drafts/* |
Post documents (published vs. draft); scans post-asset folders → PostAsset |
asset |
everything else under source/ |
Page documents (if renderable) or Asset documents |
data |
_data/* |
Data documents (exposed as site.data) |
The post processor parses front-matter (via hexo-front-matter), derives the date from front-matter, then filename tokens, then file birthtime; resolves updated per updated_option; normalizes singular aliases (tag→tags, etc.); and assigns categories/tags through the post model's setCategories/setTags. The asset processor treats renderable files as Pages and copies the rest as Assets.
Note
File exclusion rules live in
lib/plugins/processor/common.ts: temp files (*~,*%), hidden files (paths with a leading_or.segment) unlessincluded, and anything matching theexcludeconfig are skipped.
3.4 Theme processors
| Processor | Pattern | Effect |
|---|---|---|
config |
_config.<ext> at theme root |
Loads the theme's own config into theme.config |
i18n |
languages/* |
Registers translation tables with hexo-i18n |
source |
source/* |
Registers theme static assets as Asset documents |
view |
layout/* |
Registers theme templates as View objects |
4. The Data Model
Processed content lives in a warehouse database — an in-memory document store with a Mongoose-like schema/virtual/hook API, persisted to db.json (schema version 1). Models are registered at construction from lib/models/.
4.1 Models
| Model | Key fields | Notable virtuals / methods |
|---|---|---|
Post |
title, date, updated, slug, source, _content, published, comments, layout, photos |
path (via post_permalink filter), permalink, full_source, asset_dir, tags, categories; setTags(), setCategories(), notPublished() |
Page |
title, date, updated, source, path, _content, layout (default page), comments |
permalink, full_source |
Asset |
_id, path, modified, renderable |
source (absolute path) |
PostAsset |
_id, slug, post, modified, renderable |
path (relative to the post), source |
Category |
name, parent (self-ref) |
slug, path, permalink, posts, length; uniqueness hook |
Tag |
name |
slug, path, permalink, posts, length; uniqueness hook |
Data |
_id, data |
exposed as site.data[id] |
Cache |
_id, hash, modified |
compareFile() static (change detection) |
PostCategory |
post_id, category_id |
many-to-many join; maintains the index |
PostTag |
post_id, tag_id |
many-to-many join; maintains the index |
4.2 The binary relation index
Posts↔Tags and Posts↔Categories are many-to-many. Rather than scanning all join documents on every query, Hexo maintains a BinaryRelationIndex (lib/models/binary_relation_index.ts) — two Maps (key→ids, value→ids) kept in sync by save/remove hooks on PostTag/PostCategory. This makes tag.posts, tag.length, category.posts, and category.length fast lookups instead of full-table filters.
4.3 Site locals
hexo.locals (lib/hexo/locals.ts) is a cached getter map exposing site-wide data to templates:
| Local | Source |
|---|---|
site.posts |
All published posts (filtered by future and draft visibility) |
site.pages |
All pages |
site.categories |
Categories with at least one post |
site.tags |
Tags with at least one post |
site.data |
All Data documents, keyed by id |
The cache is invalidated before each generation pass so template data stays fresh in watch mode.
5. Extension Points (Plugin Development)
Plugins register capabilities into one of eleven registries on hexo.extend. A plugin is a function (hexo) => { ... } exported from a hexo-* package, a scripts/ file, or a theme scripts/ file. Below is each registry's contract.
5.1 Console — CLI commands
hexo.extend.console.register(name, desc, options, fn)
fn(args) runs with this = the Hexo instance and args parsed (minimist-style: args._ positionals plus named flags). options describes usage, arguments, and options for help output, and may set init: true if the command needs config loaded. Command names are abbreviation-resolved via abbrev, which is how g→generate works.
hexo.extend.console.register('hello', 'Say hello', {
options: [{ name: '-n, --name', desc: 'Who to greet' }]
}, function (args) {
this.log.info('Hello, %s!', args.n || args.name || 'world');
});
5.2 Deployer — publish targets
hexo.extend.deployer.register(name, fn)
fn(deployArg) receives the merged { type, ...config, ...cliArgs } and deploys public/. Invoked by hexo deploy for each deploy: entry whose type matches.
5.3 Filter — pipeline hooks
hexo.extend.filter.register(type, fn, priority?)
Filters run in priority order (default 10) and each may transform and return the data. Returning undefined keeps the previous value. Run them with hexo.execFilter(type, data, { context, args }).
Built-in filter types (the pipeline hooks plugins target):
| Filter type | Fires |
|---|---|
before_post_render (alias pre) |
Before a post's content is rendered |
after_post_render (alias post) |
After a post's content is rendered |
before_generate |
Before generators run |
after_generate |
After generation completes |
before_exit |
On shutdown (saves the database) |
after_init |
After initialization |
after_clean |
After hexo clean |
new_post_path |
Computes the file path for a new post |
post_permalink |
Computes a post's permalink |
template_locals |
Augments the locals passed to a view (e.g. i18n) |
after_render:<output> |
After rendering output of a given type (e.g. after_render:html, aliased internally as _after_html_render) |
5.4 Generator — build routes
hexo.extend.generator.register(name, fn)
fn(locals) receives the site locals and returns one or more route definitions { path, data, layout? }. Without a layout, the router serves data directly; with one, the data is rendered through the theme. Built-in generators: asset, page, post.
hexo.extend.generator.register('json-feed', function (locals) {
return {
path: 'feed.json',
data: JSON.stringify(locals.posts.toArray().map(p => ({ title: p.title, url: p.permalink })))
};
});
5.5 Helper — template functions
hexo.extend.helper.register(name, fn)
Helpers are functions available inside theme templates, bound to the page's locals (this). See §6.3 for the complete built-in catalog.
5.6 Renderer — convert content
hexo.extend.renderer.register(name, output, fn, sync?)
name is the input extension, output the result extension. fn(data, options) receives { path, text, engine, toString, onRenderEnd }. A renderer may set disableNunjucks (to skip tag processing) and may provide a compile(data) for precompiled views. Built-in renderers:
| Extensions | Output | Engine |
|---|---|---|
htm, html, css, js |
same | plain passthrough |
json |
json |
JSON.parse |
yml, yaml |
json |
js-yaml (extended schema) |
njk, j2 |
html |
Nunjucks |
Important
Markdown (
.md) is not in this list. A Markdown renderer (hexo-renderer-markedor an alternative) must be installed. This is the single most common "why isn't my content rendering" cause.
5.7 Processor — turn source files into data
hexo.extend.processor.register(pattern, fn)
pattern (a hexo-util Pattern: glob, regexp, or function) decides which files match and extracts params. fn(file) then reads the file and writes to the database. See §3.3.
5.8 Tag — {% %} snippets
hexo.extend.tag.register(name, fn, options?)
fn(args, content) returns HTML. options may set ends: true (a block tag with {% endname %}) and/or async: true. Tags are Nunjucks extensions; the registry wraps them as NunjucksTag/NunjucksBlock/NunjucksAsyncTag/NunjucksAsyncBlock. Errors are reformatted with a source-context dump for friendlier diagnostics.
5.9 Injector — inject HTML into <head>/<body>
hexo.extend.injector.register(entry, value, to?)
entry is one of head_begin, head_end, body_begin, body_end. to scopes the injection to a page type (default, home, post, page, archive, category, tag, or a layout name). Injection runs after HTML rendering, before the after_render:html filter, and is idempotent (guarded by an HTML comment marker).
5.10 Migrator — import from other systems
hexo.extend.migrator.register(name, fn)
fn(args) imports content. Invoked by hexo migrate <name>.
5.11 Highlight (SyntaxHighlight) — code highlighting backends
hexo.extend.highlight.register(name, fn)
fn(content, options) returns highlighted HTML. Built-in backends: highlight.js and prismjs, selected by the syntax_highlighter config. The code/include_code tags and the backtick-code-block filter call the active backend.
5.12 Events
Beyond filters, the Hexo instance emits lifecycle events you can subscribe to with hexo.on(...):
| Event | Fires |
|---|---|
ready |
After init() completes |
generateBefore / generateAfter |
Around a generation pass |
processBefore / processAfter |
Around processing a single file (carries { type, path }) |
new |
After hexo new creates a post (carries { path, content }) |
deployBefore / deployAfter |
Around deployment |
exit |
On shutdown |
6. Theme Development
6.1 Theme structure
A theme is a directory (under themes/ or an installed hexo-theme-* package) that the Theme box scans:
themes/<name>/
├── _config.yml # theme config (→ theme.config / `theme.*` in templates)
├── languages/ # i18n tables (en.yml, zh-CN.yml, ...)
├── layout/ # templates (index, post, page, archive, ...)
└── source/ # theme static assets (CSS, JS, images)
6.2 Views and layout chaining
Each layout/ file becomes a View (lib/theme/view.ts). A view renders its template, then — if its front-matter or the call specifies a layout — wraps the result in that parent layout, passing the child output as body. This is how a post layout nests inside a shared layout.<ext> wrapper. Views are precompiled when the renderer supports compile() (e.g. Nunjucks), so repeated renders are fast.
Within a view, the locals include the page data, the site config (config), theme config (theme), site locals (site), the current page, url, path, and every registered helper bound to the locals.
6.3 Built-in helpers reference
Helpers are registered in lib/plugins/helper/index.ts. Grouped by purpose:
URLs
| Helper | Purpose |
|---|---|
url_for(path, options?) |
Site-relative URL (honors root, pretty_urls) |
full_url_for(path) |
Fully-qualified URL (honors url) |
relative_url(from, to) |
Relative path between two URLs |
Assets / tags
| Helper | Purpose |
|---|---|
css(...paths) |
Emit <link rel="stylesheet"> tags |
js(...paths) |
Emit <script> tags |
image_tag(path, options?) |
Emit an <img> |
favicon_tag(path) |
Emit a favicon link |
feed_tag(path, options?) |
Emit an RSS/Atom <link rel="alternate"> |
link_to(path, text?, options?) |
Emit an <a> (with external handling) |
mail_to(address, text?, options?) |
Emit a mailto: link with subject/cc/bcc/body |
Dates
| Helper | Purpose |
|---|---|
date(date?, format?) |
Format a date (default date_format) |
time(date?, format?) |
Format a time (default time_format) |
full_date(date?, format?) |
Date + time |
relative_date(date?) |
"3 days ago" |
time_tag(date?, format?) |
<time datetime="..."> |
date_xml(date?) |
ISO-8601 string |
moment |
The bundled moment-timezone instance |
Lists
| Helper | Purpose |
|---|---|
list_posts(options?) |
Render a list of posts |
list_categories(options?) |
Render a (hierarchical) category list |
list_tags(options?) |
Render a tag list |
list_archives(options?) |
Render an archive (monthly/yearly) list |
tagcloud(options?) / tag_cloud |
Render a weighted tag cloud (size/color by post count) |
Conditionals (page-type checks)
is_current(path, strict?), is_home(), is_home_first_page(), is_post(), is_page(), is_archive(), is_year(year?), is_month(year?, month?), is_category(name?), is_tag(name?).
Content / SEO
| Helper | Purpose |
|---|---|
markdown(text, options?) |
Render Markdown inline |
render(text, engine, options?) |
Render with a named engine |
partial(name, locals?, options?) |
Render a partial view (with optional caching) |
fragment_cache(id, fn) |
Cache an expensive render fragment |
toc(content, options?) |
Build a nested table of contents from headings |
paginator(options?) |
Render pagination controls |
open_graph(options?) |
Emit Open Graph + Twitter Card meta tags |
meta_generator() |
Emit the <meta name="generator"> tag |
search_form(options?) |
Emit a Google site-search form |
gravatar(email, options?) |
Gravatar URL |
number_format(n, options?) |
Format a number with delimiters/precision |
Text utilities
strip_html, trim, titlecase, word_wrap, truncate, escape_html, inspect, log.
Tip
Most list helpers accept a
style: falseoption to emit inline output (separator-joined) instead of<ul>, plusclass,transform,order,orderby, andshow_count. Themeta_generatorhelper can be disabled globally withmeta_generator: falsein_config.yml.
7. Configuration Reference
The authoritative defaults live in lib/hexo/default_config.ts. Site _config.yml is deep-merged over these; a _config.<theme>.yml is merged into config.theme_config.
7.1 Complete default configuration
# Site
title: Hexo
subtitle: ''
description: ''
author: John Doe
language: en
timezone: ''
# URL
url: http://example.com
root: /
permalink: ':year/:month/:day/:title/'
permalink_defaults: {}
pretty_urls:
trailing_index: true
trailing_html: true
# Directory
source_dir: source
public_dir: public
tag_dir: tags
archive_dir: archives
category_dir: categories
code_dir: downloads/code
i18n_dir: ':lang'
skip_render: []
# Writing
new_post_name: ':title.md'
default_layout: post
titlecase: false
external_link:
enable: true
field: site
exclude: ''
filename_case: 0
render_drafts: false
post_asset_folder: false
relative_link: false
future: true
syntax_highlighter: highlight.js
highlight:
auto_detect: false
line_number: true
tab_replace: ''
wrap: true
exclude_languages: []
language_attr: false
hljs: false
line_threshold: 0
first_line_number: always1
strip_indent: true
prismjs:
preprocess: true
line_number: true
tab_replace: ''
exclude_languages: []
strip_indent: true
use_filename_as_post_title: false
# Category & Tag
default_category: uncategorized
category_map: {}
tag_map: {}
# Date / Time format
date_format: YYYY-MM-DD
time_format: HH:mm:ss
updated_option: mtime
# Pagination
per_page: 10
pagination_dir: page
# Extensions
theme: landscape
server:
cache: false
# Deployment
deploy: {}
# Files to ignore
ignore: []
# Misc
meta_generator: true
7.2 Key-by-key notes
| Key | Type | Notes |
|---|---|---|
timezone |
string | Empty = machine local (warns). Set an IANA name (UTC, America/New_York) |
permalink |
string | Token pattern for post URLs (see User Guide §7.2) |
permalink_defaults |
object | Default values for permalink tokens |
pretty_urls.trailing_index |
bool | Strip/keep index.html |
pretty_urls.trailing_html |
bool | Strip/keep .html |
skip_render |
string[] | Globs of files copied verbatim |
new_post_name |
string | Filename pattern for hexo new (its tokens also parse the date back out) |
filename_case |
0/1/2 | Slug case: keep / lower / upper |
render_drafts |
bool | Include _drafts/ in builds |
post_asset_folder |
bool | Per-post asset folders |
relative_link |
bool | Generate links relative to each page |
future |
bool | Show future-dated posts |
syntax_highlighter |
string | highlight.js, prismjs, or empty to disable |
highlight.* |
object | highlight.js tuning (line numbers, threshold, hljs classes, excluded languages) |
prismjs.* |
object | PrismJS tuning |
updated_option |
string | mtime (file mtime), date (same as created), or empty (none) |
category_map / tag_map |
object | Map a category/tag name to a custom slug |
server.cache |
bool | In-memory render cache for hexo server |
ignore |
string[] | Glob patterns excluded from box scanning |
meta_generator |
bool | Emit the generator meta tag |
Note
Many widely-used keys (
theme_config,feed,keywords,include,exclude, and any per-plugin block) are not in the default config — they are consumed by plugins/themes or read opportunistically. TheConfigtype is open ([key: string]: any), so plugins read their own namespaced blocks directly fromhexo.config.
7.3 Multi-config merging
--config a.yml,b.yml (or space-separated) merges files left→right into _multiconfig.yml (lib/hexo/multi_config_path.ts). Both .yml and .json are supported. This is the standard pattern for environment-specific overrides (dev vs. prod).
8. Security Analysis
Hexo is a build-time tool that produces static files; it has no server runtime in production (the dev server is local-only). The security surface is therefore mostly supply chain and template/content trust.
8.1 Plugin and script execution
Caution
Hexo loads every
hexo-*dependency and every file inscripts/(and the theme'sscripts/) as code, executed with full Node.js privileges in the site's process. A malicious plugin or script can read/write the filesystem, exec processes, and exfiltrate data. Treat third-party themes and plugins as you would any npm dependency: review them, pin versions, and auditscripts/.
loadPlugin (lib/hexo/index.ts) wraps each plugin/script in a function scope and runs it with runInThisContext — this is module scoping, not a sandbox. Use --safe to load nothing untrusted when diagnosing.
8.2 Template and tag injection
- Nunjucks autoescaping is disabled for tag rendering (
autoescape: false) and in the view renderer config. Tag plugins and helpers are responsible for their own escaping (most usehexo-util'sescapeHTML/htmlTag). When authoring tags/helpers, escape any attacker-influenced content. - Code blocks and Nunjucks tags inside posts are escaped/placeheld during the render pipeline (
PostRenderEscapeinlib/hexo/post.ts) to prevent Markdown↔tag interference, then restored — a careful state machine that also handles{% raw %}and inline code. - The
include_codetag guards against path traversal by resolving only files that exist as registeredPagedocuments undercode_dir(CVE-class fix referenced in source for issue #5250).
8.3 Config validation
validate_config (lib/hexo/validate_config.ts) enforces that url is a valid http(s) URL and root is a non-empty string before building, failing fast with Invalid config detected. It warns on missing/invalid/mismatched timezone.
8.4 Supply-chain hygiene
| Practice | Why |
|---|---|
Pin plugin/theme versions in package.json |
Prevents silent malicious updates |
Review scripts/ in cloned themes |
They run with full privileges |
Run npm audit periodically |
Hexo has many transitive deps (bluebird, nunjucks, moment, etc.) |
| Prefer maintained renderers/generators | Abandoned plugins accumulate vulnerable deps |
9. Operations
9.1 Build and serve
hexo clean && hexo generate # clean rebuild
hexo generate --watch # incremental rebuild on change
hexo server # local preview (hexo-server)
hexo generate --concurrency 5 # cap parallelism on constrained hosts
9.2 CI/CD
Hexo fits cleanly into CI: install, build, deploy. Example GitHub Actions outline:
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20' }
- run: npm ci
- run: npx hexo clean && npx hexo generate
# publish ./public via your host's action or hexo deploy
Tip
For reproducible CI builds, commit a lockfile, pin
timezone, and avoid--force(let the content-hash cache work — though CI usually starts clean anyway).
9.3 Performance levers
| Lever | Effect |
|---|---|
Incremental cache (db.json, output hashing) |
Skips unchanged work automatically |
--concurrency N |
Bounds parallel file generation (memory vs. speed) |
server.cache: true |
Caches renders during local preview |
fragment_cache helper |
Caches expensive template fragments |
relative_link: false |
Keeps URL helpers cacheable (relative links bust some caches) |
9.4 The database
db.json is a serialized snapshot of the warehouse store, written on graceful exit (before_exit → save_database filter). It is purely a cache: deleting it (or hexo clean) forces a full reprocess from source/. Never edit it by hand.
10. Code Quality & Conventions
| Aspect | Convention |
|---|---|
| Language | TypeScript, compiled with tsc -b to dist/ |
| Module style | CommonJS export = / require() (matches the plugin loader's require-based discovery) |
| Async | bluebird promises throughout (.asCallback, .map, .each, Promise.method) for Node-callback interop and concurrency control |
| Linting | ESLint with eslint-config-hexo (npm run eslint) |
| Tests | Mocha + Chai + Sinon, run via npm test (mocha test/scripts/**/*.ts) with ts-node; coverage via c8 |
| Git hooks | Husky + lint-staged (npm run prepare installs hooks) |
| Public API typing | lib/types.ts defines schemas, LocalsType, and generator return types |
Build scripts (package.json):
| Script | Action |
|---|---|
build |
tsc -b |
clean |
tsc -b --clean |
eslint |
Lint lib and test |
test |
Run the Mocha suite |
test-cov |
Coverage run (c8, non-parallel) |
Note
Tests live under
test/scripts/mirroringlib/(one suite per module:console/,hexo/,models/,tags/,helpers/,filters/,processors/,renderers/,generators/,theme_processors/,box/,extend/). Contributions are expected to add or update the matching suite.
11. Troubleshooting (Developer)
Error: Console \<name>` has not been registered yet!`
Cause: hexo.call('<name>') before the command's plugin loaded, or a typo.
Solution: Ensure init()/ready completed; verify the console plugin registered the name.
Error: fn must be a function / name is required from a registry
Cause: A registry register(...) call with arguments in the wrong order or a missing function.
Solution: Check the registry's signature in §5 — most accept (name, fn) or (name, options, fn).
Tag throws Nunjucks Error with a context dump
Cause: A tag plugin's Nunjucks template failed to parse/render. Solution: The dump shows the offending line with surrounding context; fix the tag's output or its arguments. Remember autoescape is off — escape manually.
Post not found from post_link/post_path
Cause: The referenced slug/title isn't in the database.
Solution: Confirm the target post exists and is published; hexo list post shows valid slugs.
Renderer not applied (content passes through unrendered)
Cause: No renderer registered for the file extension (e.g. .md with no Markdown renderer), or the file matched skip_render.
Solution: Install/verify the renderer plugin; check skip_render globs.
YAMLException on build
Cause: A YAML front-matter or data file uses a type the default schema rejects.
Solution: Hexo extends the YAML schema with js-yaml-js-types; if extension itself fails it warns (see issue #4917). Validate the YAML and avoid exotic tags.
Asset/database state looks wrong after edits
Cause: Stale db.json or output cache.
Solution: hexo clean to drop the database and public/, then rebuild. The processor/cache logic keys on mtime then hash, so an out-of-band file-time change can mislead it.
Known upstream issues to be aware of
Real community-reported issues at this revision (mine these when supporting users):
hexo gmay not copy a subdirectory insource/_posts/topublic/post/in some setups (#5671).- Slow
Buffer.fromwhen generating very large pages (#5705). - Saving database state (e.g. Asset) from within a custom processor may not persist as expected (#5680).
- Symbolic-link directories used as assets can fail to generate (#5659).
- A
permalink :titlebehavior change without a backward-compatible replacement affected some sites (#5658). - With
future: false, category/tag pages may not generate as expected (#5642). - Markdown can be over-escaped when two tags share the same prefix (#5635).
12. Contributing
12.1 Development workflow
git clone https://github.com/hexojs/hexo.git
cd hexo
npm install # installs deps + Husky hooks (npm run prepare)
npm run build # tsc -b → dist/
npm test # Mocha suite
npm run eslint # lint
12.2 Expectations
- Match the existing TypeScript + bluebird-promise style; keep
export =module shape. - Add or update the Mocha suite under
test/scripts/that mirrors the module you changed. - Run
npm run eslintandnpm testbefore submitting; lint-staged enforces this on commit. - Public-facing behavior changes (config keys, CLI flags, helper/tag signatures) should be reflected in
lib/types.tsand documented.
12.3 Where to make a change
| Goal | Touch |
|---|---|
| New CLI command | lib/plugins/console/ + register in its index.ts |
| New built-in tag | lib/plugins/tag/ + register in index.ts |
| New helper | lib/plugins/helper/ + register in index.ts |
| New built-in generator/processor/renderer/filter | the matching lib/plugins/<kind>/ dir |
| Data-model change | lib/models/ + lib/types.ts schema |
| Core lifecycle change | lib/hexo/index.ts (init/load/generate) |
| Extension contract change | lib/extend/<registry>.ts |
Important
Changes to a registry's
register(...)signature or to filter/event names are breaking for the entire plugin ecosystem. Treatlib/extend/and the built-in filter/event vocabulary as a public API and version accordingly.