用 Gatsby 框架开发博客是一件颇为惬意的事情,可以很轻易的使用 React 社区的很多资源,定制化自由度颇高。最近想要给博客文章添加最近更新时间戳功能,但这个功能 Gatsby 框架和插件并不能直接提供(issue),基于 Git 仓库本地 Markdown 文件的博客都会面临这个问题,下面是我的解决方案,思路也适用于类似的 static site generator.
如果你是用的 contenful 这类 CMS 平台就简单了,可以找下平台 API 文档,一般可以直接调用。
方案一
给每个文档 md/mdx 添加一个新的字段 - updatedAt
,每次改完 md 文件手动更新这个字段。
例如:
md1---2title: 给 Gataby 文件添加修改时间3author: devrsi0n4date: 2020-03-205updatedAt: 2020-03-216excerpt: 自动生成指定文件的修改时间戳,添加到 Gatsby Node7hero: ./images/hero.jpg8---
这个方案成本较高,增加了写文档的隐形成本,而且也很容易忘记。显然这个方案不够优雅。
方案二
给 Gatsby Node 增加 updatedAt
字段,每次 GraphQL 查询时自动获取这个字段。现在问题变成了怎么给 Gatsby Node 新增字段,以及怎么获取每个文档的最近更新时间。第一个问题好解决,翻一翻 Gatsby 文档就能轻松找到,第二个问题比较麻烦。
mtime
首先尝试直接读取文件 mtime
,这个 Gatsby 文件标准字段,获取文件的最近更新时间。
但这个方案有很大的弊端,每次重新 git clone
之后所有文件最近更新时间 mtime
都变成克隆的时刻,这是 git 的一个 bug,参考stackoverflow 链接。如果你可以保证你只在同一个本地仓库写文章,那这个方案或许也能用。
考虑总有意外情况,比如:换电脑,甚至电脑丢了,抑或文章接收其他读者修改的 Pull Request,这个方案也不 OK。
Git log
既然 git 本身不会把最近更新时间写到文件,那直接从 git log
读取指定文件的最近 commit 时间也是个办法。参考命令如下:
bash1# the committer time of the last commit2git log -1 --pretty=format:%cI -- package.json3# the committer time of the most recent commit4git log --pretty=format:%cI -- package.json | sort | tail -n 1
但这个方案也有个问题,在 zeit,netlify 这类自动构建部署平台,为了提高构建速度,构建过程并不会完整克隆整个仓库,而只拉取最新 commit 代码。
Gatsby 插件 gatsby-transformer-gitinfo 也是做类似事情,只是并没有解决构建时的问题。
最终方案
最终方案是解决平台构建时因不会 git clone
导致不能获取文件的最近更新时间。方法很简单,每次 git staged 时把 md/mdx
文件最近更新时间写入 JSON 文件,并提交 git,最后 Gatsby 构建时从 JSON 读取最近更新时间。
生成文件最近更新时间 JSON
这里没有用 git log
提取时间,因为 git staged 时还没有 commit,参考脚本如下(代码位于本仓库 ./tasks/update-post-timestamps.js):
update-post-timestamps.js
js1const moment = require('moment-timezone');2const path = require('path');3const fs = require('fs').promises;4const { promisify } = require('util');5const exec = promisify(require('child_process').exec);6const postTimestamps = require('../src/gatsby/node/postTimestamps.json');78/**9 * This function called at git staged10 */11(async function() {12 // Show staged files only13 const { stdout: gitDiff } = await exec('git diff --staged --name-only');14 const stagedFiles = gitDiff.matchAll(/content\/[\S]+\/[\S]+.mdx?/gm);15 if (stagedFiles.length === 0) {16 // MDX files are not modified17 return;18 }1920 const statPromises = [];21 for (const [file] of stagedFiles) {22 statPromises.push(fs.stat(file).then(stat => ({ stat, file })));23 }24 const statList = await Promise.all(statPromises);25 for (const { stat, file } of statList) {26 const updatedAt = moment.tz(stat.mtime, 'Asia/Shanghai').format();27 postTimestamps[file] = {28 updatedAt,29 };30 }3132 const targetFilePath = path.resolve(33 __dirname,34 '../src/gatsby/node/postTimestamps.json'35 );36 await fs.writeFile(targetFilePath, JSON.stringify(postTimestamps, null, 2));37 await exec(`git add ${targetFilePath}`);38})();
⚠️ 注意 这段代码应该在 git staged
时执行,避免代码重新克隆之后所有文件的最近更新时间被覆写的 Bug。JS 添加 git hook 也很简单,参考如下。
安装依赖, npm i -S husky lint-staged
,package.json 添加配置如下:
package.json
json1{2 //...3 "husky": {4 "hooks": {5 // git commit 之前执行 lint-staged6 "pre-commit": "lint-staged"7 }8 },9 "lint-staged": {10 // lint-staged 匹配到 mdx/md 文件执行该命令11 "**/*.{mdx,md}": ["node ./tasks/update-post-timestamps.js"]12 }13}
Gatsby 节点添加 updatedAt 字段
平台构建时,从 JSON 读取最近更新时间,在 gatsby-node.js
添加 Gataby Node Field 即可。
gatsby-node.js
js1const postTimestamps = require('./postTimestamps.json');23module.exports.onCreateNode = async function onCreateNode({ node, actions }) {4 const { createNodeField } = actions;56 if (node.internal.type !== `File`) {7 return;8 }9 const relativePath = path.relative(process.cwd(), node.fileAbsolutePath);10 const timestamp = postTimestamps[relativePath];11 createNodeField({12 node,13 name: `updatedAt`,14 // 如果没有找到给一个不可能的值,避免冗余的判断15 value:16 timestamp17 ? timestamp.updatedAt18 : '1992-10-15T10:53:18+08:00'19 });20}
至此,自动生成文章最近更新时间的功能完成,希望能帮到遇到类似问题的读者。