介绍

打造一个支持多语言的 Hexo 博客并不简单,但也没有想象中那么难!在折腾了几天之后,我总结出了一些清晰易懂的步骤,希望能帮你轻松搞定多语言设置。本教程会特别针对 Butterfly 博客主题,但大概率也适用于其他主题。无论你是 Hexo 的新手,还是已经摸索过一段时间的老司机,这篇教程都能为你提供一些帮助。让我们一起开始吧!

本站的方法主要是全站复制,然后放在输出文件夹下,好处是结构比较清晰,坏处是改的比较麻烦。

下面是主体的目录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//配置文件
your_hexo_blog
├── _config.yml <-- 默认的
├── _config-cn.yml
├── _config-fr.yml
├── _config-jp.yml

//主题配置文件
├── _config.butterfly.yml <-- 默认的
├── _config.butterfly-cn.yml
├── _config.butterfly-fr.yml
├── _config.butterfly-jp.yml

//页面主文件夹
├── source <-- 默认的
├── source-cn
├── source-fr
├── source-jp

//新的脚本文件夹
├── scripts <-- 脚本文件
├──config-debug.js
├──change_path.js

下面是一些需要修改的文件(可选):

1
2
3
4
5
6
7
(basically your theme folder)
hexo-theme-butterfly/layout/includes
├── footer.pug
├── head.pug
└── third-party
└── comments
└── twikoo.pug

下面是我用到的工具:

  • Curosr Agent(Claude 3.5 Sonnet)
  • Cursor Chat (GPT-4o)

修改config文件

新增_config-(语言代码).yml

你需要按照上面的配置文件结构添加文件并做以下修改:

举中文为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Site
language: zh-CN

# URL
## Set your site url here. For example, if you use GitHub Page, set url as 'https://username.github.io/project'
url: https://example.com/cn
root: /cn/

# Directory
source_dir: source-cn
public_dir: cn

# Include / Exclude file(s)
## include:/exclude: options only apply to the 'source/' folder
include:
exclude:
ignore:
- source-jp/
- source-fr/
- source/

其他保持不动,这边主要改的是一些基础配置。

当你使用hexo s --config _config-cn.yml 的时候,会访问localhost:4000/cn/ ,所以需要修改url和root

修改主题配置文件

我这边使用的是 butterfly 4.13 版本,虽然 5.0.0 及以后的版本对配置文件做了一些调整,但对本教程的内容影响不大。如果你使用的是较新版本,可以参考官方文档进行相应调整。

新增_config.butterfly-(语言代码).yml

举中文为例:

1
2
3
4
5
6
7
8
9
10
11
# Menu改中文
menu:
# Article[/article]
# List[/list]
# Link[/link]
# About[/overview]
文章:
中文: /tags/
中文: /archives/
中文: /analysis/
中文: /sitemap/

其他保持不动,这边主要改的是一些基础配置。

复制source文件夹

复制source文件夹,并重命名为source-(语言代码)

用cursor的Composer Agent功能 让它保持结构 把source文件夹下的文件全部翻译 就可以了,我总共64篇文件,翻译了大概20分钟。中间偶然需要你手动确认下,总体体验还可以。

添加scripts文件夹

在根目录下添加scripts文件夹,并添加config-debug.js和change_path.js文件。

config-debug.js用于匹配站点配置文件和主题配置文件(虽然目前这个方案可能不是最优解,但在找到更好的Hexo自动匹配配置文件的方法之前,它能满足我们的需求)。而change_path.js则负责处理和修改文件路径。这两个脚本都是由Claude写的,可用。

config-debug.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
35
36
37
38
39
40
41
42
'use strict';

hexo.extend.filter.register('before_generate', async () => {
// 检查是否使用了 --config 参数
const configArg = process.argv.find(arg => arg.startsWith('_config-'));
if (!configArg) return;

// 提取语言代码
const langMatch = configArg.match(/config-([a-z]{2,})\.yml$/);
if (!langMatch) return;

const lang = langMatch[1];
const themeConfigPath = `_config.butterfly-${lang}.yml`;

// 使用 Promise 确保配置完全加载
await new Promise((resolve, reject) => {
try {
const yaml = require('js-yaml');
const fs = require('fs');

// 读取文件内容
const fileContent = fs.readFileSync(themeConfigPath, 'utf8');

// 解析 YAML
const themeConfig = yaml.load(fileContent);

// 验证配置是否完整
if (!themeConfig || typeof themeConfig !== 'object') {
throw new Error('Invalid theme config format');
}

// 应用配置
hexo.theme.config = themeConfig;
console.log(`Theme config loaded successfully: ${themeConfigPath}`);

resolve();
} catch (e) {
console.error(`Failed to load theme config: ${themeConfigPath}`, e);
reject(e);
}
});
});

