Jesse A. Green / notebook

Hexo Admin & Developer Guide

Generated June 2026 from hexojs/hexo at d7a10f4

AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it.

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 hexo core package — the framework itself, not a site built with it. The repository is written in TypeScript and compiled to dist/ for publication. All file paths below refer to the lib/ 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

  1. The Hexo object — a single EventEmitter instance that holds all configuration, the database, the router, and eleven extension registries. Everything hangs off this context.
  2. Boxes and processors — the source/ folder and the active theme are each a Box. A box reads files, hashes them against a cache, and runs processors that turn files into database documents.
  3. The warehouse database — processed content becomes Post, Page, Asset, Category, Tag (etc.) documents, queryable through a Mongo-like API and exposed to themes as site.* locals.
  4. Generators → routes → renderers — generators turn documents into route definitions; the router holds the URL→content map; renderers produce the final bytes written to public/.
  5. 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 only asset, page, and post routes. 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_plugins returns 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 Cache model stores { _id, hash, modified } per file. compareFile first 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 a public/ 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) unless included, and anything matching the exclude config 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-marked or 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: false option to emit inline output (separator-joined) instead of <ul>, plus class, transform, order, orderby, and show_count. The meta_generator helper can be disabled globally with meta_generator: false in _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. The Config type is open ([key: string]: any), so plugins read their own namespaced blocks directly from hexo.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 in scripts/ (and the theme's scripts/) 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 audit scripts/.

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 use hexo-util's escapeHTML/htmlTag). When authoring tags/helpers, escape any attacker-influenced content.
  • Code blocks and Nunjucks tags inside posts are escaped/placeheld during the render pipeline (PostRenderEscape in lib/hexo/post.ts) to prevent Markdown↔tag interference, then restored — a careful state machine that also handles {% raw %} and inline code.
  • The include_code tag guards against path traversal by resolving only files that exist as registered Page documents under code_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/ mirroring lib/ (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 g may not copy a subdirectory in source/_posts/ to public/post/ in some setups (#5671).
  • Slow Buffer.from when 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 :title behavior 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 eslint and npm test before submitting; lint-staged enforces this on commit.
  • Public-facing behavior changes (config keys, CLI flags, helper/tag signatures) should be reflected in lib/types.ts and 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. Treat lib/extend/ and the built-in filter/event vocabulary as a public API and version accordingly.