结论先行:在 Hexo 静态博客中构建 10 国语言国际化矩阵时,默认的 hexo-generator-index 会粗暴地将所有语言的文章按发布时间混合输出到根目录首页,导致中文、日文、德文等内容杂糅,破坏用户阅读体验与搜索引擎抓取逻辑。最优雅的解决方案不是魔改第三方主题源码,而是利用 Hexo 原生的插件扩展机制:在 scripts/ 目录下注册自定义 Filter 自动注入语言元数据,并重写 Generator 实现按语言分组独立渲染分页首页与隐藏特定页面。
0、静态博客架构选择与多语言卡点
对于独立开发者和出海技术团队而言,静态博客系统(如 Hexo、Hugo 等)具备不可替代的工程优势:
- 零数据库维护负担:所有内容以 Markdown 文件形式版本化存储于 Git 仓库中;
- 极佳的安全抗压性能:构建出的纯 HTML/CSS 资源可以直接部署在 Cloudflare Pages、GitHub Pages 或边缘 CDN 上,从根源上免除了 SQL 注入与服务端动态执行的漏洞风险;
- 极低的运维支出:即使遭遇外部突发流量冲击,CDN 边缘缓存也能轻松吸收请求,不会产生高昂的计算实例开销。
但在实施 10 国语言(如中文、英语、日语、德语、法语、西班牙语等)全球化站点时,Hexo 默认的单语言架构遇到了严峻挑战:
- 首页文章大杂烩:如果将 10 个语言版本的文章全部放入
source/_posts/,默认的分页生成器会把所有语言混排在一起;
- 隐藏页面难以优雅实现:部分针对特定渠道的落地页、支付跳转页或法律声明,期望能够通过固定 URL 访问,但不希望出现在公开发布的最新文章列表中;
- 标签与分类交叉污染:在日文分类页中,常常莫名其妙地出现中文标签,破坏了主题的本地化语境。
1、Hexo 生命周期与扩展点解密
要编写插件精准拦截并分流内容,必须看清 Hexo 启动构建时的内部事件循环。
Hexo 提供了两类核心扩展接口:
- Filter (过滤器):用于在特定时机修改数据。例如在 Markdown 解析前向文本追加版权声明,或者在数据入库前清洗元数据字段。
- Generator (生成器):用于生成网站最终的 URL 路由与页面数据的映射关系。每个生成器必须返回一个包含
path(相对输出路径)、data(传递给模板的变量上下文)以及 layout(模板名称)的对象或数组。
2、实战开发:多语言首页独立生成器插件
无需发布 npm 包,在 Hexo 项目根目录下创建 scripts/multilang-index-generator.js,Hexo 在启动时会自动加载执行该文件。
2.1 完整插件实现源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
| 'use strict';
const pagination = require('hexo-pagination');
hexo.extend.filter.register('before_post_render', function(data) { data.hide_index = Boolean(data.hide || data.hidden || data.hide_index);
if (!data.lang) { const pathSegments = data.source.split('/'); const langMatch = pathSegments.find(segment => ['en', 'ja', 'de', 'fr', 'es', 'pt', 'ru', 'ko', 'zh-CN'].includes(segment)); data.lang = langMatch || hexo.config.language || 'zh-CN'; } return data; });
hexo.extend.generator.register('multilang_index', function(locals) { const config = hexo.config; const posts = locals.posts.sort(config.index_generator.order_by || '-date'); const perPage = config.index_generator.per_page || 10; const paginationDir = config.pagination_dir || 'page'; const defaultLang = Array.isArray(config.language) ? config.language[0] : (config.language || 'zh-CN');
const languages = new Set(); posts.forEach(post => { languages.add(post.lang || defaultLang); });
const routes = [];
languages.forEach(lang => { const filteredPosts = posts.filter(post => { const postLang = post.lang || defaultLang; return postLang === lang && !post.hide_index; });
const baseRoute = (lang === defaultLang) ? '' : `${lang}/`;
const pages = pagination(baseRoute, filteredPosts, { perPage: perPage, layout: ['index', 'archive'], format: `${paginationDir}/%d/`, data: { __index: true, current_lang: lang, is_multilang_home: true } });
routes.push(...pages); });
return routes; });
|
2.2 卸载或禁用默认的单语言 index 生成器
在 Hexo 的根目录 _config.yml 中,调整默认配置:
1 2 3 4 5
| index_generator: enable: false per_page: 10 order_by: -date
|
此时执行 hexo clean && hexo g,你可以清晰地观察到生成的 public/ 目录结构已经发生了显著变化:
- 中文(默认语言):生成
public/index.html 以及 public/page/2/index.html;
- 日语(ja):生成
public/ja/index.html 以及 public/ja/page/2/index.html;
- 英语(en):生成
public/en/index.html 以及 public/en/page/2/index.html;
- 所有 frontmatter 中带有
hide_index: true 的文章,在各语言首页列表里均被自动剔除,但其独立的文章 HTML 页面(如 /posts/special-landing/index.html)依然完整编译生成。
3、多语言归档与分类过滤扩展
除了首页,读者在访问某一语言的归档(Archive)或分类(Category)页面时,同样期望只浏览本语言的文章。
在 scripts/multilang-archive-generator.js 中继续扩展:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| 'use strict';
const pagination = require('hexo-pagination');
hexo.extend.generator.register('multilang_archive', function(locals) { const config = hexo.config; const posts = locals.posts.sort('-date'); const perPage = config.archive_generator?.per_page || 20; const defaultLang = Array.isArray(config.language) ? config.language[0] : (config.language || 'zh-CN');
const languages = new Set(); posts.forEach(post => languages.add(post.lang || defaultLang));
const routes = [];
languages.forEach(lang => { const filteredPosts = posts.filter(post => (post.lang || defaultLang) === lang); const archivePath = (lang === defaultLang) ? 'archives/' : `${lang}/archives/`;
const pages = pagination(archivePath, filteredPosts, { perPage: perPage, layout: ['archive'], format: 'page/%d/', data: { archive: true, current_lang: lang } });
routes.push(...pages); });
return routes; });
|
4、与 Butterfly 主题的无缝适配
Butterfly 主题具有成熟的国际化支持。在上述自定义生成器中,我们通过 data 属性向模板上下文中注入了 current_lang 变量。
为了让主题模板能够读取到对应语言的翻译文字(如分页按钮上的“上一页 / 下一页”、搜索提示等),我们需要在主题渲染前绑定正确的翻译上下文:
在 scripts/butterfly-i18n-bridge.js 中追加辅助函数:
1 2 3 4 5 6 7 8 9
| 'use strict';
hexo.extend.filter.register('template_locals', function(locals) { if (locals.page && locals.page.current_lang) { locals.page.lang = locals.page.current_lang; } return locals; });
|
通过这一层桥接,Butterfly 主题在渲染 /ja/index.html 时,便会自动调取 themes/butterfly/languages/ja.yml 中定义的本地化词条,彻底消除了“页面内容是日文,但界面按钮全显示中文”的错乱现象。
5、开发调试与常见报错排查
5.1 页面路由冲突报错:Route path already exists
当多个插件试图向同一个路径写入文件时,Hexo 会在控制台抛出警告。
排错策略:
- 检查是否存在旧的插件(如
hexo-generator-archive 或 hexo-generator-category)仍然处于激活状态;
- 确保
baseRoute 生成算法不会对默认语言产生类似 //index.html 这样的双斜杠重叠路径;
- 构建前使用
hexo clean 清理本地 db.json 缓存数据库。
5.2 隐藏文章依然能在站内搜索中被搜出?
上述 Generator 仅阻断了文章在“首页列表”与“归档列表”的渲染。如果站点集成了 hexo-generator-search 等生成 search.xml 的插件,默认依然会把全文索引打包进去。
如需彻底在搜索中屏蔽隐藏文章,需对搜索生成器的数据源追加统一过滤:
1 2 3
| hexo.extend.filter.register('before_generate', function() { });
|
6、实操验证清单
完成插件开发后,运行以下清单执行全面验证: