A Material Design 3 (MD3) documentation theme for Hexo, built for API documentation sites that need multiple versions and multiple languages at the same time.
- Material Design 3 — MD3 tokens (color, shape, typography), light / dark theme toggle.
- Multi-version — any number of doc versions (e.g.
v1.0,v2.0) side by side; a version switcher in the header, "latest" badge on the homepage. - Multi-language — any number of languages (e.g.
zh-cn,en); a language switcher in the header; every doc is localized independently. - Custom languages — add languages the theme doesn't ship (e.g.
zh-tw) by dropping a language pack into<site>/languages/; site packs also override built-in packs. - Automatic language negotiation — based on the visitor's
Accept-Languageheader,/redirects to the best matching language; the matching rules, fallback language and explicit aliases are all configurable (seelanguage_negotiation). - Customizable home page — the homepage is rendered from a configurable
list of sections (
home.sections), including arbitrary custom sections with Markdown content. - Client-side full-text search — no backend, instant results, covers all versions and languages; a mobile search button opens the search layer on small screens.
- Mermaid diagrams — code blocks tagged
mermaidrender as diagrams using the active light/dark theme and repaint automatically when the theme changes; the CDN is configurable (mermaid_cdn). - Subdirectory deployment — all asset and search-index URLs derive from
Hexo's
root, so the site works out of the box under/docs/or any subpath. - Accessibility — focus-trapped search dialog with focus restore,
aria-modal,prefers-reduced-motionsupport and forcedcolor-scheme. - Customizable branding — logo, title and favicon are configurable.
- Hexo 7.x
- Node.js ≥ 14
-
Clone / copy this folder into
themes/cackleof your Hexo site. -
Make sure the renderer for EJS templates is installed:
npm install hexo-renderer-ejs
-
Activate the theme in
_config.yml:theme: cackle
-
(Optional) Copy theme defaults into a site-level override file so the theme stays upgradeable:
cp themes/cackle/_config.yml _config.cackle.yml
Settings in
_config.cackle.ymloverride the theme's_config.yml. -
Run the dev server:
hexo clean && hexo server
All documentation lives under source/ using the convention:
source/
├── languages.yaml # language registry (see below)
├── index.md # homepage (layout: index)
├── v1.0/
│ ├── zh-cn/ # Simplified Chinese docs for v1.0
│ │ ├── getting-started.md
│ │ └── ...
│ └── en/ # English docs for v1.0
├── v2.0/
│ ├── zh-cn/
│ └── en/
Rules:
-
Every language (including the default one) lives in its own subdirectory named after the language
path; URLs carry the language prefix, e.g./v2.0/zh-cn/getting-started/. -
Each document's front-matter should declare its language:
--- title: Getting Started lang: en ---
The
langvalue must match a language-pack filename inthemes/cackle/languages/(e.g.en,zh-cn).The directory name is authoritative: a document living under
<version>/<language>/is always treated as that language, so the front-matterlangis optional (it is ignored when it disagrees with the directory).
Entries map to document filenames (without extension) inside each version/language folder:
menu:
- title: Getting Started
file: getting-started
- title: Authentication
file: authenticationIf a menu entry has no matching document, it is skipped with a warning.
brand:
logo: i-menu-book # icon name from icons.ejs, or a path like /images/logo.svg
text: # brand text (defaults to config.title)
favicon: /images/favicon.svgThe homepage is built from an ordered list of sections. Built-in section
types: hero, features, versions. Default order:
home:
sections:
- hero
- features
- versionsYou can reorder them, remove any of them, or insert your own custom sections with Markdown content:
home:
sections:
- hero
- type: custom
title: Changelog
content: |
- **2026-08**: added multi-language support
- [v2.0](https://example.com/v2.0/zh-cn/getting-started/) released
- versions- Custom sections render
titleas an<h2>andcontentthrough Hexo's Markdown renderer. - Other home settings (badge / title / subtitle / CTA / version block texts /
feature cards) can be customized under
home.*; empty values fall back to the language pack, then to built-in defaults.
Note on arrays: Hexo merges
_config.cackle.ymlover the theme's_config.ymlwith a deep-merge that combines arrays by index. Defining the same array (e.g.home.sections) in both files can produce surprising results. Keep array settings in a single file — preferably the theme's_config.yml.
search: true # enable client-side search
code_block_line_number: true
toc_max_depth: 3 # max heading depth in the on-page TOC (h2–h4)
mermaid_cdn: https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js
footer:
text: My API © 2026toc_max_depthcontrols how deep the on-page table of contents goes (default3→h2/h3; set4to includeh4). It is consumed client-side viawindow.CACKLE_CFG.mermaid_cdnis the URL of the Mermaid library loaded on pages that contain mermaid diagrams. Set it to an empty string to skip loading Mermaid altogether.meta.descriptionin a language pack is used as the page description fallback when neither the document nor the site defines one.config.root(Hexo site config) is respected by all asset URLs and the search index, enabling subdirectory deployment (e.g.root: /docs/).
languages:
- lang: zh_CN # BCP-47 / locale code
path: zh-cn # URL prefix & doc directory name (also matches the language-pack filename)
DisplayAs: 简体中文 # shown in the language switcher
- lang: en_US
path: en
DisplayAs: EnglishThe first language in the list is the default language.
When a visitor requests /, the theme tries to match the browser's
Accept-Language header against the configured languages and redirects
(302) to the first document of the latest version in the matched language
(requests already in the default language are served without redirect; a
Vary: Accept-Language header is included).
The behavior is fully configurable via language_negotiation:
language_negotiation:
enabled: true # set to false to disable automatic redirection entirely
fallback: en # language (path from languages.yaml) used when nothing matches
aliases: # explicit browser-tag → language-path mappings (optional)
zh: zh-cn
zh-CN: zh-cn
en: en
en-US: enMatching is tried in this order:
aliases— an explicitly mapped browser tag wins over everything else.- Exact match — browser tag (e.g.
zh-CN) equals alang(zh_CN) orpath(zh-cn). - Primary-language prefix — e.g.
zhmatcheszh_CN. fallback— the configured language (defaults toen; if that language is not registered, the default language is used instead).
When running hexo server, the middleware scripts/lang-redirect.js handles
this server-side. For pure static hosting (no server middleware), the same
rules are applied client-side by an inline script on the home page, using
navigator.language.
UI strings live in themes/cackle/languages/<lang>.yml (one file per
language). Add a new file for every language registered in languages.yaml.
To add a language the theme doesn't ship (e.g. Traditional Chinese, zh-tw),
drop a language pack into the site root's languages/ folder:
<site>/
├── _config.yml
├── languages/
│ └── zh-tw.yml # your custom language pack
└── source/
├── languages.yaml # (optional) register language metadata
├── v1.0/zh-tw/… # docs for the new language
└── v2.0/zh-tw/…
The pack filename defines the language path (URL prefix & doc directory).
You may attach metadata under a reserved cackle_lang key:
# <site>/languages/zh-tw.yml
cackle_lang:
lang: zh_TW # BCP-47 / locale code, used by language negotiation
path: zh-tw # URL prefix & doc dir (defaults to the filename)
display_as: 繁體中文 # shown in the language switcher (defaults to path)
# UI strings — for a brand-new language they may be partial; missing keys fall
# back to the default language
header:
search_placeholder: 搜尋文件…
version_switch: 切換版本Behavior:
- Same filename ⇒ user wins. If you provide a pack whose name matches a
built-in language (e.g.
languages/en.yml), your file replaces the built-in pack entirely — there is no per-key merging, your custom config takes priority over the theme's. Provide a complete pack in that case (copy the built-in file fromthemes/cackle/languages/as a starting point). - A pack whose name is not shipped registers a brand-new language. As long
as a doc directory
<version>/<language-path>/exists, the language appears automatically in the language switcher, the search index and theAccept-Languagenegotiation — no need to touchsource/languages.yaml. - If
source/languages.yamlalso lists the language, its entry takes precedence for ordering /DisplayAs/ BCP-47 code; otherwise auto-registered languages are appended as non-default languages. - JSON packs (
languages/zh-tw.json) are supported as well. - Like other site files, the folder must be watched by your deployment
workflow; a
hexo cleanis recommended after adding packs.
The theme's built-in packs live in
themes/cackle/languages/. The site folder is loaded by the theme scripts (scripts/languages.js+scripts/lib/site-languages.js) and merged into the theme's i18n at generation time.
themes/cackle/
├── _config.yml # theme defaults (overridable per-site)
├── layout/
│ ├── index.ejs # home page (section-driven)
│ ├── doc.ejs # documentation page
│ ├── page.ejs # plain page
│ └── _partial/ # head, header, sidebar, footer, switchers, icons
├── scripts/
│ ├── home-sections.js # parses home.sections → api_home_sections
│ ├── lang-redirect.js # Accept-Language negotiation middleware
│ ├── languages.js # injects <site>/languages/ packs into theme i18n
│ ├── versions.js # version & language grouping → api_versions
│ ├── generator.js # emits search-index.json (all versions & languages, strips Markdown/HTML)
│ └── lib/
│ └── site-languages.js # shared helpers for custom site languages
├── _config.yml # also holds the language_negotiation settings
├── languages/ # language packs (UI strings)
└── source/
├── css/ # base, tokens, components, highlight
├── js/ # main.js (UI), search.js (client search, reads CACKLE_CFG)
└── images/
- Changes to
scripts/require a server restart —hexo serverwatches documents and configs, but not scripts. - Site-level
_config.cackle.ymlchanges also require a restart —hexo serveronly watchessource/andthemes/; the theme's own_config.ymlis re-merged automatically, but the site-level override file is only read at startup. - After changing language packs or
source/languages.yaml, runhexo cleanto flush the generated assets. - The console error
<path> attribute d: Expected numbercomes from an inline SVG icon and is harmless.