Hexo 文章一键发布到微信公众号
博客写完,还要打开微信公众号后台,把 Markdown 粘进网页版排版工具,选主题、挑代码高亮,再把十几张图片一张张传上去,最后设封面、填原文链接……一篇文章光搬运就得半小时,搬完常常还发现图串了、代码块样式乱了。
后来我把这一整套流程压成了一条命令:
1 | pnpm run pubmp |
这条命令会:
- 自动挑出
source/_posts下最近改动的那篇文章——未提交的改动优先,没有就回溯到最近一次改过文章的提交; - 补齐 front-matter 里缺的默认值:封面、作者、原文链接、留言开关;
- 把正文里的站内路径
/gallery/...换成磁盘绝对路径,并把微信不支持的 webp 转成 png / jpg; - 通过 stdin 把处理好的内容交给文颜,由它渲染排版、上传图片、创建草稿。
选型
我要的功能很明确:读 Markdown → 渲染成公众号兼容的 HTML → 批量上传图片 → 创建草稿。这类工具其实不少,但绝大多数是网页版,用完还是得手动复制粘贴,没解决”搬运”这件事。
最后选了命令行工具文颜(wenyan-cli)(npm 包名是 @wenyan-md/cli):本地渲染、支持自定义主题和代码高亮、能调微信素材接口上传图片并建草稿,而且支持从 stdin 读取内容,天生适合做脚本的下游。
安装
1 | npm install -g @wenyan-md/cli |
下文统一用 wenyan 来调用它。
先拿到 AppID 和 AppSecret
两个前提:
- 管理员身份:必须用该公众号绑定的管理员微信扫码。
- 账号类型:订阅号、服务号都行。未认证的个人订阅号也能拿到凭据。
打开 微信开发者平台。
AppID 有两个入口,看到的都是同一个值:
- 微信开发者平台:「我的业务」→「公众号」→「基础信息」,明文显示,可一键复制;
- 公众平台后台:
mp.weixin.qq.com→ 设置与开发 → 账号设置 → 注册信息。
AppSecret 走这条路:
- 打开 https://developers.weixin.qq.com/platform,管理员微信扫码登录;
- 顶部「我的业务与服务」→ 点「公众号」进详情页;
- 切到「基础信息」Tab,下滑找到「开发密钥」;
- AppSecret 旁点「启用」(首次)或「重置」,再次扫码验证,弹出完整密钥。

三个必须记住的点:
- 只显示这一次。平台不保存明文,关掉弹窗就再也看不到,只能重置。
- 重置会让旧密钥立即失效。如果已经有脚本在用,重置前先想清楚。
配 IP 白名单
就在同一页、AppSecret 正下方有个「API IP 白名单」,点「编辑」填入服务器或本机的出口公网 IP(多个用换行或逗号分隔)。旧入口在 mp 后台「设置与开发 → 安全中心 → IP 白名单」,两处改的是同一份名单。
不知道自己 IP?访问 https://myip.ipip.net 看一眼。
坑点:
- 家用宽带 IP 会变,隔段时间可能又要重配;
- 不配会报
40164 invalid ip xxx not in whitelist;获取 token 时被拦则报61004。
把凭据交给 wenyan
文颜提供了交互式命令:
1 | wenyan credential --set |
它会依次问 AppID、AppSecret(输入不回显),再问一个可选的别名(用来简化 AppID 的显示),然后写进配置文件。想知道文件到底存在哪,执行:
注意 文颜官网 上提供的命令
wenyan credential add是错的。
1 | wenyan credential -l |
Windows 下默认是 %APPDATA%\wenyan-md\credential.json。
找出”最该发布的那篇文章”
我不想每次都手动指定文件,于是定了两条规则:
- 工作区里有未提交的
source/_posts/**/*.md,说明刚写完一篇,取其中磁盘修改时间最新的; - 没有未提交的,就交给 git,找最近一次改动过
source/_posts下 md 的提交,再从涉及的文件里取最新的。
1 | // 未提交的 md(含未暂存、未跟踪) |
两个细节:git status --porcelain 每行文件名前有 3 个字符的状态位,得 slice(3) 去掉;重命名会输出 old -> new,要取箭头右边的;git 默认会转义非 ASCII 文件名,加 -c core.quotePath=false 才对。
用 stdin 喂进去,不落盘
文颜支持管道,可以通过脚本直接将文章传给文颜:
1 | const result = spawnSync(process.execPath, [cliPath, "publish", ...args], { |
用 process.execPath + CLI 的入口文件路径来调,而不是直接敲 wenyan,这样不依赖 PATH,也不会因为不同 shell 找错二进制。默认参数写死在脚本里,命令行传的额外参数追加在后面,想换主题直接覆盖:
1 | const defaultArgs = ["--theme", "lapis", "--highlight", "github-dark"]; |
1 | pnpm run pubmp -- --theme phycat |
补齐 front-matter 默认值
公众号草稿强制要求封面,另外我还想带上作者、原文链接和留言开关。这些在每个 md 里抄一遍太啰嗦,让脚本补:
1 | function frontMatterDefaults(file) { |
原则是只补缺失的字段、已有的不覆盖,而且只改内存里的字符串,原始文件一个字节都不动。source_url 由文章相对 source/_posts 的路径拼出来,正好等于 Hexo 生成的永久链接。
站内绝对路径要换掉
正文里的图片写的是 /gallery/2026/xxx.webp 这种站内绝对路径——浏览器认,但文颜是把它当本地文件解析的,直接报找不到。所以发布前统一换成磁盘绝对路径:
1 | function absolutizeGallery(content) { |
正则只匹配 、src="..."、front-matter 列表项和 cover: 这几种位置,免得误伤代码块——我的文章里就写过 media_folder: "source/gallery/2023" 这种配置片段。
微信不收 webp
第一次真跑就翻车了:
1 | 40113: unsupported file type |
微信素材库不支持 webp,而我的图床里存的基本都是 webp。那就用 sharp 在发布前转一道:有透明通道的转 png,其余转 jpg(无损的 PNG 体积往往大不少)。
1 | const { hasAlpha } = await sharp(source).metadata(); |
顺序很重要——先把 /gallery/ 换成绝对路径,再扫 .webp 转换,不然扫不到文件。
转换结果按「路径 + mtime」缓存到系统临时目录,同一张图不会每次发布都重转;文颜自己也有一份上传缓存,同一张图不会重复上传。
踩坑记录
- AppID:文颜只有显式传
--app-id时才去读凭据文件,不传就直接报”未提供 AppID”。所以得先从credential.json里把公众号 id 取出来传进去。 - 封面:草稿强制要有封面,正文一张图都没有时会直接失败(”你必须指定一张封面图或者在正文中至少出现一张图片”)。默认封面就是为此准备的。
最后
现在写文章 → commit,然后
1 | pnpm run pubmp |
草稿箱里就躺着排好版、图片传好的草稿,剩下的只有人工复核和点群发。自动群发我没做——草稿和群发之间,我还是想留一步人工确认。
参考
Hexo 文章一键发布到微信公众号


