本站從 Wordpress 遷移到 Hugo
文章目錄
時隔多年,本站終於進行了一次較大的更新,從 Wordpress 遷移到了 Hugo。這篇文章記錄了遷移的原因和過程。
緣由
想從 Wordpress 遷移到 Hugo 已經有五六年了,但一直沒有進行。想遷移的原因非常簡單:
- Wordpress 越來越臃腫。尤其是最近幾年,我幾乎都不想登錄到 Wordpress 後臺,太多的插件年久失修,也不敢亂動,甚至 Wordpress 自身升級後連新建頁面都打不開了;
- Wordpress 對 Markdown 的支持是非原生的;雖然使用插件後看似效果不錯,但從數據源的角度來說,本質上它還是存儲為
HTML
格式,這導致文章的結構化和批量處理效果一般。 - 網站的訪問中有相當比例來源於繁體中文使用者,因此想更好的支持繁體中文的閱讀和使用。
關鍵步驟
從 Wordpress 備份提取 Markdown
這是看似很簡單的步驟,因為有好幾個插件都支持相關的功能,也有一些命令行的腳本。但是,最要命的是轉換並不完美。
我使用的是基於 NodeJS 的 blog2md 腳本。
這個腳本的使用很簡單,只需要在 Wordpress 後臺導出備份的 .xml
文件,然後執行腳本就行了。
你的計算機需要有 Node
環境。
要注意的是,一定按照其 README 中的說明,添加 paragraph-fix
參數,否則很多換行和段落會出現問題。例如:
node index.js w your-wordpress-backup-export.xml out m paragraph-fix
但是即使你採用了上面的參數,轉換出的文章很多依然是有問題的。主要表現在:
- 部分段落丟失,即缺少換行;
- 很多之前的代碼高亮的內容出現了多餘的轉義,例如
*
變成了\*
、]
變成了\]
。 我的理解是當初為了在 Wordpress 中存儲代碼高亮,相關插件本身做了轉義,但在導出數據時就很痛苦了。 - (更要命的)以
<
開頭的內容丟失。可能因為和導出格式為 XML 有關,出現了語義上的衝突。這部分可能只能自己修了。
例如一個 C++ 程序:
template<typename T>
class ...
可能會變成
template typename T\>
class ...
所以,我需要寫腳本來解決這個問題。
使用腳本處理多餘的轉義
下面是一個 NodeJS 腳本,用來批量將指定目錄下的 md 文件的正文部分包裹的代碼塊中的多餘轉義進行修復。
例如,它將 \[
修復成 [
,\_
修復成 _
。
const fs = require('fs');
const path = require('path');
// 指定目錄路徑
const DIRECTORY_PATH = 'path/to/content';
// 遞歸地查找.md文件
function findMarkdownFiles(dirPath) {
let filesToProcess = [];
const files = fs.readdirSync(dirPath);
for (const file of files) {
const filePath = path.join(dirPath, file);
const stat = fs.statSync(filePath);
if (stat.isDirectory()) {
filesToProcess = filesToProcess.concat(findMarkdownFiles(filePath));
} else if (path.extname(filePath) === '.md') {
filesToProcess.push(filePath);
}
}
return filesToProcess;
}
// 處理.md文件中的代碼塊
function processMarkdownFile(filePath) {
const content = fs.readFileSync(filePath, 'utf8');
// 使用正則表達式匹配所有代碼塊
const newContent = content.replace(/```[\s\S]*?```/g, (match) => {
// 替換代碼塊中的反轉義字符
return match.replace(/\\\[/g, '[')
.replace(/\\\]/g, ']')
.replace(/\\\*/g, '*')
.replace(/\\_/g, '_')
.replace(/\\\\/g, '\\')
});
// 覆蓋原文件
fs.writeFileSync(filePath, newContent, 'utf8');
}
// 主函數
function main() {
const markdownFiles = findMarkdownFiles(DIRECTORY_PATH);
for (const filePath of markdownFiles) {
processMarkdownFile(filePath);
}
console.log('所有.md文件中的代碼塊已處理完畢。');
}
// 運行主函數
main();
警告
- 請修改
DIRECTORY_PATH
的值,並注意轉義。 - 這個腳本會覆蓋原文件,請提前備份。而且,由於其做了批量替換,可能會影響代碼正常運行。需要手工檢查。
移除不必要的 TOC(文章目錄)
很多 Hugo 主題使用 toc
這個 Front Matter 來標記是否展開文章目錄。對於老文章,
我寫了一個腳本,可以根據這個文章是否有實際的二級標題來判斷是否啟用 toc。
也就是說,如果一個文章的 Markdown 裡沒有兩個以上的二級標題,它會把:
+++
title = '本站從 Wordpress 遷移到 Hugo'
date = 2024-04-01T16:38:34+08:00
draft = false
+++
修改為
+++
title = '本站從 Wordpress 遷移到 Hugo'
date = 2024-04-01T16:38:34+08:00
draft = false
toc = false
+++
這樣渲染出的文件就不會有目錄了,並且不需要前端使用 Javascript 動態實現。
自動翻譯簡體中文到繁體中文(或相反)
因為這次升級的一個目的是更好地支持簡繁兩種漢字的使用者,因此我使用了一個腳本, 它可以將所有 Markdown 自動進行簡繁轉換。
Hugo
支持兩種多語言模式,我使用的是文件模式,也就是簡體、繁體的文章放在一起,
繁體文章增加 .zh-hant
後綴名。
簡繁轉換不能簡單的字字對應,因為存在很多「一對多」的問題和各地習慣用語的問題。
Opencc
是一個不錯的開源工具,可以幫我們實現以詞彙為單位的簡繁轉換,提高轉換的準確率。
根據統計,本站大部分繁體用戶來自臺灣,因此我在簡體轉換繁體時,使用了臺灣的慣用詞。
下面的腳本使用 opencc
的 NodeJS
版本進行簡繁轉換。
這個腳本會把轉換後的結果放在 DIRECTORY_OUTPATH
目錄下,文件名增加 .zh-hant
後綴。
它只轉換文章的標題和正文,不會轉換文章的元數據,例如 tag
和 url
,因此比較安全。
使用前,你需要根據自己的情況對腳本進行適當的修改,並且需要安裝 opencc
:
npm i opencc
你需要提前修改腳本中的 DIRECTORY_PATH
和 DIRECTORY_OUTPATH
。
如果你使用 posts/xxx/index.md
、posts/xxx/index.zh-hant.md
這種形式,
你可以將兩個目錄設置相同。
腳本內容如下:
const fs = require("fs");
const path = require("path");
const OpenCC = require("opencc");
const converter = new OpenCC("s2twp.json");
const matter = require('gray-matter');
// 指定目錄路徑
const DIRECTORY_PATH = "path/to/blog/content";
const DIRECTORY_OUTPATH = "path/to/blog/content-hant";
// 遞歸地查找.md文件
function findMarkdownFiles(dirPath) {
let filesToProcess = [];
const files = fs.readdirSync(dirPath);
for (const file of files) {
const filePath = path.join(dirPath, file);
const stat = fs.statSync(filePath);
if (stat.isDirectory()) {
filesToProcess = filesToProcess.concat(findMarkdownFiles(filePath));
} else if (path.extname(filePath) === ".md") {
filesToProcess.push(filePath);
}
}
return filesToProcess;
}
// 處理.md文件中的代碼塊
async function processMarkdownFile(filePath) {
if (filePath.indexOf("zh-tw") >= 0) {
return;
}
// 解析Markdown文件的front matter
const content = matter(fs.readFileSync(filePath, "utf8"));
// 翻譯正文
content.content = await converter.convertPromise(content.content)
// 翻譯標題
if (content.data.title && content.data.title.length > 0) {
content.data.title = await converter.convertPromise(content.data.title)
}
// 構造輸出文件路徑
const outFilePath = path.join(DIRECTORY_OUTPATH, path.basename(filePath, ".md") + ".zh-hant.md");
console.log(outFilePath)
console.log(content.content)
// 如果文件存在則不覆蓋
if (fs.existsSync(outFilePath)) {
return;
}
const newMarkdownContent = matter.stringify(content);
fs.writeFileSync(outFilePath, newMarkdownContent)
}
// 主函數
function main() {
const markdownFiles = findMarkdownFiles(DIRECTORY_PATH);
for (const filePath of markdownFiles) {
processMarkdownFile(filePath);
}
console.log("所有.md文件已處理完畢。");
}
// 運行主函數
main();
警告
- 不要使用
zh-tw
、zh-hk
、zh-cn
等作為語言標識。因為他們代表的是地區。作為一個小站點,你估計不太可能會為香港、臺灣、新加坡各做一個不同的站點。 - 使用
zh-hant
代表傳統漢字,zh-hans
代表簡體中文是明顯更加通用的選擇。這也方便瀏覽器、搜索引擎判斷你的目標用戶。比如,你使用了zh-hant
,香港訪問者不會被排除在外。 - 你可以訪問
opencc
的 gihhub 頁面,查找不同的配置文件。不同的配置文件會使用不同的慣用詞,請根據你實際的需求進行修改。我使用的是s2tw.json
文件,代表使用臺灣慣用詞進行簡體到繁體的轉換。
你也可以通過修改 s2twp.json
配置文件的名字實現繁體->簡體的轉換。
手工檢查所有文章
雖然做了自動化處理,但很多文章依然有問題,需要手工檢查一遍。
選擇合適的評論系統
我選擇的是 Waline
,但你可以選擇任何你喜歡的評論系統。唯一的建議是,
評論數據需要掌握在自己手裡,因此必須選擇可以備份評論數據的評論系統。
否則,一旦你的託管方式發生變化,你可能會追悔莫及。
Waline
支持多種數據源,我選擇的是 sqlite
,因為我有
自己的服務器,並且我希望一個節約內存的輕量化解決方案。
從 Wordpress 評論遷移到 Waline
目前網絡上似乎還沒有從 Wordpress 評論遷移到 Waline 的現成方案。 本文提供了一種方案,可以將 Wordpress 所有評論無損地遷移到 Waline。
首先從 Wordpress 後臺,導出所有內容的 xml 文件。
然後執行我寫的腳本(你需要有 NodeJS),對 xml 文件進行分析,生成可以導入
到 Waline 的導入格式(注意修改 WORDPRESS_XML_FILE
變量):
const fs = require('fs');
const xml2js = require('xml2js');
const parser = new xml2js.Parser();
// WordPress備份文件的路徑
const WORDPRESS_XML_FILE = 'WordPress.2024-03-28.xml';
// 讀取XML文件
fs.readFile(WORDPRESS_XML_FILE, (err, data) => {
if (err) {
console.error('Error reading XML file:', err);
return;
}
// 解析XML數據
parser.parseString(data, (err, result) => {
if (err) {
console.error('Error parsing XML data:', err);
return;
}
// 初始化評論數組
let commentsArray = [];
// 遍歷每個item(文章或頁面)
result.rss.channel[0].item.forEach((post) => {
// 判斷是否有評論
if (post['wp:comment']) {
// 遍歷每條評論
post['wp:comment'].forEach((comment) => {
if (comment['wp:comment_approved'] + '' == "1" && comment['wp:comment_type'] != 'pingback') {
let commentObj = {
user_id: null,
comment: comment['wp:comment_content'] ? comment['wp:comment_content'][0] : '',
insertedAt: comment['wp:comment_date'] ? comment['wp:comment_date'][0] : '',
ip: comment['wp:comment_author_IP'] ? comment['wp:comment_author_IP'][0] : '',
link: comment['wp:comment_author_url'] ? comment['wp:comment_author_url'][0] : '',
mail: comment['wp:comment_author_email'] ? comment['wp:comment_author_email'][0] : '',
nick: comment['wp:comment_author'] ? comment['wp:comment_author'][0] : '',
rid: comment['wp:comment_parent'] && comment['wp:comment_parent'][0] !== '0' ? parseInt(comment['wp:comment_parent'][0]) : null,
pid: null,
sticky: null,
status: 'approved',
like: null,
ua: comment['wp:comment_agent'] ? comment['wp:comment_agent'][0] : '',
url: (post['link'] ? post['link'][0] : '').replace("https://ceeji.net", ""),
createdAt: comment['wp:comment_date'] ? comment['wp:comment_date'][0] : '',
updatedAt: comment['wp:comment_date'] ? comment['wp:comment_date'][0] : '',
objectId: parseInt(comment['wp:comment_id'][0])
};
commentsArray.push(commentObj);
} else {
console.log("跳過沒批准的評論")
}
});
}
});
// 將評論數組轉換為JSON並保存到文件
const jsonContent = JSON.stringify(commentsArray, null, 2);
fs.writeFile('comments.json', jsonContent, 'utf8', (err) => {
if (err) {
console.error('Error writing JSON file:', err);
} else {
console.log('Comments have been exported to comments.json');
}
});
});
});
最後,在 Waline 的管理後臺,選擇導入導出功能:
先選擇導出,打開導出後的 JSON 文件,格式類似如下:
{
"__version": "1.31.13",
"type": "waline",
"version": 1,
"time": 1711963109933,
"tables": [
"Comment",
"Counter",
"Users"
],
"data": {
"Comment": [] // 替換此處的數據
}
}
把 Comment 處的內容替換為腳本生成的內容,即可進行導入。注意,導入會丟失所有現有數據, 如原來有數據的,需要手工合併或提前備份。
實現代碼高亮的黑暗模式自適應
Hugo
自帶的代碼高亮功能,一般只能設置一個樣式。如果你的主題支持自適應黑暗模式,可能會導致
在切換黑暗模式開關的時候,代碼高亮不能跟著切換。
最新版本的 Hugo 使用 chroma
進行語法高亮。
首先,確定你想要使用的樣式。比如我們在亮色模式下使用 monokailight
樣式,在暗色模式下使用 onedark
樣式。
接下來,創建對應的 .css
文件供 Hugo 使用:
hugo gen chromastyles --style=monokailight > syntax_light.css
hugo gen chromastyles --style=onedark > syntax_dark.css
將這兩個文件移動到你的主題 assets
文件夾中(例如 ./themes/paper/assets/
)。
接下來,確保我們的 Hugo 主題加載這些樣式。
為此,編輯 ./themes/paper/layout/partials/head.html
(或者負責在你的主題中添加頭部部分的文件),添加以下內容:
{{ $syntax_dark_css := resources.Get "syntax_dark.css" | minify }}
{{ $syntax_light_css := resources.Get "syntax_light.css" | minify }}
<link rel="preload stylesheet" as="style" href="{{ $syntax_dark_css.Permalink }}" />
<link rel="preload stylesheet" as="style" href="{{ $syntax_light_css.Permalink }}" />
這將在使用 Hugo 構建站點時使 .css
文件可用,並允許頁面加載它們。
順序很重要:默認情況下,應用最後引用的文件中的樣式,因為它會覆蓋之前的樣式!
為了讓 Hugo 使用我們的 .css
文件作為 chroma
樣式,我們需要在 Hugo 的 hugo.toml
文件中明確指定以下內容:
[markup.highlight]
noClasses = false
確保一切正常後,檢查你的代碼是否已使用設置的樣式進行高亮顯示。當你切換到暗色模式時,目前應該還不會有任何變化。
然後你需要寫動態切換應用於代碼片段的 .css
文件的腳本。
在 ./themes/papers/partials/header.html
(或者負責在你的 Hugo 主題中切換模式的文件)中,找到切換模式的代碼。
每個主題都不一樣,你需要自己去找。
例如它可能類似這樣:
const setDark = (isDark) => {
metaTheme.setAttribute('content', isDark ? '#000' : lightBg);
htmlClass[isDark ? 'add' : 'remove']('dark');
localStorage.setItem('dark', isDark);
};
我們在這個函數調用中添加一個方法:
const setDark = (isDark) => {
metaTheme.setAttribute('content', isDark ? '#000' : lightBg);
htmlClass[isDark ? 'add' : 'remove']('dark');
localStorage.setItem('dark', isDark);
setSyntaxDark(isDark); // 添加的方法
};
定義 setSyntaxDark
函數和一個額外的輔助函數:
function getStyleSheet(file_name) {
for (var i = 0; i < document.styleSheets.length; i++) {
var sheet = document.styleSheets[i];
if (sheet.href.includes(file_name)) {
return sheet;
}
}
}
function setSyntaxDark(isDark) {
let sheet_light = getStyleSheet("syntax_light")
let sheet_dark = getStyleSheet("syntax_dark")
sheet_light.disabled = isDark ? true : false
sheet_dark.disabled = isDark ? false : true
}
隨後,使用 Hugo 重新構建你的網站,確保一切正常。
簡繁中文使用相同的評論數據,並自動翻譯
簡繁語言本就沒什麼大差距,不需要分開不同的評論。在 Waline 中, 你可以通過移除 url 中的語言部分使得不同語言版本共享同一個評論。
修改你的 waline
初始化代碼:
init({
el: "#xxx",
serverURL: "xxx",
locale: locale['{{ .Page.Lang }}'], // 注意這一行
dark: 'html.dark', // 可實現黑暗模式自適應
// path 中使用正則移除簡繁部分的網址
path: window.location.pathname.replace(/zh-(hans|hant)\//, '')
});
同時我們需要定義一個 locale
變量實現 waline
的本地化,例如:
const locale = {
"zh-hans": {
nick: '暱称',
nickError: '暱称不能少于3个字符',
mail: '邮箱',
mailError: '请填写正确的邮件地址',
link: '网址',
optional: '可选',
placeholder: '欢迎评论',
sofa: '来发评论吧~',
submit: '提交',
like: '喜欢',
cancelLike: '取消喜欢',
reply: '回复',
cancelReply: '取消回复',
comment: '评论',
refresh: '刷新',
more: '加载更多...',
preview: '预览',
emoji: '表情',
uploadImage: '上传图片',
seconds: '秒前',
minutes: '分钟前',
hours: '小时前',
days: '天前',
now: '刚刚',
uploading: '正在上传',
login: '登录',
logout: '退出',
admin: '博主',
sticky: '置顶',
word: '字',
wordHint: '评论字数应在 $0 到 $1 字之间!\n当前字数:$2',
anonymous: '匿名',
level0: '潜水',
level1: '冒泡',
level2: '吐槽',
level3: '活跃',
level4: '话痨',
level5: '传说',
gif: '表情包',
gifSearchPlaceholder: '搜索表情包',
profile: '个人资料',
approved: '通过',
waiting: '待审核',
spam: '垃圾',
unsticky: '取消置顶',
oldest: '按倒序',
latest: '按正序',
hottest: '按热度',
reactionTitle: '你认为这篇文章怎幺样?'
},
"zh-hant": {
nick: '暱稱',
nickError: '暱稱不能少於3個字元',
mail: '郵箱',
mailError: '請填寫正確的郵件地址',
link: '網址',
optional: '可選',
placeholder: '歡迎評論',
sofa: '來發評論吧~',
submit: '提交',
like: '喜歡',
cancelLike: '取消喜歡',
reply: '回覆',
cancelReply: '取消回覆',
comment: '評論',
refresh: '重新整理',
more: '載入更多...',
preview: '預覽',
emoji: '表情',
uploadImage: '上傳圖片',
seconds: '秒前',
minutes: '分鐘前',
hours: '小時前',
days: '天前',
now: '剛剛',
uploading: '正在上傳',
login: '登入',
logout: '退出',
admin: '博主',
sticky: '置頂',
word: '字',
wordHint: '評論字數應在 $0 到 $1 字之間!\n當前字數:$2',
anonymous: '匿名',
level0: '潛水',
level1: '冒泡',
level2: '吐槽',
level3: '活躍',
level4: '話癆',
level5: '傳說',
gif: '表情包',
gifSearchPlaceholder: '搜尋表情包',
profile: '個人資料',
approved: '透過',
waiting: '待稽核',
spam: '垃圾',
unsticky: '取消置頂',
oldest: '按倒序',
latest: '按正序',
hottest: '按熱度',
reactionTitle: '你認為這篇文章怎麼樣?'
}
}
重定向 Feed(Rss/Atom)訂閱地址
我的網站雖然是個小站,但依然有些人在訂閱。為了避免他們走丟, 我需要能夠保持原有的 Feed 訂閱地址。如果你使用的 OSS 或其他 無服務器(Serverless)的方式進行託管,你可能需要在你的 CDN 頁面使用 rewrite 來實現。
因為我是有服務器的,我使用 nginx
來實現了這一點。
Hugo 默認將 RSS/Atom 發佈在根目錄下的 index.xml
或類似的文件中。
找到這個文件的地址,然後你可以進行重定向:
location ~ ^/blog/feed/?$ {
rewrite ^ /index.xml last;
}
上面的這段 nginx
配置可以把 /blog/feed 和 /blog/feed/ 指向 index.xml
,
同時保持對外訪問地址不變。
提示
一個小技巧:在你的 nginx
訪問日誌中搜索 feed,你可能可以看到有多少人訂閱了你的網站。
除了解決 Feed 網址問題外,如果你和我一樣通過 nginx 部署,建議你還要做兩件事:
- 遷移早期定期訪問 access log,查看是否有舊網址出現了 404 錯誤,這不利於 SEO;
- 在 nginx 配置文件裡,設置正確的 404 頁面。
圖片管理
強力建議你採用下列的文件結構來存放圖片:
posts /
article /
index.md
1.jpg
這種方式管理方便,而且在 Markdown 編輯器中不會出現圖片看不到的問題。
然後,你可以通過 render-image
hook 實現圖片的 CDN 化,提升全球訪問速度。
具體可進行相關搜索。
© 轉載需附帶本文連結,依 CC BY-NC-SA 4.0 釋出。