在 Hexo 中启用 i18n:多语言博客完整指南

构建多语言博客不仅仅是翻译内容——而是在保持性能和 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') %>  <!-- 输出:Home 或 首页 -->

路径检测

i18n_dir: :lang  # 从第一个 URL 段检测语言

示例:

  • /index.htmlen(默认)
  • /zh-tw/index.htmlzh-tw
  • /archives/index.htmlen(未检测为语言)

存在的问题

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.mdArticle-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 的固定链接系统深度集成到文章处理器中:

  1. 处理器读取文件并将 permalink__permalink 转换
  2. 立即应用默认固定链接模式(:title/
  3. post_permalink 过滤器仅在 __permalink 存在时运行
  4. 虚拟 path 属性从过滤器计算,无法修改 解决方案:修补 node_modules/hexo/dist/plugins/filter/post_permalink.js 以:
  • 从文章数据中提取 lang 字段
  • 去除 slug 中的 -{lang} 后缀(例如,Article-Title-jaArticle-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 维护者提议此功能
💡贡献开源项目

与其打补丁,不如考虑

  1. 开启 GitHub issue:描述 i18n permalink 使用案例
  2. 提议解决方案:分享您的补丁作为起点
  3. 提交 pull request:向上游贡献此功能
  4. 优点
    • 功能由 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/ ✅(后缀已去除) 关键特性
  1. 语言检测:从前置数据读取 lang 字段
  2. 后缀去除:从 slug 中移除 -ja-zh-TW-zh-CN
  3. 前缀注入:为非默认语言添加 /{lang}/
  4. 手动覆盖:尊重前置数据中的显式 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.mdtools
  • 注入语言前缀(例如,/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. 索引页面

插件hexo-generator-i18n-index 创建每种语言的独立索引页面,支持置顶文章:

npm install hexo-generator-i18n-index
index_generator:
  per_page: 24
  order_by: -date

排序逻辑

  1. 常规文章在前,重定向文章(original_lang_url)在后
  2. 置顶/固定文章在顶部
  3. 然后按日期排序(最新的在前) 结果
  • / - 仅英文文章
  • /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. 标签页面

插件hexo-generator-i18n-tag 创建按语言过滤的标签页面:

i18n_tag_generator:
  enable: true
  per_page: 24

6. 规范标签

插件hexo-plugin-i18n-canonical 通过添加适当的规范标签来防止重复内容惩罚。 问题:搜索引擎将翻译页面视为重复内容:

  • /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-enheader-zh-TWheader-zh-CNheader-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)次渲染

内容管理工作流程

创建新文章

  1. 首先编写英文版本(真实来源):
hexo new post "Article Title"
# 创建:source/_posts/Article-Title.md
  1. 添加语言元数据
---
title: "Article Title"
date: 2025-10-30
lang: en
categories: Development
tags:
  - Hexo
excerpt: "英文引人注目的摘要"
---
  1. 使用语言后缀创建翻译
# 在文件名中使用语言后缀
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
  1. 更新翻译元数据(不需要固定链接):
---
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/enabling-i-18-n-in-hexo/
---

这会显示重定向通知而不是内容,将读者引导到可用的语言版本。

资源管理

图片和资源

英文文章:使用相对路径

![图表](diagram.png)

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

![图表](/2025/10/Article-Title/diagram.png)

优点

  • 无资源重复
  • 单一真实来源
  • 更易维护

内部链接

始终使用带语言前缀的绝对路径:

<!-- 英文 -->
[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 解决方案

  1. 设置 relative_link: false
  2. 清除缓存:hexo clean
  3. 重新生成:hexo generate

翻译未出现

原因:缺少 lang 元数据或固定链接不正确 解决方案:验证前置数据包含:

  • lang: zh-CN(或适当的语言代码)
  • 使用语言后缀的文件名(例如 Article-Title-zh-CN.md

结论

构建多语言 Hexo 博客需要自定义插件和仔细优化,但结果是一个快速、SEO 友好的网站,为全球受众提供服务。关键见解:

  • 分离关注点:文章集中化,页面基于目录
  • 积极缓存:每种语言的片段缓存将构建时间减少 60%
  • 客户端增强:JavaScript 处理动态元素而不破坏缓存
  • 一致的结构:英文作为真实来源,翻译遵循既定模式 此实现为跨 4 种语言的 400+ 页面博客提供支持,在不到 2 分钟内构建,同时保持出色的性能和 SEO。 静态网站生成的未来是多语言的,通过正确的架构,Hexo 可以提供世界级的结果。