项目背景
这是上一篇博客使用 Pandoc 生成静态博客的后续, 经过一天的研究和测试,将自动化脚本完成了。 这里记录下成果。
npm run build
{
"scripts": {
"build": "node scripts/build.js"
}
}
在项目中新建了一个 scripts 文件夹,专门存放脚本。 在 package.json 中, 给 build.js 绑定命令,这样可以使用约定俗成的 npm run build 生成静态 html。
更加深入 Pandoc
为了配合自动化,需要继续研究下 Pandoc。
首先,对博客文章的 md 文件,使用以下 frontmatter。
---
title: Pandoc 生成静态博客的自动化脚本
date: 2023-05-11
---
这样通过 gray-matter 这个库,可以方便地提取 title 和 date,在脚本中使用。
其次,在首页中,我想修改下 ul 列表的样式,使其有一个稍微不一样的外观。
:::::{#index}
## 2023
- May 13 [修改 liveServer 的根目录](/blog/2023/live-server-settings)
:::::
在 index.md 中使用上面这样将内容包裹在一组多个英文冒号中的写法, 可以在转换的 html 中定义一个 id 为 index 的 div。
<div id="index">
<section id="section" class="level2">
<h2>2023</h2>
<ul>
<li>---</li>
</ul>
</section>
</div>
然后在 css 中增加下首页样式,去掉列表前的圆点,并给超链接一个左边距。
#index {
@apply space-y-10;
}
#index ul {
@apply list-none;
}
#index li a {
@apply ml-4;
}
最后,为了简化命令操作, 在 Pandoc 中可以通过-d 参数指定一个本地的 yaml 文件, 保存默认配置, 这样就不需要每个命令中重复书写很长一串参数,命令可以简化为:
pandoc -d yaml文件 输入文件 -o 输出文件
本地配置文件如下:
section-divs: true
css: main.css
toc: true
variables:
toc-title: Table of Contents
standalone: true
from: markdown+east_asian_line_breaks
include-after-body: src/sources/footer.html
shelljs
使用shelljs这个库,可以很方便在脚本中执行 shell 命令,使用起来特别简单。
shell.exec('git commit -am "Auto-commit"');
自动化脚本
先上完整代码,如果你想尝试下类似的玩法,可以直接拿去用:
const shell = require('shelljs');
const path = require('path');
const fs = require('fs');
const root = process.cwd();
const yaml = path.join(root, 'src/sources/config.yaml');
const matter = require('gray-matter');
const dirs = {
index: [path.join(root, 'src/index.md'), path.join(root, 'dist/index.html')],
about: [path.join(root, 'src/about.md'), path.join(root, 'dist/about.html')]
};
const commands = {
index: `pandoc -d "${yaml}" "${dirs.index[0]}" -o "${dirs.index[1]}"`,
about: `pandoc -d "${yaml}" "${dirs.about[0]}" -o "${dirs.about[1]}"`
};
// 生成全部博客
const blogDir = path.join(root, 'src', 'blog');
const blogFiles = [];
function traverseDirectory(dir) {
fs.readdirSync(dir).forEach((file) => {
const fullPath = path.join(dir, file);
if (fs.statSync(fullPath).isDirectory()) {
traverseDirectory(fullPath);
} else {
blogFiles.push(fullPath);
}
});
}
traverseDirectory(blogDir);
let hasNewBlog = false;
const rows = {};
for (const input of blogFiles) {
const fileData = path.parse(input);
if (fileData.ext !== '.md') continue;
const { data } = matter.read(input);
if (!data.date) {
console.log('md文档中缺少date元信息', input);
continue;
}
if (!data.title) {
console.log('md文档中缺少title元信息', input);
continue;
}
const blogFile = `${data.date.toISOString().slice(0, 10)}-${fileData.name}.html`;
const output = path.join(root, 'dist', blogFile);
const stat = fs.statSync(input);
let blogMtimeMs = 0;
if (fs.existsSync(output)) blogMtimeMs = fs.statSync(output).mtimeMs;
if (stat.mtimeMs > blogMtimeMs) {
shell.exec(`pandoc -d "${yaml}" "${input}" -o "${output}"`);
console.log('已生成:', output);
hasNewBlog = true;
}
//生成首页列表
const month = data.date.toLocaleString('en-GB', { month: 'short' });
const day = data.date.getDate();
const year = data.date.getFullYear();
const tag = `- ${month + ' ' + day} [${data.title}](/${blogFile})`;
if (!rows[year]) rows[year] = [];
rows[year].push([data.date, tag]);
}
const keys = Object.keys(rows);
keys.sort((a, b) => b - a);
const strs = [];
for (const key of keys) {
strs.push(`## ${key}\n\n`);
rows[key].sort((a, b) => b[0] - a[0]);
for (const row of rows[key]) {
strs.push(`${row[1]}\n`);
}
strs.push(`\n`);
}
const indexTemple = `% Dylan Zhang Blog
:::::{#index}\n\n${strs.join('')}:::::\n`;
if (hasNewBlog) {
fs.writeFileSync(dirs.index[0], indexTemple);
shell.exec(commands.index);
console.log('已生成:', dirs.index[1]);
}
文章的生成流程如下:
- 所有的博客 md 文件保存在 src/blog 这个目录下,按照年份归档。
- 遍历所有 md 文件,检查其最后修改时间,并读取元数据。
- 检查对应的 html 文件是否已经生成,不存在直接生成。
- 如果已经生成过 html,检查 md 文件的最后修改时间,如果大于 html 文件,则重新生成。
首页的生成流程如下:
- 使用 hasNewBlog 记录是否有新文章。
- 如果有新文章生成,则重新生成 index.md 并转换 index.html
更进一步
这个脚本,还有几个小细节需要考虑:
- about 等其他常规页面的生成。
- 页面引用的 footer.html 如果有变化,所有 html 需要重新生成。
因为当前这些没有改动,暂时先不管了,等有需要时再来完善。
What I Learned
- Pandoc 转换时可以从 yaml frontmatter 中读取标题和日期等变量, 使用 gray-matter 也可以方便在 js 中调用元数据。
- Pandoc 中可将内容包裹在自定义的块中,指定 id 或 class 名称,方便 css 美化。
- Pandoc 中 -d 参数使用 yaml 默认配置文件。
- 使用 fs 遍历文件和查看文件信息。
- 日期字符串转换的一些操作。
- 使用 shelljs 在 node 中运行 shell 命令。