被公众号排版折磨够了,我做了 feishu2wx:任何 Markdown 都能稳定发公众号
- 不止飞书,任何来源的 Markdown 都能排版发公众号,粘贴检测自动区分飞书 HTML 和纯 Markdown
- 编辑器带 50 步撤销、快捷键、源码语法高亮和文章大纲,不止是个 textarea
- 4 套主题 16 种字体,设置面板分组管理,引用块背景边框间距三项独立配置
- 复制、导出 HTML、推送草稿箱三个出口共用同一套内联样式,结果一致
- CLI 复用 Web 端渲染逻辑,同一套排版跑在浏览器、终端和 CI 三种环境
大家好,我是若风。
写公众号这件事,内容是一半,排版是另一半。
很多人在飞书或者本地编辑器里把文章写完,松一口气,结果一到公众号编辑器,噩梦才开始。代码块的语法高亮没了,表格挤成一坨,引用块变成普通文字,图片得一张张手动传。一篇 3000 字的技术文章,写花两小时,排版能再花四十分钟。
我自己被折磨够了,就写了个工具,叫 feishu2wx。
项目地址:github.com/wangruofeng/feishu2wx
网页地址 1 (Github Pages):https://blog.wangruofeng007.com/feishu2wx
网页地址 2 (Cloudflare Pages):https://feishu2wx.wangruofeng007.com/
👇 网站界面如下 👇

名字叫 feishu2wx,但它其实不止转飞书。不管你的内容是从飞书复制来的,还是在 VS Code、Cursor、Obsidian 里手写的 Markdown,甚至是别的 Markdown 工具渲染后复制过来的,都能排版成公众号能稳定显示的样子,复制、导出、或者直接推到草稿箱。
今天这篇,就把核心功能从头到尾梳理一遍。
一条管道,三个出口

feishu2wx 一条管道连接三个出口
先看整体。feishu2wx 的核心是一条数据处理管道。
飞书 HTML / Markdown 文本
-> 粘贴检测,飞书 HTML 转成 Markdown
-> Markdown 渲染成预览 HTML
-> 内联样式注入 + 微信兼容处理
-> 复制到剪贴板 / 导出 HTML 文件 / 推送草稿箱
重点在这条管道的设计。不管最后从哪个出口出去,中间走的都是同一套渲染逻辑。复制到公众号编辑器、导出一个 HTML 文件、直接推送到草稿箱,这三个动作产出的样式完全一致。你不会遇到「预览里好看,复制过去就变形」这种事。
下面顺着这条管道,一段一段讲。
输入端,不止飞书

feishu2wx 支持多种输入来源
工具叫 feishu2wx,但输入端不挑食。它支持三种内容进来。
飞书粘贴,自动识别

飞书粘贴内容的自动识别与转换
从飞书复制内容,剪贴板里带的是 HTML,而且带特征标记,data-lark、larksuite、feishu.cn这些关键词。feishu2wx 检测到这些标记,就知道这是飞书来的,走专门的转换规则,把 HTML 转成 Markdown。
转换基于 Turndown 加 GFM 插件,但飞书有自己的方言,得单独适配。比如高亮用 ==text==语法,代码块的 HTML 结构有好几种写法,表格经常带colspan要补齐列数。这些不是装个插件就能搞定的。
纯 Markdown,直接吃
如果你根本不用飞书,在本地编辑器写 Markdown,直接粘贴进来就行。粘贴检测会判断,如果纯文本本身就是合法的 Markdown,就当 Markdown 处理,不会多此一举去转 HTML。
也支持导入本地 .md 文件,拖进来就完事。习惯在 Cursor、VS Code 里写东西的人,不用改工作流。
渲染后的 Markdown 页面,也能还原
还有种麻烦情况。你从某个已经渲染好的 Markdown 页面复制内容,比如别人的博客,剪贴板里的纯文本已经丢了 Markdown 语法,变成普通文字了。feishu2wx 会根据 HTML 结构判断,如果有h1-h6、blockquote、pre这些标签,但纯文本不像 Markdown 源码,就按 HTML 结构还原成 Markdown,而不是退化成普通文本。
当然,要是这些自动判断偶尔不靠谱,设置面板里有个「智能 HTML 转 Markdown」开关,关掉就只保留纯文本。工具应该听你的,不是替你做决定。
编辑器,不只是个 textarea
内容进来了,左边是编辑区。这个编辑区看着普通,其实不是简单的 <textarea>。
50 步撤销,光标精确回位
它有自定义的撤销重做系统,保存 50 步历史记录,每一步都记了光标位置。按 Cmd+Z 撤销,光标会精确回到你编辑那个位置,而不是跳到文末。这个细节用惯了编辑器的人会很敏感,光标乱跳真的很烦。
快捷键一套
Cmd+B 加粗、Cmd+I 斜体、Cmd+U 下划线、Cmd+K 插入链接。如果光标处有选中文本,自动包裹语法,如果文本已经被包裹了,智能解包裹。按 Option+E 还能切换编辑和预览。所有快捷键在底部抽屉面板里一览无余。

