Initializing...
学习如何使用 Hexo 创建最佳的多语言博客,在这本终极指南中简化您的设置过程,释放国际化的力量,以接触全球受众。本教程提供清晰的步骤和专家提示,帮助您掌握 Hexo 的多语言功能,轻松提升您的博客体验。
介绍 打造一个支持多语言的 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 language: zh-CN url: https://example.com/cn root: /cn/ source_dir: source-cn public_dir: cn 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: 文章: 中文: /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 () => { 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` ; await new Promise ((resolve, reject ) => { try { const yaml = require ('js-yaml' ); const fs = require ('fs' ); const fileContent = fs.readFileSync (themeConfigPath, 'utf8' ); 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 '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://' ]; 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 = [ { pattern : /(src=["'])(\/[^"']+)(["'])/g }, { pattern : /(href=["'])(\/[^"']+)(["'])/g }, { pattern : /(data-url=["'])(\/[^"']+)(["'])/g }, { pattern : /(content=["'])(\/[^"']+)(["'])/g }, { pattern : /(url\(["']?)(\/[^"')]+)(["']?\))/g } ]; patterns.forEach (({ pattern } ) => { result = result.replace (pattern, processPath); }); return result; }); 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: 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