给博客图床造了个轮子:从手动上传到自动水印的 blogtool
PicGo 配不好的痛,化成了自己造轮子的动力。用 AI 写了一个 Markdown 图片自动上传 + 水印工具,顺手开源了。
写博客的人大概都有一个共同的痛点:图片管理太麻烦了。
本地写 Markdown 的时候引用本地图片,发布时再一张张上传到图床、手动替换链接——这流程重复、无聊、容易出错。在写上一篇文章的过程中,我终于下定决心把这个事情彻底解决掉。
为什么需要一个图床?
静态博客(比如我用的 Astro)本身不会帮你托管图片。虽然可以把图片放在 public/ 目录里一起构建,但随着文章增多,仓库体积会快速膨胀。把图片放到独立的对象存储服务(图床),用 CDN 加速访问,才是更合理的架构。
图床 = 图片的”云硬盘”,通过 URL 直接访问,博客文章里只需要放链接即可。
Cloudflare R2:薅个免费羊毛
对比了一圈主流对象存储之后,我选择了 Cloudflare R2。原因很简单:
- 免费额度够用:每月 10GB 存储 + 1000 万次读请求,个人博客完全够
- S3 兼容协议:直接用 AWS SDK 操作,生态成熟
- 自带 CDN:Cloudflare 全球节点加速,图片加载飞快
- 可绑定自定义域名:比如我的图床域名是
img.maigic.top

R2 的创建流程很直观:注册 Cloudflare 账号 → 左侧菜单找到 R2 → 创建存储桶 → 记下 Endpoint 和密钥。如果你是第一次用,可以参考 Cloudflare R2 官方文档。
图床上传工具的折腾之路
有了 R2 存储桶之后,接下来就是怎么把图片传上去的问题。
手动上传?算了吧
R2 控制台自带文件上传功能,但对于博客场景来说,每次写完文章都要手动上传、复制链接、替换 Markdown 里的路径……三步操作重复 N 次,写一篇有 10 张图的文章就得操作 30 次。
PicGo:过于庞大,依赖插件
PicGo 是国内最知名的图床上传工具,支持多种图床、自动复制链接、甚至有 VS Code 插件。看起来完美。
但有两个问题:
- 本身程序较大,如果是GUI版本的,安装后就几百M大小了
- 功能需要开发者维护第三方插件,我在实际配置的时候卡住了——R2 的 S3 插件配置项和 R2 的实际参数对不上,试了几种组合都连接失败
可能是我配置方式不对,也可能是插件版本兼容问题,总之 PicGo 和 R2 这对组合,在我这里没能愉快地工作起来。
既然现成的不好用,那就自己写
作为一个 AI 时代的开发者,我的第一反应不再是”去哪找个更好的工具”,而是——让 AI 帮我写一个。
于是我花了一个下午,从零开始搭建了一个基于 AWS S3 SDK 的上传工具。核心功能很简单:
- 扫描 Markdown 文件,找到所有本地图片引用(
这种) - 上传到 R2,按内容 hash 自动命名,避免重复上传
- 替换原始链接,把本地路径换成 R2 的公网 URL
第一次跑通的时候,看着 Markdown 里的本地路径一个个变成 https://img.maigic.top/blog/... 的链接,那一刻的满足感,比写十篇博客还强。

新需求:给图片加上品牌水印
工具用了一段时间之后,我有了新的想法——给上传的图片自动加上品牌水印。
毕竟写技术博客免不了截图,截图里可能有产品界面、代码编辑器、终端输出。如果这些图片被别人转载,没有水印的话完全看不出处。
但手动加水印又回到了老问题:重复劳动。既然上传已经自动化了,水印为什么不一起加?
于是我继续迭代这个工具,增加了水印功能:
- 使用 Sharp 库处理图片,性能优秀
- 支持 SVG / PNG 水印图片源
- 水印大小按图片短边动态缩放(不会在小图上贴一个巨大的 logo)
- 图片太小时自动跳过(短边 < 400px 不加水印,避免喧宾夺主)
- 可配置透明度、位置、最小图片尺寸

水印放在左下角,透明度很低(15%),正常浏览几乎看不出来,但放大或者仔细看可以辨认。存在感恰到好处。
整合开源:@maigic/blogtool
工具在本地跑稳之后,我觉得这个东西也许能帮到其他有类似需求的人。于是我把它整理成了一个标准的 npm 包,开源到了 GitHub。
@maigic/blogtool — 一个 Markdown 图片自动上传 + 水印工具。
安装
npm install @maigic/blogtool
快速开始
首先在项目根目录创建配置文件 blogtool.config.mjs:
export default {
storage: {
endpoint: "https://<ACCOUNT_ID>.r2.cloudflarestorage.com",
credentials: {
accessKeyId: "你的 Access Key ID",
secretAccessKey: "你的 Secret Access Key",
},
bucket: "你的存储桶名称",
publicDomain: "你的图床域名",
},
watermark: {
imagePath: "./logo.svg", // 水印图片路径
},
markdown: {
dirs: ["./src/content/blog"], // 要扫描的 Markdown 目录
},
};
如果懒得手写配置,可以直接运行
npx blogtool init,会自动生成一份模板。
然后运行:
npx blogtool
工具会自动扫描配置目录下所有 Markdown 文件,找到本地图片引用,上传到 R2 并替换链接。

加到 Git Hook 自动化
如果你想在每次 git commit 之前自动处理图片,可以添加 pre-commit hook:
echo '#!/bin/sh\nnpx blogtool' > .git/hooks/pre-commit
这样每次提交代码时,所有新的本地图片都会自动上传并替换链接。写博客的时候只管用本地路径引用图片,提交时一切自动完成。
也支持编程调用
如果你需要在构建流程中集成,也可以作为 Node.js 模块使用:
import { processImages } from "@maigic/blogtool";
const result = await processImages({
storage: { /* ... */ },
markdown: { dirs: ["./posts"] },
});
console.log(`上传了 ${result.totalUploaded} 张图片`);
技术细节
在开发过程中遇到了一些有意思的技术问题,记录一下。
图片去重
同一张图片如果出现在多篇文章里,不应该重复上传。工具使用文件的 MD5 hash 作为 R2 的 key 的一部分,上传前先 HEAD 请求检查文件是否存在,已存在则跳过。这样相同的图片无论引用多少次,在 R2 里只有一份。
SVG 水印处理
SVG 水印不能直接贴上去——原始 SVG 可能有大面积背景色,直接叠加会遮挡图片。工具会自动解析 SVG 结构,移除背景矩形,只保留图形元素,再用 Sharp 以指定透明度合成到目标图片上。
跨平台兼容
工具使用 tsup 构建,同时输出 ESM 和 CJS 格式,支持 Node.js 18+。CLI 和编程接口都有完整的 TypeScript 类型定义。
写在最后
这个工具的诞生路径很典型:遇到问题 → 现有方案不满意 → 用 AI 快速实现 → 迭代完善 → 开源分享。
整个过程里 AI 写了大部分代码,我做的是:定义需求、做技术决策、测试验证、处理 AI 搞不定的边界情况(比如 SVG 水印渲染的细节调优)。这种协作模式在可预见的未来会越来越普遍。
如果你也在维护一个博客,并且被图片管理折磨过,不妨试试 Maigic 的 blogtool。有问题或者建议,欢迎在 GitHub 提 Issue。