change_path.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
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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
// scripts/resource-path-handler.js
'use strict';

hexo.extend.filter.register('after_render:html', function(str, data) {
// 从命令行参数中获取配置文件名
const configFile = process.argv.find(arg => arg.includes('config-') && arg.endsWith('.yml'));
if (!configFile) return str;

// 提取语言代码
const langMatch = configFile.match(/config-([a-z]{2,})\.yml$/);
if (!langMatch) return str;

const lang = langMatch[1];
if (lang === 'default') return str;

// 需要排除的路径
const excludePaths = [
'/search.xml',
'/sitemap.xml',
'/robots.txt',
'/feed.xml',
'http://',
'https://',
'data:image',
'//',
'ws://',
'wss://'
];

// 处理HTML中的静态资源路径
let result = str;

// 通用的资源路径处理规则
const processPath = (match, p1, p2, p3) => {
// 检查是否在排除列表中
if (excludePaths.some(exclude => p2.startsWith(exclude))) {
return match;
}
// 检查路径是否已经包含语言前缀
if (p2.startsWith(`/${lang}/`)) {
return match;
}
// 确保路径以 / 开头且不是相对路径
if (p2.startsWith('/') && !p2.startsWith('//')) {
return `${p1}/${lang}${p2}${p3}`;
}
return match;
};

// 处理所有可能的资源引用
const patterns = [
// src 属性 (脚本、图片、视频、音频等)
{
pattern: /(src=["'])(\/[^"']+)(["'])/g
},
// href 属性 (样式表、链接等)
{
pattern: /(href=["'])(\/[^"']+)(["'])/g
},
// data-url 属性
{
pattern: /(data-url=["'])(\/[^"']+)(["'])/g
},
// content 属性 (meta 标签)
{
pattern: /(content=["'])(\/[^"']+)(["'])/g
},
// 内联样式中的 URL
{
pattern: /(url\(["']?)(\/[^"')]+)(["']?\))/g
}
];

// 应用所有模式
patterns.forEach(({ pattern }) => {
result = result.replace(pattern, processPath);
});

return result;
});

// 注册 helper 函数用于模板中手动控制资源路径
hexo.extend.helper.register('langPath', function(path) {
if (!path || typeof path !== 'string') return path;

const configFile = process.argv.find(arg => arg.includes('config-') && arg.endsWith('.yml'));
if (!configFile) return path;

const langMatch = configFile.match(/config-([a-z]{2,})\.yml$/);
if (!langMatch) return path;

const lang = langMatch[1];
if (lang === 'default') return path;

// 检查是否是需要排除的路径
const excludePaths = [
'/search.xml',
'/sitemap.xml',
'/robots.txt',
'/feed.xml',
'http://',
'https://',
'data:image',
'//',
'ws://',
'wss://'
];

if (excludePaths.some(exclude => path.startsWith(exclude))) {
return path;
}

// 检查路径是否已经包含语言前缀
if (path.startsWith(`/${lang}/`)) {
return path;
}

return path.startsWith('/') ? `/${lang}${path}` : path;
});

可选内容

以下组件主要用于优化用户体验,可根据需要选择性使用:

这个是页脚组件,主要用于显示版权信息、备案信息、友情链接等。语言转换按钮可以放在footer.pug或者navbar.pug。如果要体验好一点的话,可以搞个js,看用户浏览器语言一样不一样,如果不一样,然后有匹配的语言,提前访问下,如果不是404,跳个弹窗,让用户选择语言。

head.pug

主要用于添加多语言的link标签,用于搜索引擎识别。目前只做了主页的。其实可以写配置文件 我为了省力,先这样弄了,能跑就行。

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
if is_home()
- var fullUrl = config.url
- var baseUrl = fullUrl.replace(/\/(cn|jp|fr)$/, '') // 移除语言后缀,得到基础URL
- var currentLang = fullUrl.match(/\/(cn|jp|fr)$/) ? fullUrl.match(/\/(cn|jp|fr)$/)[1] : 'en' // 获取当前语言

