md-blog-publisher:把 Push Markdown 搬上网页

目录 · 9 节
为什么又折腾一遍
之前写过 Push Markdown 的重构,那是把别人的 Electron 桌面工具用 Vue3 + TypeScript 重做了一遍。桌面版用了好几年,确实方便:本地 Markdown 渲染成 HTML,图片批量上传,再通过 MetaWeblog(XML-RPC)发到 WordPress 或博客园。
但桌面版有几个长期痛点:
- 跨平台打包麻烦。Windows / macOS 各打一份,Electron 体积又大,升级还得搞自动更新。
- 文档其实已经在服务器上了。我自己的文章仓库就放在服务器,桌面端还得先同步到本机,再打开软件发布,多绕了一圈。
- 想在手机或别的电脑上改一两处再发布,还得装客户端,不太现实。
于是就有了这次的网页化重写:md-blog-publisher。核心想法很简单——部署一份服务,浏览器打开链接就能用;文档根目录改成服务器上的某个目录,发布逻辑还是原来那套思路。网页版干脆砍掉了 XML-RPC / cnblogs,只对接本服务所在的 WordPress,用当前登录身份走 REST API。
它是什么
一句话:在网页里编辑、预览服务器上的 Markdown,并以 WordPress 登录身份一键发布。
嵌入 WordPress 后台后的桌面端界面大致是这样——左侧文件树,中间编辑,右侧预览,顶栏有同步 Git、设置和发布:
和桌面版比,有两处本质变化:
- 文件系统:从「读用户电脑上的文件夹」变成「读服务器本地的文档目录」(
docsRoot)。 - Markdown 渲染:从依赖浏览器 DOM,改成后端纯字符串渲染(
markdown-it+highlight.js),前端只展示结果。
发布体验尽量对齐原来:front matter(标题、摘要、分类、标签、固定链接、发布时间)、本地图片上传与同名覆盖、文章缓存避免重复创建,这些都还在。不再保存站点账号密码,也不再走 XML-RPC。
整体架构
项目拆成三块:
client/ Vue 3 + Vite 前端
server/ Node + Express 后端
wordpress-plugin/ WordPress 插件(登录态校验 + 媒体上传)
浏览器只跟自己的后端说话。发布请求都由 server 带着 WordPress 登录 cookie 去调 REST API——浏览器不直连 WordPress,账号凭据也不落盘。
大概的数据流是这样的:
浏览器 (Vue)
│ /api/*
▼
Express 后端
├── 读/写 docsRoot 下的 .md 与图片
├── markdown-it 渲染成 HTML
├── 校验 WordPress 登录态,拿 wp_rest nonce
└── 以当前用户调用 WordPress REST
│
▼
WordPress(文章 / 分类标签 / 媒体)
前端负责文件树、多标签编辑、预览、站点设置、发布面板;后端负责真正碰磁盘、碰博客接口。生产环境下 client/dist 由后端一并托管,外面再挂 nginx 反代就行。
前端在干什么
技术栈很常规:Vue 3 + Vite + Pinia。宽屏下布局和桌面版差不多——左侧文件树,中间编辑/预览,设置和发布走弹层。
比较有意思的几点:
- 预览不在前端渲染 Markdown。打开或保存文件时,后端返回渲染好的 HTML,前端直接塞进预览区。这样公式、代码高亮、任务列表等行为前后端一致,发布出去的内容和你看到的预览也一致。
- 发布进度用 SSE。桌面版是界面状态提示;网页版改成服务端事件流,前端能实时显示「读取中 / 上传图片 / 发布中 / 完成」。
- Git 同步。文档根目录如果在 git 仓库里,顶栏会出现「同步 Git」:提交该目录内的改动 →
pull --rebase→push。文章仓库本身就在服务器上时,网页里改完直接推回远程,很顺手。 - 移动端适配。窄屏下改成顶栏 +「编辑 / 预览」切换,文件树变成侧滑抽屉(可拖动手势),编辑页还能开一个悬浮预览小窗。网页化本来就是为了手机上也能改两句再发,这块补上之后才算闭环。
从左到右分别是:侧滑文件树、编辑页(带悬浮预览窗)、全屏预览:
后端在干什么
Express 按模块挂路由:文件树与读写、静态资源(文档里的图片)、设置、发布、上传、打开的标签页、Git。
和发布相关的核心可以概括成一层 Publisher + BlogClient:
Publisher管流程:读文章、处理本地图片、查缓存、新建或更新文章。BlogClient目前只剩 WordPress REST 这一套实现:用浏览器转发来的登录 cookie + 插件签发的wp_restnonce,以当前后台用户发文章、传图片。
图片和文章都有发布缓存(publish-cache.json),按「站点地址::用户名」分区。设置里的 url / username 不参与请求,只用来对齐已有发布记录——从桌面版 XML-RPC 迁过来时,填原来的站点地址和用户名就能命中旧缓存,不会把历史文章当成新文章再发一遍。换机器部署时把设置和缓存一起迁过去即可。
文档根目录有路径守卫,请求不能逃出 docsRoot,避免登录用户把任意系统路径读出来。
浏览器和 WordPress 之间,中间隔着一层 Node。 你在网页里点的预览、保存、发布,请求都是浏览器先到本工具的后端;后端再读服务器上的 .md 和图片文件。实时预览是把编辑器里的正文 POST 给后端渲染,不落盘,所以会走你访问站点的那条链路(公网、CDN 都算在内)。编辑时「插图」则是浏览器把文件上传到后端,写进文档目录,同样会经过外网。
发布时传 WordPress 媒体库不一样。 浏览器只负责触发发布流程;真正上传图片是后端从 docsRoot 读本地文件,再带着登录 cookie 调 REST(含插件提供的同名覆盖媒体接口)。同机部署时可以在环境变量里配一个 WordPress 的内网/回环地址(例如本机 nginx 的 HTTP 入口),让校验登录、发文章、传图都走服务器内部,不绕公网域名、也不经 CDN。对外仍用 HTTPS 域名给浏览器用,两套地址各干各的。没配内网地址时,后端会退回对外的 WP_URL,传图也会从服务器再绕一圈公网,又慢又可能多计 CDN 流量。
鉴权与发布
公网挂一个能读写服务器文档目录的工具,鉴权必须认真点。这里直接复用 WordPress 后台登录:把插件丢进 WordPress,后台左侧菜单嵌入本工具。要求本服务和 WordPress 同域名(子路径),这样浏览器会带上 WordPress 的登录 cookie;服务端只把 wordpress_logged_in_* 转发给插件的校验接口确认身份。本地调试可以设 AUTH_DISABLED=true 跳过登录。
发布只走这一条路:
- 插件的
mbp_whoami在校验登录态的同时签发wp_restnonce,服务端只在内存里保存,不下发给浏览器。 - 发文章时带着 cookie + nonce 调
/wp/v2/posts等接口,作者就是当前登录用户;WordPress 退出后就不能再发布。 - 图片走插件的
POST /wp-json/mbp/v1/media,同名可覆盖,媒体库不会堆出一堆-1后缀。这取代了以前每次升级 WordPress 都要对 XML-RPC 打的同名覆盖补丁。
既然本工具已经不依赖 XML-RPC,如果没有别的东西(手机 App、Jetpack 等)还在用,可以在 nginx 或安全插件里直接关掉 xmlrpc.php,少一个被扫的入口。
部署形态
我自己的用法是:和 WordPress 同域、子路径反代。systemd 跑 Node,nginx 把某个子路径转到本机端口;发布接口是 SSE,nginx 那边要关掉 proxy buffering,不然进度会攒到最后一次性吐出来。
前面有 CDN 时还要注意别把带登录态的页面和接口缓存坏了,未登录时应老老实实返回 401,前端才能提示去 WordPress 登录。这些都是「能跑」和「线上真能用」之间的坑,README 里写得比较细,这里就不展开了。
和桌面版还差在哪
网页版砍掉了 Electron 相关的一切:系统托盘、多窗口、自动更新、本地菜单。也砍掉了 cnblogs / MetaWeblog——只发布到本服务所在的 WordPress,用当前登录身份走 REST。
渲染从 DOM 操作改成字符串处理之后,极端 Markdown 写法下预览可能和桌面版有细微差异,但日常文章基本无感。
小结
md-blog-publisher 不是另起炉灶造一个新编辑器,而是把 Push Markdown 的发布链路搬到「服务器文档目录 + 浏览器」这个场景里,并换成更干净的 WordPress REST 路径:
- 前端负责交互与展示;
- 后端负责文件、渲染、带着登录态转发 REST;
- 插件把工具嵌进 WordPress 后台,并提供登录校验和同名覆盖的媒体上传。
对我来说,最大的收益是:文章仓库放在哪,编辑和发布就在哪,不用再在本机和维护桌面安装包之间来回倒腾,也不用再惦记 XML-RPC 补丁。如果你也是「Markdown 写博客 + WordPress 托管」,又懒得装 Electron 客户端,这种网页化形态会舒服很多。


评论
还没有评论,来说第一句。
发表评论