源码语法高亮
编辑器里写 Markdown 的时候,源码本身有语法高亮。四种配色,GitHub、Dracula、Monokai,或者关掉。实现方式是 overlay 模式,在 textarea 下面叠一层高亮,文字透明只露着色,不影响你正常输入。写代码块、标题、列表的时候,颜色提示很直观。

文章大纲,长文导航
写长文容易迷路。编辑器底部有个大纲按钮,会解析文章里的 H1 到 H3 标题,跳过 frontmatter 和代码块里的标题,生成一个大纲列表。点哪一项,光标就滚到对应标题那行。几百行的文章,跳转很方便。

底部工具栏还塞了一堆快捷按钮,插入标题、列表、链接、引用,切换 H1 样式、图片模式、代码块风格,一键复制公众号,常用操作不用敲键盘。


排版系统,主题和细节都能控

feishu2wx 主题与细粒度排版设置
内容编辑好,右边预览区实时显示渲染效果,左边滚右边跟着滚。但光渲染出来不够,得能调样式。
4 套主题,深浅都支持
内置 4 套主题,经典黑白、橙色、蓝色、青绿。每套主题都有浅色和深色两种模式,还能跟随系统暗黑设置自动切换。经典主题的深色模式是独立调色的,不走 CSS 变量继承,颜色更可控。

每套主题定义了 9 个颜色值,主色、H1 到 H6 各级标题色、链接色、引用块边框色和背景色、表头背景和文字色。这些颜色贯穿整个排版链路,预览里用 CSS 类名,复制到微信时全部转成内联样式。
16 种字体,全免费无版权
支持 16 种字体,系统默认、微软雅黑、宋体、黑体,英文的 Arial、Helvetica、Georgia 这些,还有 Google Fonts 的 Roboto、Open Sans、Lato 等。全是免费无版权的,公众号商用也放心。

设置面板分组管理
设置入口在右上角,点开是一个分组面板。通用、标题、正文、引用块、图片、代码块、模板、公众号,八组分开管。字体、H1 样式、分割线、表格阴影、图片模式、代码块风格,都在对应分组里,不会堆成一坨。

细粒度开关,每个都是踩坑来的
除了换主题换字体,还有一堆细粒度开关。
H1 能不能显示底部横线,能不能反显(主色背景加白字,背景宽度随文字自适应,多行标题背景色还连续不断),居中还是左对齐。图片有默认、边框、阴影三种模式,还能开圆角。代码块有经典浅色和现代深色两种风格,现代那种带三个圆点头部的窗口样式。表格阴影、水平分割线,都能单独控。
引用块这块做得特别细,背景、边框色、间距三项独立配置。背景能开能关,边框色跟主题走,间距有宽松和紧凑两档。预览层和微信输出层都同步处理,不会预览里一个样复制过去又一个样。这三项在 Web 端和 CLI 里都能配,命令行用 —blockquote-background-mode、—blockquote-color-mode、—blockquote-height-mode三个选项。
预览,所见即所得
预览区不只看个大概。
双设备预览
电脑和手机两种模式切换。手机模式下内容以 420px 宽度居中,模拟真实阅读效果。很多排版在电脑上看着没问题,一缩到手机宽度就露馅,这个切换能提前发现问题。

全屏预览
全屏模式下,电脑预览内容 60% 宽度居中,手机预览 420px 居中,多余间隙都去掉,保持圆角样式。按 ESC 或点退出就回来。

图片查看器
预览区里的图片能点击放大查看。自动收集文章内所有图片,键盘左右箭头切换浏览,底部显示序号,比如 2 / 5。写图文并茂的文章时,检查图片很方便。
还能隐藏左侧源码,只看预览,专注排版效果。

三个出口,同一套内联样式

同一套内联样式通往三个出口
这是整个工具最核心的部分,也是最容易踩坑的地方。
为什么微信必须内联样式

