Introduction

  • Why Support Multiple Languages: To reach a broader user base. Although the internet theoretically connects people worldwide, most individuals instinctively cluster around their native languages. English and Chinese are the two most widely used languages globally, so support for these two languages is prioritized.
  • This document is part of the Hexo blog setup guide series, version 1.4, which mainly introduces the configuration and optimization plan for the Hexo blog using the Butterfly theme that supports language switching. The entire blog setup guide series can be found at: Hexo Blog Setup Guide: Systematic Solutions and Detailed Construction Process.
  • Overall Plan:
    • Hexo blog using the Butterfly theme.
    • The Chinese and English blogs are set up as two independent projects:
      • Chinese blog: my-hexo-blog-cn; English blog: my-hexo-blog-en.
      • Benefits: mutual backup; simplified management, reducing interference between Chinese and English versions.
    • The Chinese and English blogs are constructed as subdirectories rather than subdomains, which helps consolidate their search weight.
  • Refer to Hexo Blog Setup Guide: Systematic Solutions and Detailed Construction Process for an overall understanding of the current multi-language plan, as shown below:

Overall Blog Plan

Overall Process:

Overall Process

Explanation:

  • The overall process includes three stages: preparation, multi-language configuration support, and additional optimizations, comprising four modules: local Hexo - Chinese blog, local Hexo - English blog, GitHub, Cloudflare.
  • Use the “Switch language” feature to toggle between the Chinese and English blogs by adding or removing the /en/ prefix in the address bar.
    • Since the blog can only be deployed to Cloudflare Pages at the root directory, a Cloudflare worker is required to implement subdirectory deployment. The free quota is 100,000 requests per day, which is generally sufficient during language switching.

Preparation

Setting Up and Deploying the Chinese Blog

Setting Up and Deploying the English Blog

  • The process for setting up and deploying the English blog is similar to that of the Chinese blog.
  • Notes:
    • The GitHub repository name for the English blog should be /en/.

Multi-Language Configuration Support

Local Configuration for Chinese Blog

Configuration of hexo’s config file

1
2
3
# Set basic URL and root directory for the website
url: https://blog.dmindie.com
root: /

Configuration of _config.butterfly.yml file

  • Add a language switch button to the navigation bar.
1
2
3
4
5
6
7
8
9
10
11
12
menu: # Navigation menu names
首页: / || fas fa-home
分类: /categories/ || fas fa-folder-open
标签: /tags/ || fas fa-tags
归档: /archives/ || fas fa-archive
关于: /about/ || fas fa-heart #fas fa-heart;
友链: /link/ || fas -link
# Add language switch menu items
语言||fas fa-language:
简体中文: javascript:switchLanguage('cn')
English: javascript:switchLanguage('en')

