博客第一次较大规模折腾复盘
这次是尝试让 Claude Opus 4.6 写的主体,我作一些修改。Gemini 实在太弱智了。 所以可能文风会有一些不像我,但是技术文说实话,还是以讲清楚和能写下来为最重要的,所以这次算是一个尝试吧。
说在前面
距离上次搭博客(2025年11月)已经过了大半年。期间写了21篇,但80%以上是网文/番剧观后感,技术向内容少得可怜。本来搞博客是想做技术记录的,结果变成了追书追剧记录本。
不是说写网文不好,但是明明我生活中可写的技术素材、复盘等其他类别内容多到爆炸,有时候感觉24小时里面,就有至少3-4个值得记下来的题材,但就是没写。分析了一下原因:写作流程不够顺滑、编辑器不够舒服、没有图片方案、metadata全靠手动改。每一个都不致命,但叠在一起就足够让人"想到就烦"。
所以这次折腾的目标很明确:降低写作阻力。呈现端改的不多,主要是写作端的工作流大升级。
本次改动概览
- VS Code + Front Matter CMS 插件作为写作主力
- Page Bundle 结构 + 图片直接粘贴
- 自定义脚本实现 date/slug 一键生成
- 图片渲染模板(控制大小)
- .gitignore 规范化
- 各种坑的记录
编辑器选型:Obsidian vs VS Code
一开始考虑过 Obsidian。官网下载试了一下,至少暗黑模式下没有让我眼前一亮的感觉,而且没有官方 Windows Portable 版本,对我这种在意安全性和污染性的人来说不太友好。
最终选了继续用 VS Code,配合插件增强体验。其实 VS Code 自己就有一个实验性的 Markdown Editor(右上角省略号 → Reopen Editor With → Markdown Editor (Experimental)),效果和 Obsidian 的所见即所得几乎一模一样,体验还不错,不过,在我这里不能用鼠标滚轮切换字体大小,有一点点不爽,所以我目前还是用的左边原md文件,右边预览的模式。
Front Matter CMS 插件
这是本次折腾的核心。它是一个 VS Code 插件,提供了类似 CMS 后台的体验:一键创建文章、侧边栏直接改 metadata、一键启动 Hugo Server、内置 Git 同步(commit + push)(虽然不太好用)、Dashboard 管理文章列表等等。
安装与基本配置
装完插件之后,Initialize project 之前,必须确保打开的文件夹是博客根目录,不是 content/post 那一层。否则 Start Server 的路径会错,Create Content 的路径也会错,json文件等等生成的位置也会有问题。
核心配置文件是根目录下的 frontmatter.json。有部分设置可以在插件提供的设置界面修改,但是很多还是必须进到这个 JSON 里修改:
{
"$schema": "https://frontmatter.codes/frontmatter.schema.json",
"frontMatter.taxonomy.contentTypes": [
{
"name": "default",
"pageBundle": true,
"previewPath": null,
"fields": [
{
"title": "Title",
"name": "title",
"type": "string",
},
{
"title": "Slug",
"name": "slug",
"type": "slug",
"editable": true,
"default": "{{slug}}",
},
{
"title": "Description",
"name": "description",
"type": "string",
},
{
"title": "Publishing date",
"name": "date",
"type": "datetime",
"default": "{{now}}",
"isPublishDate": true,
},
{
"title": "Is in draft",
"name": "draft",
"type": "draft",
},
{
"title": "Tags",
"name": "tags",
"type": "tags",
},
{
"title": "Categories",
"name": "categories",
"type": "categories",
},
],
},
],
"frontMatter.framework.id": "hugo",
"frontMatter.content.publicFolder": "static",
"frontMatter.preview.host": "http://localhost:1313",
"frontMatter.content.pageFolders": [
{
"title": "post",
"path": "[[workspace]]/content/post",
},
],
"frontMatter.git.enabled": true,
"frontMatter.framework.startCommand": "hugo.exe server -D",
}
坑1:pageFolders 路径
一开始 path 写的是 [[workspace]](根目录),结果 Create Content 直接把文章创建到了根目录。必须写成 [[workspace]]/content/post。
坑2:Page Bundle
"pageBundle": true 这个选项非常重要。开启后,每篇文章会生成一个独立文件夹,里面放 index.md 和该文章的所有图片。这样图片就不会全混到一起,也不需要什么图床了。
如图。
有意思的是,Hugo 还是比较智能的。Page Bundle 模式和单文件模式是可以共存的,不需要把以前的全都修改一遍。
坑3:多设备同步时的 frontmatter.json 冲突
我在 Windows 上配好了插件,但是配的时候,一开始是打开的 Post 文件夹,所以frontmatter.json等等在post文件夹里面。push 之后到 Mac 上 pull,Mac 上第一次装 Front Matter CMS 插件时,它在根目录又自动初始化了一个默认的 frontmatter.json,导致我改过的那个嵌套的 JSON 失效了,搞得我一脸懵逼。
解决方法:确保只有根目录下有一份 frontmatter.json,嵌套目录下如果有多余的就删掉。多设备首次使用时注意这个初始化行为。事实上,如果你打开的 folder 里面本来就有 JSON 文件,它就不会提醒你要初始化。所以如果它还是让你在第2个设备上初始化,说明已经出问题了。
坑4:Start Server 命令
Front Matter 的 Start Server 默认跑的是 hugo server -D,但在我的 Windows 环境里,WSL 有完整 Node 环境而 Windows 本身只有 hugo.exe。最后发现直接把命令改成 hugo.exe server -D 就行了,在 CMS 的 Settings 里就可以改。
坑5:Other Actions - Enable writing settings
我本来以为这个的意思是允许你写入设置或者说修改 JSON,我还想怎么那么高级,没有想到这个其实是一个改变 VS Code 对 MD 的默认 Text Editor 样式的设置而已,就是把什么行号去掉,然后超过一定字符在视觉上给你自动换行,这样,反正我是觉得非常讨厌,所以把它关掉了。它下面那个 Toggle Center Mode 其实也是一个意思,都只是样式的问题。
时区问题
Front Matter 生成的 date 字段默认是 UTC 时间(+00:00),而我在东八区,看着不爽。
本来想要在 frontmatter.json 里加上时区设置,比如类似于:
"frontMatter.taxonomy.dateFormat": "yyyy-MM-dd'T'HH:mm:ssxxx"
但实际测试下来,仅靠这个设置并不能让它输出 +08:00。Front Matter 似乎原生不太支持本地时区序列化。最终靠自定义脚本彻底解决(见下文)。
Slug 问题:折腾最久的一个
背景
以前我是手动写英文 slug,比如 wenix-blog-setup-review。优点是 SEO 友好、URL 有意义;缺点是每次都要想一遍英文翻译,有时还要问 AI 怎么翻比较地道,还要修改格式之类。太烦了。
所以开始想有没有其他办法。当然维持原来这个格式也可以,就是要写一点自动调用 API 翻译的脚本之类的;然后就是像 N 网或者一些论坛那样,从1开始往后累计,但是这个的问题,一个是每次都要再确认最新的一篇的编号,或者要写一个相对复杂的脚本吧,来确认目前最大的编号,然后再把它加一。一个是,要是以后要删除、隐藏之类的跳号或者其他问题,感觉都有点麻烦。
最后选择了用时间戳。这样子,每个 slug 都是相对独立的,也比较方便让我快速的生成出来。
尝试过的方案
- Front Matter 的 slugTemplate:配了
"frontmatter.taxonomy.slugTemplate": "{{yyyy}}-{{MM}}-{{dd}}-{{HH}}-{{mm}}-{{ss}}",完全没生效。 - slug 字段 type 设为 “string” + default:
"default": "{{now:yyyy-MM-dd-HH-mm-ss}}",生成出来是空字符串,不会被解析。 - slug 字段 type 设为 “slug”:它只会把 title 转成 slug(空格变连字符),对中文标题毫无帮助,生成一堆
%E4%B8%AD%E6%96%87的垃圾 URL。
Front Matter Docs 的官方 AI Bot 也确认了:从 date 字段派生 slug 这个功能原生不支持。
最终方案:自定义 CJS 脚本 + task.json + 绑定 VS Code 快捷键
- 在
.frontmatter/scripts/下创建set-date-slug.cjs;
const fs = require("fs");
const path = require("path");
const filePath = process.argv[2];
if (!filePath) {
console.error("没有收到 Markdown 文件路径。");
process.exit(1);
}
const absolutePath = path.resolve(filePath);
if (!fs.existsSync(absolutePath)) {
console.error(`文件不存在:${absolutePath}`);
process.exit(1);
}
let content = fs.readFileSync(absolutePath, "utf8");
const dateMatch = content.match(/^date:\s*(.+)$/m);
if (!dateMatch) {
console.error("没有找到 date 字段。");
process.exit(1);
}
// 使用 CMS 已经生成的时间,而不是再次获取当前时间。
// 这样即使运行脚本时跨秒,date 和 slug 仍然完全一致。
const sourceDate = dateMatch[1].trim().replace(/^["']|["']$/g, "");
const date = new Date(sourceDate);
if (Number.isNaN(date.getTime())) {
console.error(`无法解析 date:${sourceDate}`);
process.exit(1);
}
const pad = (value) => String(value).padStart(2, "0");
const year = date.getFullYear();
const month = pad(date.getMonth() + 1);
const day = pad(date.getDate());
const hour = pad(date.getHours());
const minute = pad(date.getMinutes());
const second = pad(date.getSeconds());
// 获取当前系统时区偏移。
// 在中国时区环境中应得到 +08:00。
const offsetMinutes = -date.getTimezoneOffset();
const offsetSign = offsetMinutes >= 0 ? "+" : "-";
const offsetHour = pad(Math.floor(Math.abs(offsetMinutes) / 60));
const offsetMinute = pad(Math.abs(offsetMinutes) % 60);
const timezone = `${offsetSign}${offsetHour}:${offsetMinute}`;
const localDate = `${year}-${month}-${day}T${hour}:${minute}:${second}${timezone}`;
const slug = `${year}-${month}-${day}-${hour}-${minute}-${second}`;
content = content.replace(/^date:\s*.*$/m, `date: ${localDate}`);
if (/^slug:\s*.*$/m.test(content)) {
content = content.replace(/^slug:\s*.*$/m, `slug: ${slug}`);
} else {
content = content.replace(/^(title:\s*.*)$/m, `$1\nslug: ${slug}`);
}
fs.writeFileSync(absolutePath, content, "utf8");
console.log(`已更新:${absolutePath}`);
console.log(`date: ${localDate}`);
console.log(`slug: ${slug}`);
- 创建
.vscode/tasks.json; 这一步结束后,可以用 VS Code Run Task 来尝试是否跑通。
{
"version": "2.0.0",
"tasks": [
{
"label": "Blog: 生成本地日期和时间 Slug",
"type": "process",
"command": "node",
"args": [
"${workspaceFolder}/.frontmatter/scripts/set-date-slug.cjs",
"${file}",
],
"problemMatcher": [],
"presentation": {
"reveal": "silent",
"panel": "shared",
"clear": false,
},
},
],
}
- 去
Preferences: Open Keyboard Shortcuts (JSON)绑定一个 VS Code 快捷键。我目前是只配了 Windows 的,用的是ctrl+alt+s。
脚本逻辑是:读取当前 markdown 文件,把 date 改为本地时区时间(+08:00),同时把 slug 设为同样的时间戳格式(yyyy-MM-dd-HH-mm-ss)。
创建文章后按一下快捷键,date 和 slug 就都正确了。效果:
---
title: Alpha Bravo Charlie
slug: 2026-07-22-17-10-06
description: ""
date: 2026-07-22T17:10:06+08:00
draft: true
tags: []
categories: []
---
注意这个脚本需要 Node.js 环境。我的 Node 装在 WSL 里,所以 Front Matter 的 custom script 配置里 command 要对应上 WSL 的 node 路径。
坑6:VS Code 的嵌套文件夹折叠
VS Code 默认会把只有一个子文件夹的目录折叠显示(compact folders)。这导致我以为在 .frontmatter/scripts/ 下创建了脚本,实际上创建到了 .frontmatter/database/scripts/ 里面。报错 MODULE_NOT_FOUND 排查了好一会儿。
这个行为可以在设置里关掉:explorer.compactFolders: false。
图片方案
Page Bundle + Ctrl+V 粘贴
开启 Page Bundle 之后,Front Matter 支持直接在编辑器里 Ctrl+V 粘贴截图,图片会自动保存到当前文章的文件夹里,并插入正确的 markdown 引用。这一步开箱即用,非常舒服。
图片大小控制
默认粘贴的图片会占满整个页面宽度,巨大无比。解决方法是创建一个 Hugo 的 render hook。
在 layouts/_default/_markup/ 下创建 render-image.html,在里面写自定义的 img 标签,控制 max-width 之类的样式。具体数值看个人喜好,我都是让 AI 填的。
EXIF 隐私问题
截图本身一般没有 EXIF 信息。但如果是手机拍的照片直接往博客里贴,GPS 坐标、设备信息什么的都会暴露。
我查了一下 Hugo 的官方文档,应该是对图片经过任何 Hugo 里面内置的处理之后,EXIF 信息就会被丢掉。所以理论上,我现在做了上一步之后就没有信息了。
.gitignore 规范化
之前没有合理的 .gitignore,导致 public/ 文件夹(Hugo 生成的静态网页)、.frontmatter/database/(CMS 缓存)等等,全部被 git 追踪了。Source Control 页面一片 Modified 。
最终的 .gitignore:
# Hugo 生成的静态网页和缓存
public/
resources/
.hugo_build.lock
# Front Matter CMS 的本地缓存文件
.frontmatter/database/
坑7:themes/Stack submodule dirty
即使 .gitignore 配对了,git status 仍然显示 themes/Stack (modified content)。diff 一看是 submodule 的 commit hash 后面多了个 -dirty。
这是因为 Stack 主题文件夹里有未追踪的修改(可能是 .hugo_build.lock 之类的临时文件)。解决方法:进入 themes/Stack 目录,git restore . 撤销一下,git clean -fd清除缓存,然后应该就是搞好了。
坑8:git add * 不会添加 dotfiles
git add * 不会添加 .gitignore、.frontmatter/ 这些以 . 开头的文件/文件夹。要用 git add . 或者显式 git add .gitignore。
Git 相关
Front Matter 的 Sync 功能
开启 "frontMatter.git.enabled": true 之后,CMS 面板上会出现 Sync 按钮,点一下就是 commit + push。比手动敲 hpush 还快一步。
VS Code 自己的 Source Control 面板也能用,Sync 按钮等价于 pull + push。
但是在实战过程中,Front Matter 的 sync 和 fetch 都经常点一下好半天没反应,然后也没有任何成果,不知道为什么,所以现在我都是用 Source Control 来控制。
最终工作流
- VS Code 打开WSL、打开博客根目录(如果上次关闭时是这个页面,会自动打开)
- Front Matter CMS 的 Create Content 创建新文章(自动 Page Bundle)
- 按快捷键运行自定义脚本,自动填充 date(+08:00)和 slug(时间戳)
- 在编辑器里写正文。截图直接 Ctrl+V 粘贴;可以开着 server,实时看一下
- 在 CMS 的侧边栏,选择 Categories 和 Tags
- Is in draft 改 false
- Source control 界面 commit + push 上去
比起以前的流程,省掉了:手动想英文 slug、手动改时区、没有图片方案的尴尬、到处找终端敲命令。写作阻力确实降低了不少。
还没搞但之后想搞的
- AI 辅助写作(特别是技术文章,有些本来就是和 AI 聊出来的,直接让它整理)。这个我还是没搞懂,我试图让 AI 提炼文风,但是最后的结果感觉不太能让我满意,反而就像我现在这一篇,是只给了当时折腾过程的 chat 和 当年那篇博客搭建过程的复盘,让他模仿着写,反而还可以。虽然可能到现在我大概改了20%以上,花的时间也不短,但是确实感觉其他80%是可用状态。
- 评论系统可能换掉 Giscus(需要 GitHub 登录筛掉了99%的人)
- Stack 主题的信息密度偏低,之后可能换主题或者魔改
- 文章封面图/预览图(和 Stack 主题强相关,暂时搁置)
写在最后
这次折腾大概花了一整天,坑比想象的多——特别是 Front Matter CMS 的各种配置项和它与 Hugo 之间的适配。但折腾完之后确实感觉写文章的阻力小了不少。希望这次折腾能让之后的技术文章产出频率高一些吧。
我是正在考虑租房的 Wenix, 希望你开心~