Multilingual & Wiki System
This page explains a signature feature of this blog — the multilingual multi-Wiki system: one config manages multiple wiki document sets (docs/api/tutorial), each wiki can have multiple language versions (zh-cn/en), missing translations fall back to the default language automatically, and correct canonical/hreflang tags are output for search engines. This page is also the creation & maintenance manual for wiki pages.
The
preferencesection ofuserConfig/_config.tmp.ymlis the authoritative config source.
1. Feature Overview
| Feature | Description |
|---|---|
| Multi-wiki | auto-scans source/{wiki}/ directories (currently: docs / api / tutorial) |
| Multilingual | 9-language capability table (de/en/eo/es/ja/ru/zh-CN/zh-HK/zh-TW); lang_meta enabled controls activation (currently: zh-cn / en) |
| Path prefix | language versions are distinguished by path prefix: /docs/setup/ (default) vs /en/docs/setup/ (English) |
| Fallback | 3-level fallback chain: requested → fallback_lang → default_lang → any available; the notice bar shows the actual fallback source language |
| Independent sidebar | one source/_data/{wiki}-sidebar.yml per wiki |
| SEO | correct html lang + canonical per page; hreflang output when a translated version exists |
2. Config Details (preference language section)
Config (theme config, preference section; migrated from preference.wiki on 2026-09-08 to site-wide, removed duplicate langs):
preference:
default_lang: zh-cn # default language (used for prefix-less paths; all components: wiki/post/search)
fallback_lang: en # fallback language: used first when the requested language version is missing (must be enabled in lang_meta)
lang_meta: # language capability table + enable switch: key=language, enabled=true only takes effect
zh-cn:
name: 简体中文
flag: 🇨🇳
enabled: true
en:
name: English
flag: 🇬🇧
enabled: true
de:
name: Deutsch
flag: 🇩🇪
enabled: false
# ... all 9 languages (de/en/eo/es/ja/ru/zh-CN/zh-HK/zh-TW), enabled controls activation
wiki:
enable: true # master switch of the multilingual wiki system (wiki-specific switch stays here)
# Chinese trad/simp conversion (formerly footer.translate, consolidated here on 2026-08-16)
translate:
enable: trueNotes:
- The wiki system auto-scans
source/{wiki}/directories (front-matterlayout: wiki) andsource/_data/{wiki}-sidebar.yml— no config registration needed - Language capability table = keys of
lang_meta(all 9 languages supported); active languages = keys withenabled: true— inactive languages generate no fallback pages, appear in no language dropdown, and are not fallback targets fallback_lang: middle fallback layer — used first when the requested language is missing; defaults to the first enabled non-default languagename/flaginlang_metadrive the language dropdown display; they must be added when introducing a new languagedefault_langis not written into the URL prefix (/docs/is Chinese); other languages are (/en/docs/)- The old
preference.wiki.default_lang/langslocation is deprecated (migrated 2026-09-08;lang-config.jsstill reads it as fallback during the transition)
3. Language Switching (preference panel)
Purpose: readers switch language/trad-simp from the preference panel in the top-right corner.
- Language switching: the panel’s language dropdown lists the enabled languages (
lang_metawithenabled: true, with flags); switching jumps to the current page’s version in that language - Trad/simp conversion:
preference.translate.enablecontrols the Chinese conversion button; the default direction is decided byzhDefaultEncoding(1 traditional / 2 simplified) - Remembered preference: the choice is stored in localStorage (key
wiki_lang)
Client language strategy (2026-09-10, Plan A — site-wide follow):
- First-visit prompt: no saved preference and browser language matches an enabled (non-default) language → a dialog asks whether to switch; accept = remember + jump, decline = remember the default language (never asked again)
- Load / refresh follow: with a saved preference, any page loads directly in that language version (inline redirect in
head, before render — no flash); crawlers are skipped (SEO); paths without a multilingual version are skipped (no 404) - Panel display: the language dropdown shows the saved preference (
localStorage.wiki_lang), not the current URL language
404 / aggregate / encrypted page language (2026-09-27):
- 404 page: switched to client-side language detection (single file) — copy language follows the visitor’s browser language / saved preference instead of statically generating one page per language (the old
/en/404.htmletc. are no longer needed) - archives aggregate page: unified pagination across all languages — no longer diverges per language; every language version shares the same pagination structure
- Encrypted page language: encryption prompt / button copy follows the page language (hbe-bundle fallback copy goes through
MATERY_I18N, E7 2026-09-27) - In-content links & runtime copy:
url_for_langpoints to the matching language version; search / dialog runtime copy is i18n-ised too (P0/P1)
4. Wiki Page Creation Flow
Purpose: create a new wiki (or add pages to an existing one). Using the tutorial wiki as an example (this document is its product), 4 steps total:
① Create the page directory (source/{wiki}/):
mkdir -p source/tutorial/config # pages go in subdirectories by chapter② Write the page file (front-matter must contain layout: wiki + wiki: <name>):
---
title: Global Configuration
layout: wiki
wiki: tutorial
---
## Global Configuration
Body content...③ Configure the sidebar (source/_data/{wiki}-sidebar.yml, chapter → page mapping):
Getting Started:
overview: index.html
install: install.html
Configuration Reference:
global: global.html
multilingual: multilingual.html④ Done: the wiki system auto-scans the source/tutorial/ directory (front-matter layout: wiki + wiki: tutorial) — no config registration needed. Visiting /tutorial/ enters the wiki; the left sidebar renders the chapter tree from tutorial-sidebar.yml automatically (collapsible), with in-wiki navigation at the top of the page.
5. Sidebar Data Format
File: source/_data/{wiki}-sidebar.yml (e.g. docs-sidebar.yml, tutorial-sidebar.yml).
Format: section name: page-key: page.html — the key is the page file’s slug, the value is the HTML path relative to that wiki’s root:
# tutorial-sidebar.yml example
Feature Guide:
layout: layout.html
tag-plugins: tag-plugins.html
Configuration Reference:
global: global.html
nav-home: nav-home.html
post: post.html
widgets: widgets.html
code-highlight: code-highlight.html
multilingual: multilingual.htmlNotes:
- Section order = render order; page order within a section = file order
- The page filename (slug) MUST match the key in
{wiki}-sidebar.yml, otherwise the sidebar link 404s - Adding a page = ① create the md file ② add a line to the corresponding section in sidebar.yml — both steps required
6. Multilingual & Translation
Directory convention: translations live under source/{lang}/{wiki}/, same name and path as the source files:
source/
├── docs/ # default language (zh-cn): docs wiki Chinese pages
│ ├── index.md
│ ├── setup.md
│ └── …
├── en/
│ ├── docs/ # docs wiki English translations
│ │ ├── index.md
│ │ ├── setup.md
│ │ └── …
│ ├── api/
│ └── tutorial/
└── _data/
├── docs-sidebar.yml
└── tutorial-sidebar.ymlTranslation essentials:
- English pages also write
layout: wiki+wiki: docsin front-matter; addlang: enso the theme knows the page language (the helpers usepage.langfor the sidebar link prefix and canonical) - The language codes in
lang_metamatch the directory names (en↔/en/) - The sidebar data is maintained once for the default language only; translations reuse it
Post translation (source/{lang}/_posts/): same “same filename” convention as wiki —
| Type | Translation source | front-matter |
|---|---|---|
| Post | source/{lang}/_posts/{same name as Chinese}.md | keep the same abbrlink + lang: en |
- Two mapping modes: with the abbrlink plugin → matched by identical abbrlink; without it → matched by identical filename (with a filesystem fallback check)
- Hexo natively only recognizes
source/_posts/(en/_posts/is ignored); post translations are generated by the theme’si18n-post-generator.jsat/{lang}/posts/{abbrlink}/ - A real translation → a real content page; otherwise → a fallback page (see §7)
7. Fallback Mechanism (3-level chain, unified 2026-09-08)
Purpose: when a page has no translation in a language, visiting that language path still returns content instead of a blank page.
Fallback chain (unified across all modules: wiki/post/archive-pages/search, resolved by lang-config.js):
requested language exists? → use it
→ not found → fallback_lang exists? → fall back to fallback_lang
→ not found → default_lang exists? → fall back to default_lang
→ not found → any available language (first one)Flow (generation-level fallback, generated by scripts/generators/{post,wiki}-fallback.js):
visit /de/docs/setup/ (de enabled but untranslated)
→ en translation exists → fallback page content in en (fallback_lang takes effect) + notice "de not found, showing en"
→ no en translation → fallback page content in zh-cn + notice "de not found, showing zh-cn"Effect:
- Content language is chosen by the fallback chain (
_fallbackFrommarks the actual language); the notice bar shows “xx language not found, showing yy language version” - canonical points to the default language, preventing search engines from indexing duplicate content
- Standalone pages (about/musics/movies/bb/friends/galleries etc.) do generate multilingual versions (
/{lang}/{page}/) — UI copy goes through i18n keys (languages/*.yml), content stays in the author’s language; generated bystandalone-page-generator.jsvia an exclusion rule (layout ∉ {wiki, tags, categories, 404}). Navigation links (url_for_lang) add a language prefix to content paths, pointing to the matching language version - Archive pages (tag/category/archive) generate enabled-language versions (generated only, no notice bar)
8. SEO (canonical / hreflang)
Output rules (auto-generated per wiki page):
| Scenario | html lang | canonical | hreflang |
|---|---|---|---|
| Default-language page (has translation) | zh-CN | /docs/setup/ | itself + /en/docs/setup/ |
| Translated page | en | /en/docs/setup/ | itself + /docs/setup/ |
| Fallback page (no translation) | zh-CN | /docs/setup/ (points to default) | none |
Notes: path prefix (not query string) + hreflang + canonical is the standard Google multilingual SEO practice; the sitemap is output uniformly by hexo-generator-sitemap, wiki pages included automatically.
9. Common Operations
Add a Wiki
- Create the
source/{new-wiki}/directory and pages (front-matter:layout: wiki+wiki: {new-wiki}) - Write the
source/_data/{new-wiki}-sidebar.ymlsidebar - Done — the wiki system auto-scans, no config registration needed
Add a Language
- Add the language code +
name/flag/enabled: truetopreference.lang_meta(enable directly within the 9-language capability table) - Optional: set
preference.fallback_langto that language (as the middle fallback layer) - Create
source/{lang}/{wiki}/for translation files
Add a Post (existing wiki)
- Write
source/tutorial/xxx.md(front-matter as above) - Add a line to the corresponding section of
_data/tutorial-sidebar.yml - Optional: translate to
source/en/tutorial/xxx.md
Add a Post Translation
- Create a file with the same name under
source/en/_posts/(e.g.source/en/_posts/my-post.md) - In front-matter, keep the same abbrlink and add
lang: en - Translate the body to English
- After build, the page is automatically generated at
/en/posts/{abbrlink}/
Key: the abbrlink must match the Chinese original, otherwise the two language versions cannot be linked. The abbrlink is generated by the
hexo-abbrlinkplugin on first build and written into front-matter (withwriteback: falseit is no longer written back, the field is fixed) — never manually edit the abbrlink of an existing post.
Common Troubleshooting
- Sidebar link 404: sidebar.yml key and md filename don’t match
- Sidebar not showing: front-matter missing
layout: wiki, or thewikiname doesn’t match thesource/_data/{wiki}-sidebar.ymlfilename - Config changes not taking effect:
rm -f db.json && hexo generate(see the development notes)
10. Wiki-Wide Encryption Configuration
Purpose: encrypt the entire wiki’s content column and right-side TOC; the left navigation stays visible (navigation is equivalent to a title, and its exposure is acceptable).
Declaration location: top-level keys in source/_data/{wiki}-sidebar.yml — side by side with navigation section keys like getting_started. The encrypt_ prefix was unified on 2026-09-07 to distinguish them:
| Key | Required | Description |
|---|---|---|
encrypt_password | Yes | Encryption password; the presence of this key enables wiki-wide encryption |
encrypt_message | No | Encryption prompt message (overrides theme encrypt.message) |
encrypt_wpm | No | Wrong-password message (overrides theme encrypt.wrong_pass_message) |
encrypt_placeholder | No | Password input box placeholder text |
encrypt_button_text | No | Decrypt button label |
The password in the sidebar data uses a
{{PASSWORD}}placeholder. The real value must not be committed to the repository.
Behavior (based on themes/matery/layout/wiki.ejs and themes/matery/source/js/wiki.js):
- Encryption scope: content column only (
encryptHtml(page.content, …)) and right-side TOC (#wiki-toc-asidegets thewiki-toc-encryptedclass); the leftwiki-sidebarnavigation always renders and stays visible - Encryption parameters:
stableSalt: true+storageKey: 'wiki.' + page.wiki— all pages in the same wiki share a single localStorage cache key - TOC rebuild after decryption: a successful frontend decryption triggers the
hexo-blog-decryptevent →wiki.jsremoves thewiki-toc-encryptedclass, changes the content column froml10tol8, then runstocbot.destroy()followed bytocbot.init()— in the encrypted state, body headings are unreadable, so the TOC must be rebuilt after decryption
Appendix: Multilingual & Wiki Quick Reference
| Config/file | Description |
|---|---|
preference.enable (inherited from preference.wiki.enable) | system master switch |
preference.default_lang | default language (prefix-less paths) |
preference.fallback_lang | middle fallback language (used first when the requested language is missing) |
preference.lang_meta | language capability table + enabled switch (9 languages) + name/flag |
preference.translate | Chinese trad/simp conversion switch |
source/_data/{wiki}-sidebar.yml | sidebar section → page mapping |
source/{lang}/{wiki}/ | wiki pages per language |
| Fallback | 3-level chain: requested → fallback_lang → default_lang → any available; the notice bar shows the actual fallback source language |
| Wiki-wide encryption | top-level encrypt_password in {wiki}-sidebar.yml (other encrypt_* keys optional) |
| SEO | html lang / canonical / hreflang / sitemap output automatically |