# Hexo Admin & Developer Guide

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

```text
                          ┌──────────────────────────────────────┐
   _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

```text
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

```text
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 `Page`s and copies the rest as `Asset`s.

> [!NOTE]
> File exclusion rules live in `lib/plugins/processor/common.ts`: temp files (`*~`, `*%`), hidden files (paths with a leading `_` or `.` segment) unless `include`d, 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 `Map`s (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

```js
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.

```js
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

```js
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

```js
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

```js
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`.

```js
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

```js
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

```js
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

```js
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

```js
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>`

```js
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

```js
hexo.extend.migrator.register(name, fn)
```

`fn(args)` imports content. Invoked by `hexo migrate <name>`.

### 5.11 Highlight (SyntaxHighlight) — code highlighting backends

```js
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:

```text
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

```yaml
# 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

```bash
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:

```yaml
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

```bash
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.

---

_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. Unofficial; not affiliated with or reviewed by the project._