Adding Switch Language Logic

  • Add the following code to layout.pug (my-hexo-blog-cn/themes/butterfly/layout/includes/layout.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
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
- var globalPageType = getPageType(page, is_home)
- var htmlClassHideAside = theme.aside.enable && theme.aside.hide ? 'hide-aside' : ''
- page.aside = globalPageType === 'archive' ? theme.aside.display.archive : globalPageType === 'category' ? theme.aside.display.category : globalPageType === 'tag' ? theme.aside.display.tag : page.aside
- var hideAside = !theme.aside.enable || page.aside === false ? 'hide-aside' : ''
- var pageType = globalPageType === 'post' ? 'post' : 'page'
- pageType = page.type ? pageType + ' type-' + page.type : pageType

doctype html
html(lang=config.language data-theme=theme.display_mode class=htmlClassHideAside)
head
include ./head.pug
body
!=partial('includes/loading/index', {}, {cache: true})

if theme.background
#web_bg(style=getBgPath(theme.background))

!=partial('includes/sidebar', {}, {cache: true})

#body-wrap(class=pageType)
include ./header/index.pug

main#content-inner.layout(class=hideAside)
if body
div!= body
else
block content
if theme.aside.enable && page.aside !== false
include widget/index.pug

- const footerBg = theme.footer_img
- const footer_bg = footerBg ? footerBg === true ? bg_img : getBgPath(footerBg) : ''
footer#footer(style=footer_bg)
!=partial('includes/footer', {}, {cache: true})

include ./rightside.pug
include ./additional-js.pug

// Below is Switch language logic
script.
function switchLanguage(targetLang) {
const currentPath = window.location.pathname;
let newPath;

if (targetLang === 'cn') {
// Switch to Chinese
if (currentPath.startsWith('/en/')) {
// Currently on an English page, need to remove /en/ prefix
newPath = currentPath.substring(3) || '/';

// Check if new path exists
fetch(newPath)
.then(response => {
if (response.ok) {
// If page exists, redirect directly
window.location.href = newPath;
} else if (response.status === 404) {
// If page does not exist, redirect to Chinese homepage
window.location.href = '/';
}
})
.catch(() => {
// Handle network errors or other issues with default behavior
console.error('Network error occurred.');
});
} else {
console.log('Already on the Chinese page.');
}
} else if (targetLang === 'en') {
// Switch to English
if (!currentPath.startsWith('/en/')) {
// Currently on a Chinese page, need to add /en/ prefix
newPath = '/en' + (currentPath === '/' ? '/' : currentPath);

// Check if new path exists
fetch(newPath)
.then(response => {
if (response.ok) {
// If page exists, redirect directly
window.location.href = newPath;
} else if (response.status === 404) {
// If page does not exist, redirect to no-match page
window.location.href = '/en/no-match/';
}
})
.catch(() => {
// Handle network errors or other issues with default behavior
console.error('Network error occurred.');
});
} else {
// Already on an English page; no need to change.
console.log('Already on the English page.');
}
}
}

Local Configuration for English Blog

Configuration of hexo’s config file

1
2
3
# Set basic URL and root directory for the website
url: https://blog.dmindie.com/en/
root: /en/

Configuration of _config.butterfly.yml file

1
2
3
4
5
6
7
8
9
10
11
12
menu: # Navigation menu names
Home: / || fas fa-home
Categories: /categories/ || fas fa-folder-open
Tags: /tags/ || fas fa-tags
Archives: /archives/ || fas fa-archive
About: /about/ || fas fa-heart #fas fa-heart;
Link: /link/ || fas fa-link
# Add language switch menu items
Language||fas fa-language:
简体中文: javascript:switchLanguage('cn')
English: javascript:switchLanguage('en')

Adding Switch Language Logic

  • Add Switch language logic in layout.pug (my-hexo-blog-en/themes/butterfly/layout/includes/layout.pug), with code similar to that in the Chinese blog.

New no-match.md File

  • Purpose: When switching from Chinese to English, if no corresponding content exists, redirect to no-match first, then use Cloudflare worker to redirect to the English blog homepage.
  • Content of no-match (my-blog/my-hexo-blog-en/source/no-match.md):
1
2
3
4
5
6
7
---
title: No Match Found
date: 2025-01-07 17:25:30
description: "The English version of this page is not available. You will be redirected to the homepage."
---

The English version of this page is not available. You will be redirected to the homepage.

Cloudflare Worker Configuration for English Blog

Worker Code Configuration

  • Log in to Cloudflare Dashboard and create a new Worker in the “Workers & Pages” section.
  • Use the following code to proxy requests starting with /en/ to the English blog project:
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
addEventListener('fetch', event => {
const url = new URL(event.request.url);

// Check if it's an English blog request
if (url.pathname.startsWith('/en')) {
// Replace with English blog address
url.hostname = 'my-hexo-blog-en.pages.dev';
url.pathname = url.pathname.replace('/en', '');

event.respondWith(
fetch(url.toString()).then(response => {
// Check if it's a no-match page
if (url.pathname === '/no-match/') {
// If it's a /no-match/ page, redirect to English homepage
return Response.redirect('<https://blog.dmindie.com/en/>', 302);
}
return response;
})
);
} else {
// Default handling for other requests (assumed as Chinese blog)
event.respondWith(fetch(event.request));
}
});

Binding Worker to Main Domain

  1. Log in to Cloudflare Dashboard.
  2. Go to Workers & Pages.
  3. In the left menu, select Workers & Pages.
  4. Find your newly created Worker (e.g., for multi-language proxy).
  5. Configure worker routing rules:
    • Domain: dmindie.com
    • Route: blog.dmindie.com/en*
    • Fallback mode: Open automatically on failure.

Binding Worker to Main Domain

Deployment Preview

  • After modifying both versions of the blog, deploy them to GitHub Pages and Cloudflare.
  • Open a browser and visit both versions of the blog:
    • https://blog.dmindie.com/
    • https://blog.dmindie.com/en

With these configurations in place, you can freely switch between languages on key pages such as home, categories, tags, archives, about, and links.

Multi-Language Optimization Support

Supporting Multi-Language Switching for Posts

To facilitate quick switching between Chinese and English posts on blogs, ensure that their URLs remain consistent except for the difference in prefixes (/en/) in permalinks:

Implementation Steps

  • Notion configuration: Both Chinese and English versions of Notion databases should include a field named urlname.
  • Elog configuration: Configure synchronization rules by adding urlname field in elog.config.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
module.exports = {
write: {
platform: 'notion',
notion: {
token: process.env.NOTION_TOKEN,
databaseId: process.env.NOTION_DATABASE_ID,
filter: true, // {property: 'status', select: {equals: '已发布'}}
frontMatter: {
enable: true, // Enable Front Matter
include: ['title', 'categories', 'tags', 'description', 'status', 'cover', 'urlname', 'date'], // Fields to include; deploy and write should be configured consistently
},
},
},
deploy: {
platform: 'local',
local: { // For publishing the deployed content
outputDir: './source/_posts/', // Directory to publish to Hexo's posts
filename: 'title',
format: 'matter-markdown',
frontMatter: {
enable: true, // Enable Front Matter
include: ['title', 'categories', 'tags', 'description', 'status', 'cover', 'urlname', 'date'], // Fields to include; deploy and write should be configured consistently
},
},
},
}

  1. For details on syncing Notion content with Hexo blogs using Elog, refer to Hexo Blog Setup Guide via Elog Syncing Notion Content.
  2. Verify results by checking how articles switch between their Chinese and English versions.

SEO Optimization Based on Hreflang Tags

Adding Hreflang Tags

For the Chinese blog in head.pug (projects/my-blog/my-hexo-blog-cn/themes/butterfly/layout/includes/head.pug), add:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Add hreflang tags
// Define base URL
- const baseUrl = '<https://blog.dmindie.com>'
- let currentPath = page.path
// Handle various paths
if (currentPath === 'index.html' || currentPath === '/index.html') {
currentPath = ''
} else {
currentPath = currentPath.replace(/index\\.html$/, '').replace(/^\\/+|\\/+$/g,'')
}
// Add hreflang tags
link(rel="alternate" hreflang="zh-CN" href=`${baseUrl}/${currentPath}`)
link(rel="alternate" hreflang="en" href=`${baseUrl}/en/${currentPath}`)
link(rel="alternate" hreflang="x-default" href=`${baseUrl}/${currentPath}`)

For the English blog in head.pug (projects/my-blog/my-hexo-blog-en/themes/butterfly/layout/includes/head.pug), add:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Add hreflang tags
// Define base URL
const baseUrl = '<https://blog.dmindie.com>'
let currentPath = page.path
// Handle various paths
if (currentPath === 'index.html' || currentPath === '/index.html') {
currentPath = ''
} else {
currentPath = currentPath.replace(/index\\.html$/, '').replace(/^\\/+|\\/+$/g,'')
}
// Remove en prefix from path (if exists)
currentPath = currentPath.replace(/^en\\//,'')
// Add hreflang tags
link(rel="alternate" hreflang="en" href=`${baseUrl}/en/${currentPath}`)
link(rel="alternate" hreflang="zh-CN" href=`${baseUrl}/${currentPath}`)
link(rel="alternate" hreflang="x-default" href=`${baseUrl}/${currentPath}`)

Other Multi-Language Solutions

Simplified Multi-Language Deployment Only on GitHub Pages

Deployment Plan Adjustments

  • The first version of the blog will be deployed at GitHub Pages root directory.
  • Remaining versions will be deployed in subdirectories under GitHub Pages since multiple versions are automatically deployed into subdirectories there.
  • No longer deploy on Cloudflare Pages.

Adjusting Switch Language Logic

The config files for hexo and _config.butterfly.yml will remain consistent with previous plans.

For both Chinese and English projects within layout.pug (my-hexo-blog-cn/themes/butterfly/layout/includes/layout.pug) and layout.pug (my-hexo-blog-en/themes/butterfly/layout/includes/layout.pug), add Switch language logic as follows:

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
- var globalPageType = getPageType(page, is_home)
- var htmlClassHideAside = theme.aside.enable && theme.aside.hide ? 'hide-aside' : ''
- page.aside = globalPageType === 'archive' ? theme.aside.display.archive : globalPageType === 'category' ? theme.aside.display.category : globalPageType === 'tag' ? theme.aside.display.tag : page.aside
- var hideAside = !theme.aside.enable || page.aside === false ? 'hide-aside' : ''
- var pageType = globalPageType === 'post' ? 'post' : 'page'
- pageType = page.type ? pageType + ' type-' + page.type : pageType

doctype html
html(lang=config.language data-theme=theme.display_mode class=htmlClassHideAside)
head
include ./head.pug
body
!=partial('includes/loading/index', {}, {cache: true})

if theme.background
#web_bg(style=getBgPath(theme.background))

!=partial('includes/sidebar', {}, {cache: true})

#body-wrap(class=pageType)
include ./header/index.pug

main#content-inner.layout(class=hideAside)
if body
div!= body
else
block content
if theme.aside.enable && page.aside !== false
include widget/index.pug

- const footerBg = theme.footer_img
- const footer_bg = footerBg ? footerBg === true ? bg_img : getBgPath(footerBg) : ''
footer#footer(style=footer_bg)
!=partial('includes/footer', {}, {cache: true})

include ./rightside.pug
include ./additional-js.pug

// The following is the Switch language logic
script.
function switchLanguage(targetLang) {
const currentPath = window.location.pathname;
let newPath;

if (targetLang === 'cn') {
// If the target is Chinese
if (currentPath.startsWith('/en/')) {
// Currently on an English page, need to remove the /en/ prefix
newPath = currentPath.substring(3) || '/';
} else {
// Already on a Chinese page, no need to change
newPath = currentPath;
}
} else {
// If the target is English
if (!currentPath.startsWith('/en/')) {
// Currently on a Chinese page, need to add the /en/ prefix
newPath = '/en' + (currentPath === '/' ? '/' : currentPath);
} else {
// Already on an English page, no need to change
newPath = currentPath;
}
}

// Check if the new path exists
fetch(newPath)
.then(response => {
if (response.ok) {
// If the page exists, redirect directly
window.location.href = newPath;
} else {
// If the page does not exist, redirect to the corresponding language homepage
window.location.href = targetLang === 'cn' ? '/' : '/en/';
}
})
.catch(() => {
// Redirect to the corresponding language homepage in case of an error
window.location.href = targetLang === 'cn' ? '/' : '/en/';
});
}

Conclusion

With these configurations completed, anyone can set up a Hexo blog that supports multi-language switching effectively. Moving forward based on this multi-language solution will lead to further optimizations along with continuous updates of this article’s content.

If you have better solutions or ideas, please feel free to share them with us; we will continue updating this article to keep its content current and optimal.