在 Hexo 部署時自動更新文章的編輯時間
前言
昨天修改了文章後,發現更新時間沒有跟著改變,查了一下似乎只能自行更改 front-matter (markdown 最上面那一區)裡的 updated
。
但是每次改一篇文章都要手動修改感覺有點蠢,於是我繼續尋找有沒有辦法自動化,最後找到了使用 JavaScript 的方法。以下程式碼修改自原文章,順便做個筆記。
為現有文章加上 updated
若文章 front-matter 還沒有 updated
,需要手動加入,可以直接複製 date: xxxx-xx-xx xx:xx:xx
這行,然後把 date
改成 updated
就好(之後執行程式會自動修正)。
---
title: 在 Hexo 部署時自動更新文章的編輯時間
date: 2023-01-18 15:09:05
updated: 2023-01-20 21:33:18
categories:
- 架站筆記
tags:
- Hexo
- JavaScript
---
修改模板
我們當然不會想要每次都手動加上 updated
,因此需要修改模版的 front-matter:
---
title: {{ title }}
date: {{ date }}
updated: {{ date }}
categories:
-
tags:
-
---
這樣以後生成新文章就會自動加上 updated
了。
自動修改更新日期
在根目錄建立 UpdateFileTime.js
(若放在 source/
裡,deploy 時會生成在網站裡):
#!/usr/bin/env node
// 自動更新文章的修改時間
console.log('開始執行');
var fs = require("fs"); // 用於讀寫文件
var RegExp = /(updated:\s*)((\d{2}(([02468][048])|([13579][26]))[\-\/\s]?((((0?[13578])|(1[02]))[\-\/\s]?((0?[1-9])|([1-2][0-9])|(3[01])))|(((0?[469])|(11))[\-\/\s]?((0?[1-9])|([1-2][0-9])|(30)))|(0?2[\-\/\s]?((0?[1-9])|([1-2][0-9])))))|(\d{2}(([02468][1235679])|([13579][01345789]))[\-\/\s]?((((0?[13578])|(1[02]))[\-\/\s]?((0?[1-9])|([1-2][0-9])|(3[01])))|(((0?[469])|(11))[\-\/\s]?((0?[1-9])|([1-2][0-9])|(30)))|(0?2[\-\/\s]?((0?[1-9])|(1[0-9])|(2[0-8]))))))(\s((([0-1][0-9])|(2?[0-3]))\:([0-5]?[0-9])((\s)|(\:([0-5]?[0-9])))))/;
let toppath = "./source/_posts/";
function fn(path) {
fs.readdir(path, (err, files) => {
if (err) return console.log(err);
files.forEach(function (item) {
fs.stat(path + item, (err, data) => {
if (err) return console.log(err);
if (data.isFile()) {
if (item.indexOf(".md") > -1) {
writeFileTime(path + item, fs);
}
} else {
fn(path + item + '/');
}
})
})
})
}
fn(toppath);
function writeFileTime(file, fs) {
fs.readFile(file, 'utf8', function (err, data) { // 讀取文件內容
if (err) return console.log("讀取檔案內容錯誤:", err);
if (RegExp.test(data)) { // 尋找 updated 字串
fs.stat(file, function (err, stats) { // 讀取文件建立時間等資訊
if (err) return console.log("讀取檔案資料錯誤:", err);
var updateds = data.match(RegExp);
var updated = updateds[0].replace("updated: ", "").replace(/-/g, "/"); // 時間格式化為 xxxx/xx/xx xx:xx:xx
if (new Date(stats.mtime).getTime() - new Date(Date.parse(updated)) > 1000 * 60 * 10) { // 只要修改時間和文章內 updated 時間差大於 10 分鍾就觸發更新
var result = data.replace(RegExp, "updated: " + getFormatDate(stats.mtime)); // 替換更新時間
console.log(result);
fs.writeFile(file, result, 'utf8', function(err) { // 寫入新的檔案內容
if (err) return console.log(err);
fs.utimes(file, new Date(stats.atime), new Date(stats.mtime), function (err) { // 還原訪問時間和修改時間
if (err) return console.log("修改時間失敗:", err);
console.log(file, "成功更新時間");
});
});
}
});
}
});
}
/*
timeStr:時間,格式可為:"September 16,2016 14:15:05、
"September 16,2016"、"2016/09/16 14:15:05"、"2016/09/16"、
'2014-04-23T18:55:49'和毫秒
dateSeparator:年、月、日之間的分隔符,預設為"-",
timeSeparator:時、分、秒之間的分隔符,預設為":"
*/
function getFormatDate(timeStr, dateSeparator, timeSeparator) {
dateSeparator = dateSeparator ? dateSeparator : "-";
timeSeparator = timeSeparator ? timeSeparator : ":";
var date = new Date(timeStr),
year = date.getFullYear(), // 四位數
month = date.getMonth(), // 0-11
day = date.getDate(), // 1-31
hour = date.getHours(), // 0-23
minute = date.getMinutes(), // 0-59
seconds = date.getSeconds(), // 0-59
Y = year + dateSeparator,
M = ((month + 1) > 9 ? (month + 1) : ('0' + (month + 1))) + dateSeparator,
D = (day > 9 ? day : ('0' + day)) + ' ',
h = (hour > 9 ? hour : ('0' + hour)) + timeSeparator,
m = (minute > 9 ? minute : ('0' + minute)) + timeSeparator,
s = (seconds > 9 ? seconds : ('0' + seconds)),
formatDate = Y + M + D + h + m + s;
return formatDate;
}
UpdateFileTime.js 解析
在這裡做個筆記,避免之後忘了這段程式在做什麼。
-
Line 5:
fs
(file system) module 用於讀寫文件。 -
Line 7:
updated
與時間格式的正則表達式 (regular expression),型別是RegExp
。- 若後面加上 g (global) 變成
/.../g
,會搜尋所有符合的結果,否則只會搜尋第一個符合的,可以避免文章內的updated
也被替換。
- 若後面加上 g (global) 變成
-
Line 9 ~ 26: 只尋找
source/_post/
內的檔案,只選取.md
檔,其他檔案跳過,若是目錄則繼續遞迴。選中的檔案進行時間更新。- Line 11:
fs.readdir(path[, options], callback)
讀取資料夾內所有檔案,callback 是參數為err, data
的 function。
- Line 11:
-
Line 30: 需使用 UTF-8 打開,否則可能會編碼錯誤。
-
Line 32 & 35:
RegExp.test(str)
可以檢查str
內是否有符合RegExp
的部分。str.match(RegExp)
則會回傳一個包含所有符合結果的陣列。 -
Line 33:
fs.stat(path[, options], callback)
會回傳一個fs.Stats
物件,結構如下:Stats { dev: 16777220, mode: 33188, nlink: 1, uid: 501, gid: 20, rdev: 0, blksize: 4096, ino: 14214074, size: 8, blocks: 8, atimeMs: 1561174616618.8555, mtimeMs: 1561174614584, ctimeMs: 1561174614583.8145, birthtimeMs: 1561174007710.7478, atime: 2019-06-22T03:36:56.619Z, mtime: 2019-06-22T03:36:54.584Z, ctime: 2019-06-22T03:36:54.584Z, birthtime: 2019-06-22T03:26:47.711Z }
-
Line 40: 由於執行這個程式會更動到檔案,因此使用
fs.utimes()
將存取與編輯時間還原成執行前的狀態。
我在寫這段的時候遇到一個笨問題,不管怎麼執行,文章的 updated
都沒有更新,一開始以為是出 bug,上網找了各種方法都沒用。
後來發現 VSCode 不會即時同步檔案(我一直以為會),把檔案關掉重開就是正確的結果了。
執行檔案
在根目錄執行一次這個檔案並查看效果:
node .\source\UpdateFileTime.js
在遠端 pull 更新正確時間
使用這段的腳本前提是原始檔案 (至少包含 source/_post/
)已經被 git 追蹤,否則不會有效果。
若我們在其他電腦將原始檔案 pull 下來,檔案的最後編輯時間會變成 pull 的時間,會導致執行 UpdateFileTime.js
時全部變成當天的日期,因此在 pull 之後可以執行下面的腳本,將最後編輯時間復原為最後一次 commit 的時間(若檔案沒有被 git 追蹤則不變)。
git ls-files -z | while read path;
do
touch -d "$(git log -1 --format="@%ct" "$path")" "$path";
done;
Windows 預設的換行格式是 CRLF (\r\n
),請確保將此檔案的換行格式轉為 LF (\n
),否則執行時會出現錯誤。
解析:
-
git ls-files
: 顯示當前目錄中所有被 git 追蹤的檔案:$ git ls-files aaa.md bbb.md ccc.md
-
|
: pipe,將左邊的輸出作為右邊的輸入。由於 pipe 裡的每個指令都會在獨立的 subshell 執行,因此在 pipe 裡產生的變數不會影響到外部。
-
read
: 讀入資料,分割並分配給後面的變數。$ read var1 var2 <<< "Hello world!" $ echo $var1; echo var2 Hello world!
-
while read path
: 重複「讀取直到換行後傳給變數path
」到輸入結束為止。do
、done
分別為迴圈的開頭與結束。
-
touch
: 用於更改檔案或目錄的時間戳記,或是建立空檔案。-d
: 設定日期與時間,其中一種格式為,使用 Unix timestamp 前面要加上@
。
-
git log
: 顯示 commit 紀錄。-<n>
: 顯示最後 n 筆 commit。
$ git log -1 commit 8cf50d13ccb7983e8c406573bf834fa9394418aa (HEAD -> master) Author: JHTNT <xxxxx@gmail.com> Date: Wed Oct 12 16:25:10 2022 +0800 Draft: temp
--format
: 可以自訂輸出的格式,例如:%H
,%h
: 顯示完整 / 簡短的 commit hash 值。%an
: 作者的名字。%cn
: 提交者的名字。%ct
: 以 UNIX timestamp 形式顯示 commit 時間。%cs
: 以YYYY-MM-DD
形式顯示 commit 時間。
一鍵部署
我們可以將 UpdateFileTime.js
與 Hexo 部署的指令寫成一個腳本,方便之後直接一鍵部署,並同時將原始檔案 push 到 github,關於儲存原始檔案可以查看這篇。
call node .\UpdateFileTime.js
call git add .
call git commit -m %1
call git push
call hexo clean
call hexo generate
call hexo deploy
之後要部署只需要在根目錄執行 .\deploy.bat "[commit message]"
指令即可(記得加上引號),例如:
.\deploy.bat "add: commit test"
雖然最後編輯時間會與 commit 時間有所差別,但目前沒找到解決辦法,且我覺得不會造成太大的問題,因此就暫時保持這樣。
Reference
- hexo 自动更新文章修改时间 | Anubis的小窝
- hexo 自动更新文章修改时间 - 知乎
- The update time of the article is incorrect · Issue #893 · theme-next/hexo-theme-next
- Git - git-ls-files Documentation
- bash - Left side of pipe is the subshell? - Stack Overflow
- How To Use The Bash read Command {10 Examples}
- delimiter - What does the bash read -d ‘’ do? - Stack Overflow
- Linux touch命令 | 菜鸟教程
- Git - git-log Documentation