构建多语言博客不仅仅是翻译内容——而是在保持性能和 SEO 的同时,创造跨语言的无缝体验。本指南详细介绍了本网站使用的完整 i18n 实现,从 URL 结构到缓存策略。
什么是 Hexo?
Hexo 是一个基于 Node.js 构建的快速、简单且强大的静态网站生成器。它将 Markdown 文件转换为完整的网站,具有以下特点:
- 闪电般的生成速度:几秒钟内生成数百个文件
- Markdown 支持:使用纯文本格式编写内容
- 可扩展插件:丰富的生态系统提供额外功能
- 主题系统:可自定义的模板和布局
- Git 友好:对整个站点进行版本控制
适用于:
- 带有代码示例的技术博客
- 文档站点
- 个人作品集
- 多语言内容(需要适当设置)
安装:
npm install -g hexo-cli
hexo init my-blog
cd my-blog
npm install
hexo server
📌 版本说明
本指南基于 Hexo 8.1.0 或更高版本。早期版本的固定链接行为和过滤器钩子可能有所不同。
Hexo i18n 的挑战
Hexo 提供的功能
Hexo 具有基本的 i18n 支持,但仅用于主题翻译:
# _config.yml
language:
- en
- zh-TW
- zh-CN
- ja
i18n_dir: :lang # 从 URL 路径检测语言
语言文件(主题翻译):
# themes/your-theme/languages/en.yml
menu:
home: Home
archives: Archives
# themes/your-theme/languages/zh-TW.yml
menu:
home: 首页
archives: 归档
模板使用辅助函数:
<%= __('menu.home') %>
路径检测:
i18n_dir: :lang # 从第一个 URL 段检测语言
示例:
/index.html→en(默认)/zh-tw/index.html→zh-tw/archives/index.html→en(未检测为语言)
存在的问题
Hexo 的原生 i18n 仅翻译 UI 文本,不翻译内容:
❌ 无内容过滤:
- 索引页面显示所有语言的文章
- 归档页面混合英文和中文文章
- 分类/标签不按语言过滤
❌ 无语言特定生成器:
/archives/显示所有语言的文章/categories/Development/混合所有语言版本- 无法单独生成
/zh-TW/archives/
❌ 无 SEO 支持:
- 翻译内容没有规范标签
- 没有语言特定的站点地图
- 搜索引擎将其视为重复内容
❌ 文章需要变通方法(在 Hexo 8.1.0 中测试):
- Hexo 的固定链接系统在任何过滤器拦截之前应用默认模式(
:title/) post_permalink过滤器仅在从前置数据设置__permalink时运行- 处理器在文件读取期间将
permalink→__permalink转换,时机太早无法自定义逻辑 - 虚拟
path属性在生成开始后无法修改 - 解决方案:修补 Hexo 的
post_permalink.js过滤器以自动注入语言前缀并去除语言后缀
需要什么
创建真正的多语言博客需要:
- 所有内容类型的语言特定 URL
- 每种语言的独立索引/归档/分类/标签页面
- 包含所有语言的适当站点地图生成
- 规范标签以防止重复内容惩罚
- 性能优化以处理多种语言(本站支持 4 种:en、zh-TW、zh-CN、ja)
URL 结构策略
文章与页面:不同的方法
博客文章具有自动语言处理:
- 可以组织在任何位置:
source/_posts/、source/zh-TW/_posts/等 - 使用文件名后缀约定:
Article-Title-ja.md、Article-Title-zh-TW.md - 通过前置数据中的
lang字段识别语言 - 补丁自动生成带语言前缀的 URL,无后缀重复
---
title: "文章标题"
date: 2025-10-30
lang: zh-CN
categories: Development
tags:
- Hexo
---
结果:Article-Title-zh-CN.md → /zh-CN/2025/10/Article-Title/(后缀已去除)
页面使用基于目录的组织:
- 英文:
source/tools/index.md→/tools/ - 中文:
source/zh-TW/tools/index.md→/zh-TW/tools/ - 日文:
source/ja/tools/index.md→/ja/tools/
---
title: 工具
layout: tools
lang: zh-CN
---
⚠️ 文章需要 Hexo 核心补丁
为什么文章需要补丁:文章使用 post_permalink 过滤器,在语言检测之前应用模式。页面使用不同的处理器,直接尊重目录结构。
问题:Hexo 的固定链接系统深度集成到文章处理器中:
- 处理器读取文件并将
permalink→__permalink转换 - 立即应用默认固定链接模式(
:title/) post_permalink过滤器仅在__permalink存在时运行- 虚拟
path属性从过滤器计算,无法修改
解决方案:修补 node_modules/hexo/dist/plugins/filter/post_permalink.js 以:
- 从文章数据中提取
lang字段 - 去除 slug 中的
-{lang}后缀(例如,Article-Title-ja→Article-Title) - 为非默认语言自动注入
/{lang}/前缀 - 处理手动和生成的固定链接
为什么页面不需要补丁:页面使用基于目录的组织,Hexo 的原生 i18n_dir: :lang 自动处理。补丁仅影响文章处理。
参见:下面的 Hexo 核心补丁 部分了解实现。
配置
# _config.yml
language:
- en
- zh-TW
- zh-CN
- ja
i18n_dir: :lang
permalink: :title/
Hexo 核心补丁
自动固定链接解决方案
要消除手动固定链接配置,请修补 Hexo 的核心固定链接过滤器:
文件:node_modules/hexo/dist/plugins/filter/post_permalink.js
function postPermalinkFilter(data) {
// 提取语言并清理 slug
const lang = data.lang || 'en';
const defaultLang = this.config.language?.[0] || 'en';
// 如果存在,从 slug 中去除语言后缀
let cleanSlug = data.slug;
const langPattern = new RegExp(`-(${this.config.language?.join('|')})$`);
if (langPattern.test(cleanSlug)) {
cleanSlug = cleanSlug.replace(langPattern, '');
}
const meta = {
id: data.id,
title: cleanSlug, // 使用清理后的 slug
name: cleanSlug, // 使用清理后的 slug
post_title: data.title,
year: data.date.format('YYYY'),
month: data.date.format('MM'),
day: data.date.format('DD'),
i_month: data.date.format('M'),
i_day: data.date.format('D')
};
// 处理手动固定链接
if (data.__permalink) {
let permalink = this.renderSync(data.__permalink, meta);
// 为非默认语言注入语言前缀
if (lang !== defaultLang && !permalink.startsWith(`/${lang}/`)) {
permalink = `/${lang}${permalink.startsWith('/') ? '' : '/'}${permalink}`;
}
return permalink;
}
// 处理生成的固定链接
let permalink = this.renderSync(this.config.permalink, meta);
// 为非默认语言注入语言前缀
if (lang !== defaultLang) {
permalink = `/${lang}${permalink.startsWith('/') ? '' : '/'}${permalink}`;
}
return permalink;
}
为什么打补丁不是理想做法
⚠️ 补丁的缺点
维护负担:
- 每次 Hexo 升级后都会失效
- 需要在
npm install后重新应用 - 可能与未来的 Hexo 更改冲突
- 团队成员必须记得应用补丁
脆弱性:
- Hexo 的内部结构可能改变
- 补丁文件格式很脆弱
- 无法保证跨版本兼容性
- 修改核心后调试变得更困难
更好的方法:向 Hexo 维护者提议此功能
💡 贡献开源项目
与其打补丁,不如考虑:
- 开启 GitHub issue:描述 i18n permalink 使用案例
- 提议解决方案:分享您的补丁作为起点
- 提交 pull request:向上游贡献此功能
- 优点:
- 功能由 Hexo 团队维护
- 所有人都能开箱即用
- 无补丁维护负担
- 社区测试与改进
Hexo 仓库:hexojs/hexo
在此之前:将补丁作为临时解决方案,但计划在官方支持存在后迁移。
应用补丁
选项 1:手动补丁(npm install 后需要重新应用):
cd hexo-blog
patch -p0 < ../patches/hexo-i18n-post-permalink.patch
选项 2:安装后脚本(自动):
// package.json
{
"scripts": {
"postinstall": "patch -p0 < ../patches/hexo-i18n-post-permalink.patch || true"
}
}
补丁文件:
patches/hexo-i18n-post-permalink.patch- 用于博客文章patches/hexo-i18n-page-permalink.patch- 用于页面(可选)
工作原理
补丁前:
- 文件名:
Tools-Games-ja.md - 生成的 URL:
/ja/2025/10/Tools-Games-ja/❌(后缀重复)
补丁后:
- 文件名:
Tools-Games-ja.md - 生成的 URL:
/ja/2025/10/Tools-Games/✅(后缀已去除)
关键特性:
- 语言检测:从前置数据读取
lang字段 - 后缀去除:从 slug 中移除
-ja、-zh-TW、-zh-CN - 前缀注入:为非默认语言添加
/{lang}/ - 手动覆盖:尊重前置数据中的显式
permalink
💡 文件名约定
在文件名中使用语言后缀进行组织:
Article-Title.md(英文)Article-Title-zh-TW.md(繁体中文)Article-Title-zh-CN.md(简体中文)Article-Title-ja.md(日文)
补丁会自动从 URL 中去除后缀。
能否也为页面打补丁?
技术上可以,但没有必要。页面的基于目录的方法:
- 开箱即用:Hexo 的原生
i18n_dir: :lang处理它 - 更清晰的组织:文件系统中可见语言结构
- 更易于维护:不需要文件名后缀约定
- 无补丁维护负担:在
npm install后无需重新应用
最佳实践:对文章(带有日期的动态内容)使用补丁,对页面(静态内容,如关于、工具等)使用目录结构。
📋 本网站的方法
本网站对文章和页面都使用补丁方法(页面不使用基于目录的方法):
为什么集中化所有内容?
- 一致性:所有内容类型的相同文件名后缀约定
- 更简单的结构:所有内容在标准位置(
source/_posts/、source/tools/) - 更容易重构:移动文件不会破坏语言检测
- 更少重复:无需为每种语言重建目录结构
- 统一工作流:文章和页面的相同翻译流程
权衡:页面可以使用基于目录的方法(开箱即用),但对所有内容使用补丁保持了一致性并简化了心智模型。
参见:下面的 扩展到页面 部分了解实现。
扩展补丁到页面
要将相同的方法应用于页面,创建一个 Hexo 脚本:
文件:scripts/page-i18n-permalink.js
// 页面 i18n 固定链接处理器
hexo.extend.filter.register('before_generate', function() {
const defaultLang = this.config.language?.[0] || 'en';
const languages = this.config.language || ['en'];
const langPattern = new RegExp(`-(${languages.join('|')})$`);
this.locals.get('pages').forEach(page => {
const lang = page.lang || 'en';
// 如果是默认语言或已有语言前缀,则跳过
if (lang === defaultLang || page.path.startsWith(`${lang}/`)) return;
// 从源中提取基本名称
const sourceParts = page.source.split('/');
const filename = sourceParts[sourceParts.length - 1]
.replace(/\.(md|markdown|html)$/i, '');
// 如果文件名中存在,则从路径中去除语言后缀
let newPath = page.path;
if (langPattern.test(filename)) {
newPath = newPath.replace(langPattern, '');
}
// 注入语言前缀
page.path = `${lang}/${newPath}`;
page.permalink = this.config.url + '/' + page.path;
});
});
工作原理:
- Hexo 自动从
scripts/文件夹加载脚本 - 在网站生成之前运行
- 处理所有带有
lang字段的页面 - 从文件名中去除语言后缀(例如,
tools-ja.md→tools) - 注入语言前缀(例如,
/tools/→/ja/tools/)
示例:
- 文件:
source/tools-ja.md带有lang: ja - 生成的 URL:
/ja/tools/(✅ 后缀已去除,前缀已添加) - 文件:
source/about-zh-CN.md带有lang: zh-CN - 生成的 URL:
/zh-CN/about/(✅ 后缀已去除,前缀已添加)
必备 i18n 插件
标准 Hexo 生成器不支持语言特定内容。自定义插件填补了这一空白:
⚠️ 首先移除官方插件
卸载 Hexo 的默认生成器以避免冲突:
npm uninstall hexo-generator-index hexo-generator-archive hexo-generator-category hexo-generator-tag hexo-generator-sitemap
i18n 版本完全替代这些插件。
1. 索引页面
创建每种语言的独立索引页面,支持置顶文章:
npm install hexo-generator-i18n-index
index_generator:
per_page: 24
order_by: -date
排序逻辑:
- 常规文章在前,重定向文章(
original_lang_url)在后 - 置顶/固定文章在顶部
- 然后按日期排序(最新的在前)
结果:
/- 仅英文文章/zh-TW/- 仅繁体中文文章/zh-CN/- 仅简体中文文章/ja/- 仅日文文章
2. 站点地图生成
插件:hexo-generator-i18n-sitemap
生成包含所有语言版本的统一站点地图:
/sitemap.xml- 所有语言的所有页面/sitemap.txt- 简单 URL 列表格式
sitemap_i18n:
enable: true
languages:
- en
- zh-TW
- zh-CN
- ja
changefreq: monthly
priority: 0.6
3. 归档页面
插件:hexo-generator-i18n-archive
创建语言特定的归档:
/archives/(英文)/zh-TW/archives/(繁体中文)/ja/archives/(日文)
i18n_archive_generator:
enable: true
per_page: 24
yearly: true
monthly: false
4. 分类页面
插件:hexo-generator-i18n-category
为每种语言生成带有翻译分类名称的分类页面:
i18n_category_generator:
enable: true
per_page: 24
category_i18n:
Development:
en: "Development"
zh-TW: "开发"
zh-CN: "开发"
ja: "開発"
5. 标签页面
创建按语言过滤的标签页面:
i18n_tag_generator:
enable: true
per_page: 24
6. 规范标签
通过添加适当的规范标签来防止重复内容惩罚。
问题:搜索引擎将翻译页面视为重复内容:
/2025/10/Article-Title/(英文)/zh-TW/2025/10/Article-Title/(中文)/ja/2025/10/Article-Title/(日文)
没有规范标签,Google 可能会:
- 在翻译之间分散排名信号
- 索引错误的语言版本
- 因"重复"内容惩罚网站
解决方案:规范标签告诉搜索引擎哪个版本是主要版本:
<link rel="canonical" href="https://neo01.com/2025/10/Article-Title/" />
canonical_multilang:
enable: true
default_lang: en # 主要语言
languages:
- en
- zh-TW
- zh-CN
- ja
结果:搜索引擎理解这些是翻译,而不是重复内容,保留 SEO 价值。
通过片段缓存进行性能优化
多语言性能挑战
本网站支持 4 种语言(en、zh-TW、zh-CN、ja),这意味着:
- 生成 4× 页面:每篇文章创建 4 个索引条目、4 个归档页面、4 个分类页面、4 个标签页面
- 渲染 4× 模板:每种页面类型的页眉、页脚、侧边栏渲染 4 次
- 使用 4× 内存:每种语言维护单独的页面对象
- 指数增长:100 篇文章 × 4 种语言 = 400+ 页面(不包括归档/分类/标签)
一般规则:对于 N 种语言,在没有优化的情况下,预计构建时间和内存使用量为 N×。
片段缓存解决方案
Hexo 的 fragment_cache() 函数保存渲染的模板内容并在页面之间重用。对于不随页面变化的组件至关重要。
关键原则:按语言缓存,而不是全局缓存。
<%
if (!config.relative_link) {
%>
<%- fragment_cache('header-' + (page.lang || 'en'), function(){
return partial('common/header');
}) %>
<%
} else {
%>
<%- partial('common/header') %>
<%
}
%>
缓存键策略:
header-en、header-zh-TW、header-zh-CN、header-ja(4 个缓存条目)- 每种语言获得隔离的缓存
- 在该语言的所有页面中重用
缓存什么
页眉(layout.ejs):
<%- fragment_cache('header-' + (page.lang || 'en'), function(){
return partial('common/header');
}) %>
- 导航、语言选择器、徽标
- 4 个缓存条目(每种语言一个)
- 在每种语言的约 100+ 页面中重用
侧边栏(sidebar.ejs):
<%- fragment_cache('sidebar-static-' + (page.lang || 'en'), function(){
return partial('common/sidebar-static');
}) %>
- 社交链接、结构
- 小部件按语言单独缓存
小部件(sidebar-widgets.ejs):
<%- fragment_cache('widget-recent_posts-' + (page.lang || 'en'), function(){
return partial('widget/recent_posts');
}) %>
- 在模板中按语言过滤的最近文章:
<%_ site.posts.filter(function(post) {
return (post.lang || 'en') === currentLang;
}).limit(5).each(function(post) { _%>
- 在该语言的所有页面中显示相同的 5 篇文章
页脚(footer.ejs):
<%- fragment_cache('footer-static-' + (page.lang || 'en'), function(){
return partial('common/footer-static');
}) %>
<%- partial('common/footer-dynamic') %>
- 静态:版权、致谢(已缓存)
- 动态:二维码(特定于页面)
脚本(layout.ejs):
<%- fragment_cache('scripts-' + (page.lang || 'en'), function(){
return partial('common/scripts');
}) %>
- JavaScript 包含按语言缓存
配置要求
# _config.yml
relative_link: false # 安全缓存所需
⚠️ 相对链接破坏缓存
片段缓存捕获页面对象。使用 relative_link: true 时,缓存的内容包含错误的路径。启用缓存时始终使用绝对路径。
客户端语言选择器
服务器端语言切换会破坏片段缓存。解决方案:基于 JavaScript 的 URL 生成。
// 检测当前路径
var currentPath = window.location.pathname;
var basePath = currentPath.replace(/^\/[a-z]{2}(-[A-Z]{2})?\//, '/');
// 生成语言特定的 URL
document.querySelectorAll('[data-lang]').forEach(function(link) {
var targetLang = link.getAttribute('data-lang');
var targetUrl = targetLang === 'en' ? basePath : '/' + targetLang + basePath;
link.href = targetUrl;
});
优点:
- 语言选择器 HTML 按语言缓存
- URL 从真实浏览器位置生成
- 没有过时的页面对象问题
性能影响
优化前(本站 4 种语言):
- 构建时间:约 1 分钟,400+ 页面(4 种语言 × 100+ 篇文章)
- 内存使用:高模板渲染开销
- 每个页面从头开始渲染页眉/页脚/侧边栏
优化后:
- 构建时间:约 40 秒(减少 60%)
- 内存使用:显著降低
- 缓存命中率:每种语言 80-95%
本站的数学计算:
- 无缓存:400 页 × 5 个组件 = 2,000 次渲染
- 有缓存:20 个缓存条目(4 种语言 × 5 个组件)+ 400 次动态渲染
- 一般公式:每个组件 N 个缓存条目,而不是(页面 × N)次渲染
内容管理工作流程
创建新文章
- 首先编写英文版本(真实来源):
hexo new post "Article Title"
# 创建:source/_posts/Article-Title.md
- 添加语言元数据:
---
title: "Article Title"
date: 2025-10-30
lang: en
categories: Development
tags:
- Hexo
excerpt: "英文引人注目的摘要"
---
- 使用语言后缀创建翻译:
# 在文件名中使用语言后缀
cp source/_posts/Article-Title.md source/_posts/Article-Title-zh-TW.md
cp source/_posts/Article-Title.md source/_posts/Article-Title-zh-CN.md
cp source/_posts/Article-Title.md source/_posts/Article-Title-ja.md
- 更新翻译元数据(不需要固定链接):
---
title: "文章标题"
date: 2025-10-30
lang: zh-CN # 补丁自动生成:/zh-CN/2025/10/Article-Title/
categories: Development
tags:
- Hexo
excerpt: "简体中文摘要"
---
💡 自动 URL 生成
应用补丁后:
Article-Title-zh-TW.md→/zh-TW/2025/10/Article-Title/Article-Title-ja.md→/ja/2025/10/Article-Title/- 语言后缀自动从 URL 中去除
- 不需要手动
permalink配置
处理未翻译的内容
对于仅以一种语言提供的内容,使用 original_lang_url:
---
title: "CISP 认证"
date: 2025-10-30
lang: zh-CN
categories: Cybersecurity
original_lang_url: /zh-CN/2025/10/cisp-certification/
---
这会显示重定向通知而不是内容,将读者引导到可用的语言版本。
资源管理
图片和资源
英文文章:使用相对路径

翻译文章:使用指向英文资源的绝对路径

优点:
- 无资源重复
- 单一真实来源
- 更易维护
内部链接
始终使用带语言前缀的绝对路径:
[Tools](/tools/)
[Previous Post](/2025/09/Previous-Article/)
[工具](/zh-CN/tools/)
[上一篇文章](/zh-CN/2025/09/Previous-Article/)
SEO 注意事项
摘要管理
每种语言具有不同的字符密度:
excerpt_length:
default: 200
en: 200
zh-TW: 100
zh-CN: 100
ja: 100
始终在前置数据中提供手动摘要以获得更好的控制:
excerpt: "通过自定义插件掌握 Hexo 中的多语言内容管理。"
分类翻译
保持分类键为英文,翻译显示名称:
category_i18n:
Development:
en: "Development"
zh-TW: "开发"
zh-CN: "开发"
ja: "開発"
标签
无论内容语言如何,始终使用英文标签:
tags:
- Hexo
- i18n
- Static Site
这确保了跨语言的一致标签页面。
其他性能提示
开发期间禁用插件
许多 i18n 插件可以在本地预览期间禁用以加快开发速度:
# _config.yml
i18n_archive_generator:
enable: false # 预览时禁用
i18n_category_generator:
enable: false # 预览时禁用
i18n_tag_generator:
enable: false # 预览时禁用
sitemap_i18n:
enable: false # 预览时禁用
canonical_multilang:
enable: false # 预览时禁用
部署前启用:
# 部署前为所有插件设置 enable: true
hexo clean && hexo generate
优点:
- 更快的本地构建(秒而不是分钟)
- 内容创建期间更快的迭代
- 仅在生产需要时才具有完整功能
💡 开发工作流程
在日常工作中保持插件在 _config.yml 中禁用。仅在 CI/CD 管道中或手动部署前启用它们。
故障排除
出现错误语言内容
原因:片段缓存未使用语言特定的键
解决方案:验证缓存键包含 page.lang:
<%- fragment_cache('component-' + (page.lang || 'en'), function(){
return partial('common/component');
}) %>
语言选择器显示 404
原因:服务器端路径检测与片段缓存
解决方案:实现客户端 URL 生成(见上文)
构建性能下降
原因:缓存已禁用或 relative_link: true
解决方案:
- 设置
relative_link: false - 清除缓存:
hexo clean - 重新生成:
hexo generate
翻译未出现
原因:缺少 lang 元数据或固定链接不正确
解决方案:验证前置数据包含:
lang: zh-CN(或适当的语言代码)- 使用语言后缀的文件名(例如
Article-Title-zh-CN.md)
结论
构建多语言 Hexo 博客需要自定义插件和仔细优化,但结果是一个快速、SEO 友好的网站,为全球受众提供服务。关键见解:
- 分离关注点:文章集中化,页面基于目录
- 积极缓存:每种语言的片段缓存将构建时间减少 60%
- 客户端增强:JavaScript 处理动态元素而不破坏缓存
- 一致的结构:英文作为真实来源,翻译遵循既定模式
此实现为跨 4 种语言的 400+ 页面博客提供支持,在不到 2 分钟内构建,同时保持出色的性能和 SEO。
静态网站生成的未来是多语言的,通过正确的架构,Hexo 可以提供世界级的结果。