给 du2333/flare-stack-blog
(TanStack Start + TipTap + Better Auth 的 Cloudflare Workers 博客系统)用的非官方运维工具链。
它解决的问题:往这套系统批量发文章,官方没有导入接口。 唯一能程序化发文的通道是 Admin API + API Key,而脚本/Agent 拿不到浏览器 session cookie —— 所以第一件事是给自己造一把 API Key(见下文)。
顺带还做了:Markdown 自动转 TipTap JSON、发布后上线巡检、一键换站点图标与头像。
所有脚本零依赖(Node 18+ 原生 fetch / FormData / Blob),站点地址全靠环境变量传入,
代码里不写死任何域名。
| 脚本 | 干什么 |
|---|---|
publish.mjs |
批量发布 content/*.md:建草稿 → 设分类/slug → 建/挂标签 → 发布 |
md2tiptap.mjs |
Markdown → TipTap(ProseMirror) JSON 转换器(发布的前提) |
verify.mjs |
上线巡检:总数、状态分布、分类计数、逐条 /post/{slug} 状态码、atom.xml 条目数 |
make-index.mjs |
把本地稿子与线上已发布文章求交集,生成可浏览的全站清单 HTML |
upload-assets.mjs |
把 favicon 六件套 + 头像上传到站点 R2 资源区 |
set-site-assets.mjs |
改站点配置里的图标/头像指向,并让公共缓存失效 |
make-site-art.py |
用代码画站点图标与头像(SVG → Chrome 高倍渲染 → Pillow 降采样) |
用 @better-auth/api-key 插件。几个必须知道的点:
- Key 明文格式 =
prefix+ 随机串(本项目defaultPrefix="fsb_",随机部分 64 字符) - 数据库里不存明文,存的是
SHA-256(明文) → base64url start字段存明文前 6 位(fsb_Ez),后台列表展示用enableSessionForAPIKeys=true,所以 Key 可以直接当 session 用
生成并插库:
KEY="fsb_$(node -e "console.log(require('crypto').randomBytes(48).toString('base64url').slice(0,64))")"
HASH=$(node -e "console.log(require('crypto').createHash('sha256').update(process.argv[1]).digest('base64url'))" "$KEY")
START=${KEY:0:6}
NOW=$(node -e "console.log(Date.now())")INSERT INTO apikey
(id, config_id, name, start, reference_id, prefix, key, enabled, rate_limit_enabled, created_at, updated_at)
VALUES
('auto-' || hex(randomblob(8)), 'default', 'automation', '<START>', '<USER_ID>',
'fsb_', '<HASH>', 1, 0, <NOW>, <NOW>);reference_id 必须是该站点的 owner user id(查 user 表)。执行要带 --remote:
cd <worker 源码目录>
CI=1 wrangler d1 execute DB --remote --file=<sql 文件>
wrangler d1 execute偶尔报fetch failed / No internet connection,直接重试,第二次通常就过。
把明文 key 写进工作目录的 .fsb-apikey.txt(只放一行),它已经在 .gitignore 里。
验证:
curl -s -H "x-api-key: $KEY" https://blog.example.com/api/admin/posts | head -c 200
# 期望 200 + {"items":[...],"total":N};无 key 应为 401git clone https://github.com/FBX-Dev/flare-stack-blog-toolkit.git
cd flare-stack-blog-toolkit
export FSB_BASE=https://blog.example.com
echo 'fsb_你的key' > .fsb-apikey.txt
mkdir -p content
cp examples/example.md content/01-example.md
# 先空跑,核对分类/标签/块数是否正确
node scripts/publish.mjs --dry
# 真发(增量发布务必带 --only,否则整个 content 目录会被再发一遍)
node scripts/publish.mjs --only=01
# 巡检
node scripts/verify.mjs
# 生成全站清单
node scripts/make-index.mjs每篇一个 .md,顶部 --- 包住元数据,空行,然后正文:
---
title: 标题
summary: 摘要(列表页显示)
category: 技术
tags: 标签A,标签B
slug: my-post-slug
---
## 第一个小标题
正文……slug 强烈建议手写。 中文标题自动生成的 slug 可能为空或重复,
而前台 URL 就是 /post/{slug},为空就访问不到。
支持的 Markdown 语法:##/###/#### 标题、段落、- 无序列表、1. 有序列表、
> 引用、--- 分割线;行内 **粗**、*斜*、`码`、[文字](链接)。
一级标题
#会被自动降为二级 —— 这个 schema 的 heading 只到 2–4 级。
用之前先看这几条,能省几小时:
-
发布是四步,顺序不能错
POST /api/admin/posts→PATCH /api/admin/posts/{id}(设categoryId+slug) →PUT /api/admin/posts/{id}/tags→POST /api/admin/posts/{id}/publish -
GET /api/admin/posts单次最多返回 50 条,limit传 200 也只给 50。 文章超过 50 篇时必须用offset翻页,否则会出现 「总数看着对、逐条检查却只覆盖前 50 篇」,而且不报任何错。 (?page=2无效,接口只认offset。total字段仍然返回真实总数,容易造成假象。) 本仓库的verify.mjs/make-index.mjs已内置翻页。 -
列表项不含
categoryId(只有 id/title/summary/slug/status/时间)。 要按分类汇总,用GET /api/admin/categories的postCount。 -
前台 URL 是
/post/{slug},不是/posts/{slug}—— 后者是 API 路由,直接访问 404。 -
contentJson是 TipTap JSON,没有 markdown 转换接口,得自己转。 安全可用的节点:doc/paragraph/heading(2-4)/bulletList/orderedList/listItem/blockquote/codeBlock/horizontalRule/text(marks:bolditaliccodelink)。 -
改站点配置必须整段提交:
PATCH /api/admin/config传section:"site"+expectedRevision+ 完整的config.site,省略字段会回落默认值。 revision 版本号在响应体的revisions.site,不在config.site里面。 revision 过期返回 409,要重新 GET 再改,别盲目重试。 改完记得POST /api/admin/cache/invalidate,否则首页还吐旧的<link rel="icon">。 -
上传资源走
POST /api/admin/config/assets(multipart,字段file+assetPath)。assetPath有白名单前缀:favicon/、social/、themes/fuwari/。 落盘 key 是asset/${assetPath},公开地址 =/images/asset/${assetPath},同路径重传即覆盖。
所有脚本共用 scripts/_env.mjs 读取配置:
| 变量 | 默认 | 说明 |
|---|---|---|
FSB_BASE |
必填 | 站点根 URL,结尾斜杠自动去掉 |
FSB_KEY_FILE |
./.fsb-apikey.txt |
API Key 明文文件 |
FSB_CONTENT_DIR |
./content |
文章目录 |
FSB_REPORT_OUT |
./publish-report.json |
发布报告 |
FSB_INDEX_OUT |
./index.html |
清单 HTML |
FSB_SITE_TITLE |
FSB_BASE 的 hostname |
清单页站点名 |
FSB_CAT_ORDER |
空 | 清单页分类顺序(逗号分隔) |
FSB_INDEX_NOTE |
空 | 清单页顶部说明,支持 {n} |
FSB_ASSET_DIR |
./assets |
图稿目录 |
FSB_ASSET_URL_BASE |
/images/asset |
资源 URL 前缀 |
FSB_BASE 故意不给默认值 —— 默认值一旦指向某个真实站点,
别人 clone 下来直接跑就会打到别人家的服务器上。
# 1) 生成图稿(需要 Chrome;Python 需要 Pillow)
ART_CALLSIGN=YOURCALL ART_OUT=./assets python scripts/make-site-art.py
# 2) 上传到 R2
node scripts/upload-assets.mjs --dry # 先看要传什么
node scripts/upload-assets.mjs
# 3) 改配置指向 + 失效缓存
node scripts/set-site-assets.mjs --dry
node scripts/set-site-assets.mjsmake-site-art.py 的思路值得说一句:SVG 是唯一几何源。
Pillow 不能栅格化 SVG,Chrome 能但小尺寸直出有锯齿,所以走
「Chrome 2048px 渲染一次 → Pillow LANCZOS 降采样出全套尺寸」,
这样 16px 的 .ico 和 512px 的 PWA 图标与 SVG 完全同源,不会出现
「Chrome 里和 Safari 里长得不一样」。
两个容易翻车的细节:
- 圆角 vs 满幅要分开:favicon 系列用圆角 + 透明角(标签页里好看);
apple-touch-icon和 PWA manifest 图标用满幅不透明 —— iOS/Android 会自己套形状蒙版, 透明角会被填成黑边。 - 生成后一定看一眼输出目录里的
_usage_light.png/_usage_dark.png预览条, 确认 16px 下还认得出图形,再上传。
- 没有官方导入接口,本工具链是逆向 Admin API 得到的,上游改版可能失效。
- 发布 20 篇实测约 6 分钟(每篇 4 次 API 调用 + 标签创建 + 400ms 节流), 篇数多时请放后台跑,别放在会超时的前台任务里。
- 只覆盖「发文 + 站点资源」这两块,不做主题开发、不做数据库迁移。
- 脚本里对响应结构的解析做了几种兼容写法(
json.items || json.data), 但仍然假定是 Better Auth 系的返回形状。
本项目与 flare-stack-blog 上游无隶属关系,属于第三方工具。