Markdown 发布时面对的微信兼容边界
微信公众号编辑器对 HTML 的限制,比大多数人想的严格。CSS 类名全部失效,你写的 .code-block类复用不了,得给每个元素写完整的style属性。em、rem、vh这些单位直接被吞掉,只认px。列表结构会被重新排列,精心设计的嵌套列表粘贴进去大概率打乱。
外部图片链接直接被过滤,不是微信 CDN 上的图片会被移除。SVG 完全不支持,连多色图标都显示不了。链接的标签颜色也会被吞掉。
这些问题单个看都不致命,加在一起,足以让任何想在公众号发技术文章的人崩溃。
出口一,一键复制
点复制按钮,把格式化后的内容复制到剪贴板,打开公众号编辑器按 Cmd+V 粘贴,样式尽量完整保留。
这一步用了三级回退策略。第一级是 Clipboard API 加 ClipboardItem,现代浏览器支持的方案,HTML 和纯文本同时写入剪贴板。第二级是 execCommand 加 contenteditable div,兼容方案,创建临时编辑区域选中后复制。第三级是 textarea 兜底,至少保证纯文本能出来。
还支持智能选区。你在预览区选中了部分内容,只复制选中部分,没选中就复制全文。
代码块的语法高亮有个讲究。highlight.js 生成的高亮是靠 CSS 类名实现的,但微信不认类名。feishu2wx 的做法是从预览区 DOM 里递归读取每个元素的getComputedStyle(),把计算后的颜色值直接写成内联样式。这样做最准确,但也是最耗性能的一步。

出口二,导出 HTML 文件
点导出按钮,把排版后的内联样式 HTML 下载成一个独立的 .html 文件。文件名自动取文章标题,frontmatter 的 title 字段或者正文首个 H1。下载的文件浏览器双击就能打开,样式和复制到公众号完全一致。
这个出口适合存档,或者给不用公众号的人看排版效果。

出口三,推送草稿箱
配置好公众号 AppID 和 AppSecret,点推送按钮,文章直接进公众号草稿箱,登录公众号后台就能看到。
推送流程是这样的。浏览器 localStorage 存凭证,推送时凭证随请求发送到 Cloudflare Functions,Functions 调用微信 API,获取 access_token,上传图片,创建草稿,返回结果。


微信标签保守基线
微信公众号编辑器没有公开的 HTML 标签白名单,很多标签「有时能用」不代表「值得依赖」。feishu2wx 在项目里明确了一份保守基线。
稳定依赖的标签,p、span、strong、em、ul/ol/li、a、img、section、blockquote、h1-h6、table系列、hr、sup/sub。不依赖的,figure、figcaption、div、复杂布局标签。不支持的,script、style、iframe、video。
对应实现做了收敛。预览层可以保留更语义化的figure加figcaption,微信导出层自动降级成section加img加p。不再把「标签语义正确」误认为「公众号里就一定稳定」。
图片处理
图片这块踩了不少坑。外部图片链接在复制时会被微信过滤,推送草稿箱时,后端会先把所有图片上传到微信素材库,再把 HTML 里的 URL 替换成微信 CDN 地址,并发上传最多 4 个。
WebP 图片微信正文图不支持,服务端归一化处理。静态 WebP 转 PNG,动态 WebP 转 GIF,通过 sharp 库逐级缩小尺寸直到低于 1MB。SVG 完全不支持,导出时自动通过 Canvas 光栅化成 PNG。
带说明文字的图片,预览层用figure加figcaption,复制到公众号时自动降级成section加img加p,更稳妥。图片的上下间距由外层块统一控制,不在 <img> 上设上下 margin,避免微信里间距不一致。
推送草稿箱,凭证只存在你浏览器里
推送草稿箱这个功能,最值得说的是安全设计。
公众号 AppID 和 AppSecret,只存在用户浏览器的 localStorage 里,不经过任何服务端持久化存储。每次推送时凭证随请求发送到后端,后端只用来调用微信 API,用完即走,不存储。
多用户场景也没问题。每个人在前端配置自己的凭证,凭证存在各自浏览器里,互不干扰。几个人共用同一个在线版本,各自配置各自的,谁也看不到谁的。
推送时还有几个自动化。文章标题自动读取,优先取 frontmatter 的title字段,没有就取正文首个 H1,都没有显示「未命名文章」。封面图自动读取 frontmatter 的cover字段。文章首尾模板可以配置,推送或复制时自动拼接到正文前后,frontmatter 之后正文之前拼一个,正文之后再拼一个。
封面图没准备的话,工具自动生成。用文章第一张图片裁剪成微信要求的 2.35:1 比例。如果文章里没图片,就用渐变背景加标题文字生成一张。
注意 ⚠️:使用推送草稿箱功能,在第一次配置时需要去微信开发者平台https://developers.weixin.qq.com/platform,获取 AppID 和 AppSecret 配置到设置菜单底部,然后点击一次「推送」按钮,接口会报错,并且告诉你需要家白的 IP,将这个 IP 添加 IP 白名单,下次即可直接推送到微信公众号草稿箱。