if currentLang === 'cn'
link(rel="alternate" hreflang="zh-CN" href=`${baseUrl}/cn`)
link(rel="alternate" hreflang="en" href=baseUrl)
link(rel="alternate" hreflang="ja-JP" href=`${baseUrl}/jp`)
link(rel="alternate" hreflang="fr" href=`${baseUrl}/fr`)
link(rel="alternate" hreflang="x-default" href=baseUrl)
else if currentLang === 'jp'
link(rel="alternate" hreflang="ja-JP" href=`${baseUrl}/jp`)
link(rel="alternate" hreflang="en" href=baseUrl)
link(rel="alternate" hreflang="zh-CN" href=`${baseUrl}/cn`)
link(rel="alternate" hreflang="fr" href=`${baseUrl}/fr`)
link(rel="alternate" hreflang="x-default" href=baseUrl)
else if currentLang === 'fr'
link(rel="alternate" hreflang="fr" href=`${baseUrl}/fr`)
link(rel="alternate" hreflang="en" href=baseUrl)
link(rel="alternate" hreflang="zh-CN" href=`${baseUrl}/cn`)
link(rel="alternate" hreflang="ja-JP" href=`${baseUrl}/jp`)
link(rel="alternate" hreflang="x-default" href=baseUrl)
else
link(rel="alternate" hreflang="en" href=baseUrl)
link(rel="alternate" hreflang="zh-CN" href=`${baseUrl}/cn`)
link(rel="alternate" hreflang="ja-JP" href=`${baseUrl}/jp`)
link(rel="alternate" hreflang="fr" href=`${baseUrl}/fr`)
link(rel="alternate" hreflang="x-default" href=baseUrl)

twikoo.pug

主要是为了做评论融合。

举个例子:

通过修改twikoo.pug配置,可以让中文版文章显示原始文章的评论内容,实现评论数据共享。这样用户无论访问哪个语言版本的文章,都能看到完整的评论记录。

twikoo.pug 的配置文件如下,复制粘贴即可:

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
- const { envId, region, option } = theme.twikoo
- const { use, lazyload, count } = theme.comments
- const multi_lang = theme.twikoo && theme.twikoo.multi_lang_prefix_handling === true
- const lang_prefixes = multi_lang ? theme.twikoo.lang_prefixes : null

script.
(() => {
const getCount = () => {
const countELement = document.getElementById('twikoo-count')
if(!countELement) return
twikoo.getCommentsCount({
envId: '!{envId}',
region: '!{region}',
urls: [window.location.pathname],
includeReply: false
}).then(res => {
countELement.textContent = res[0].count
}).catch(err => {
console.error(err)
})
}

const init = () => {
// 获取处理后的路径
let path = window.location.pathname
if (!{multi_lang} && !{JSON.stringify(lang_prefixes)}) {
const prefixes = !{JSON.stringify(lang_prefixes)}
if (prefixes && prefixes.length > 0) {
const langRegex = new RegExp(`^/(${prefixes.join('|')})/`)
path = path.replace(langRegex, '/')
}
}

twikoo.init(Object.assign({
el: '#twikoo-wrap',
envId: '!{envId}',
region: '!{region}',
path: path,
onCommentLoaded: () => {
btf.loadLightbox(document.querySelectorAll('#twikoo .tk-content img:not(.tk-owo-emotion)'))
}
}, !{JSON.stringify(option)}))

!{count ? 'GLOBAL_CONFIG_SITE.isPost && getCount()' : ''}
}

const loadTwikoo = () => {
if (typeof twikoo === 'object') setTimeout(init,0)
else getScript('!{url_for(theme.asset.twikoo)}').then(init)
}

if ('!{use[0]}' === 'Twikoo' || !!{lazyload}) {
if (!{lazyload}) btf.loadComment(document.getElementById('twikoo-wrap'), loadTwikoo)
else loadTwikoo()
} else {
window.loadOtherComment = loadTwikoo
}
})()

需要修改下不同语言文件的_config.butterfly-(语言代码).yml的twikoo配置,把multi_lang_prefix_handling设置为true。

1
2
3
4
5
6
7
8
9
10
11
12
# Twikoo
# https://github.com/imaegoo/twikoo
twikoo:
envId:
region:
visitor: false
multi_lang_prefix_handling: true
lang_prefixes:
- cn
- fr
- jp
option:

总结

至此,我们已经完成了多语言博客的所有配置工作。

要测试生成所有语言版本的静态文件,运行以下命令:

1
hexo clean && hexo g && hexo clean  --config _config-cn.yml && hexo g --config _config-cn.yml && hexo clean  --config _config-fr.yml && hexo g --config _config-fr.yml && hexo clean  --config _config-jp.yml && hexo g --config _config-jp.yml 

之后可以放package.json 里面,按照各自的hexo d 部署到服务器即可。

运行完以上命令后,你只需按照各自的配置文件,使用 hexo d 将不同语言版本的内容部署到服务器即可。这样,一个支持多语言的 Hexo 博客就成功上线了!

如果使用框架如 Next.js,配置多语言支持更加简单。。。

参考:

Cover From Internet