CLI,把排版能力搬出浏览器

同一套排版逻辑运行在浏览器、CLI 和 CI
feishu2wx 最早就是个网页,打开粘贴预览复制。但真实使用场景开始往两个方向延伸,有人继续在网页里可视化排版,有人在本地编辑器、脚本、CI、批量发布流程里工作,希望同一套排版规则也能在命令行跑。如果能力锁在浏览器里,这个工具天花板就很低。
v1.19 之后补上了完整的 CLI。现在直接在终端能完成所有操作。
# 初始化配置
feishu2wx init --project
# 配置公众号凭证
feishu2wx auth set --app-id APPID --app-secret APPSECRET
# 设置主题和排版参数,引用块三维度和正文对齐都能配
feishu2wx theme set blue \
--show-h1-underline \
--align-h1-left \
--blockquote-background-mode theme \
--blockquote-color-mode theme \
--blockquote-height-mode compact \
--text-align-mode justify
# 渲染并预览,本地图片自动内联为 base64
feishu2wx render article.md --preview
# 直接推送到草稿箱
feishu2wx publish article.md --title "文章标题" --cover cover.jpg
CLI 的排版选项和 Web 端完全对齐。主题、字体、代码块样式、图片模式、H1/H2 反显和对齐这些常规项都有,引用块三项独立配置(背景、边框色、间距)和正文对齐也能在命令行里设。旧的 —show-blockquote-bg布尔开关保留兼容,但新的 —blockquote-background-mode优先级更高。
CLI 不是额外写了一堆命令行功能。渲染时在 Node.js 里用 JSDOM 模拟浏览器环境,然后调用 Web 端同一套 renderMarkdown() 和 formatForWeChat() 函数,零逻辑重复。同一套排版能力现在跑在三种环境里,浏览器交互、本地终端、自动化脚本。
还支持从标准输入读取内容,方便管道操作。
cat article.md | feishu2wx render --preview
—copy选项把渲染后的 HTML 直接写入系统剪贴板,macOS 用pbcopy,Windows 用clip,Linux 用xclip,不用手动复制文件内容。
项目级配置,排版规则跟着仓库走
过去配置是用户级的,默认放在 ~/.feishu2wx/config.json。单人用没问题,但多人协作或多项目并行时,不同项目的排版偏好会互相污染。
现在支持项目级配置,放在项目根目录的 .feishu2wx/config.json,自动优先读取。配置读取优先级很清晰,CLI 参数大于环境变量,大于项目级配置,大于用户级配置,大于默认值。每一层都能覆盖上一层,但不会互相污染。
配置文件权限默认设为0o600,仅用户可读写,目录权限0o700,不会意外泄露凭证。还支持环境变量覆盖,FEISHU2WX_WECHAT_APP_ID和FEISHU2WX_WECHAT_APP_SECRET可以覆盖配置文件里的值,方便 CI 环境。
这个变化把工具从「个人偏好面板」推进到了「项目工作流配置」,排版规则可以跟着仓库走,也能写进 CI 脚本。
值得注意的是,Web 端和 CLI 端配置是独立的。浏览器里设的主题字体存在 localStorage,CLI 不读,CLI 的配置存在文件系统,浏览器也不碰。两端互不干扰,各自维护偏好。
部署,双通道各取所需
项目采用双通道部署。
GitHub Pages,纯前端。每次推送到 main 分支自动部署,只包含排版和复制功能,不需要后端,适合只用排版功能的用户。
Cloudflare Pages,全栈。包含完整前后端功能,Cloudflare Functions 处理微信 API 的代理请求。两个部署通道共享同一个前端代码。
本地开发三种模式,npm start纯前端,npm run dev前端加 Express 后端,npm run cf:dev前端加 Cloudflare Functions 本地模拟。
写在最后
feishu2wx 最早只想解决一个很具体的问题,把飞书写的东西排版好看一点发到公众号。
但认真对待一个问题,它会把你带得更远。排版规则要可配置可复用,发布流程要可自动化可脚本化,配置要跟着项目走而不是锁在浏览器里。最后这个工具从一个网页小东西,变成了一套能从浏览器、命令行、CI 脚本三种入口使用的排版工作流。
核心就一句话,把飞书内容或者任何 Markdown,变成公众号能稳定显示的样子,少折腾。
而且它叫 feishu2wx,但不只转飞书。你在任何地方写的 Markdown,都能拿来用。
如果你也在为公众号排版头疼,欢迎试用。项目地址,github.com/wangruofeng/feishu2wx
觉得有用的话,给个 Star 就更好了。
评论互动