做技术公众号的人大概都有一份隐蔽的困扰:内容管理用Markdown,发布却要面对微信编辑器那一套网页排版。写的时候行云流水,粘贴进后台就原形毕露——代码块塌掉、表格错位、图片裂开。前前后后我折腾过好几套转换方案,目前用得最顺手的是一个叫md2wechat-skill的本地命令行工具——它能把Markdown文档转换成微信公众号后台能接受的内联样式HTML,再配合内置主题和图片策略,基本能做到"所见即所得"。这篇文章会完整记录我实际的安装、配置和踩坑过程,写给正在为公众号排版发愁,又不想放弃Markdown习惯的朋友们。
1. 微信编辑器与Markdown之间的断层,值得为它专门做个工具
先说一个许多技术写作者都撞过的场景:你在本地用Markdown写了一篇带代码块、表格、流程说明的长文,检查了两遍,很满意。然后打开公众号后台,把内容复制粘贴进去。标题层级还算正常,但代码块成了一坨没有缩进的纯文本,表格布局歪七扭八,图片因为本地路径全部裂开。你花二十分钟重新排版,下一次、再下一次,每次都这样。
这就是我一直坚持本地Markdown写作却总要面对的问题。公众号编辑器本质上是网页富文本编辑器,它对Markdown语法没有任何支持,而且粘贴进来的内容会经过它的过滤器,大段class样式、外部样式表、非内联的CSS规则都会被裁掉。微信后台只认内联样式,也就是style属性直接写在HTML标签上的那种。这意味着你写Markdown时享受的所有排版能力,在粘贴进微信那一刻几乎全部失效。
在这个背景下,转换工具就成了Markdown写作者和微信发布之间的桥。市面上其实有不少在线转换网站,把Markdown粘上去再复制结果,也能用。但做工程的人用几次就会不爽:在线转换一次只能处理一篇,样式模板固定没法细调,代码块高亮主题单一,还有内容隐私问题——有些文章发布前并不想送到第三方服务器。所以才有了本地化的命令行转换工具,md2wechat-skill就是其中之一。它的定位很清晰:读取本地Markdown文件,输出一份带完整内联样式的HTML文档,你只需把这份HTML复制并粘贴进公众号编辑器,就能得到接近你在Markdown工具里看到的排版效果。
也顺带解释一下工具名里的"skill"。它更多指"内置技能包"的概念,也就是说这个工具不只是做简单的格式转换,而是封装了一套面向公众号场景的处理规则:代码如何高亮、标题字号如何缩放、表格如何限宽、图片如何处理。选择不同的skill主题,输出风格会跟着变化。这种设计比单纯转换器灵活得多。
对使用者来说,适合它的画像大概是这样:平时用Typora、VS Code或Obsidian这类工具写技术内容,发布平台是微信公众号;不想花大量时间在后台手动排版,也不放心把未发布的文档交给在线转换站;希望有一套本地、可配置、可批量的转换方案。这篇文章后面全部围绕这个需求展开。
2. 安装前先对齐环境:Node版本、依赖项和文件包选择
2.1 运行环境的最低要求与验证方法
md2wechat-skill是典型的Node.js命令行工具。我建议装之前先确认机器上的运行时环境,免得半路被依赖问题绊住。最低要求是Node.js 16以上,建议直接上18或20的LTS版本。另外npm要能正常访问仓库,如果公司网络有镜像配置,建议提前把registry切到你能用的镜像源,省得后面下载依赖超时。
在终端里先跑两个命令确认:
node -v npm -v如果node命令都找不到,就需要先装Node。装完后有可能你本机有多个Node版本,建议用版本管理器统一管理,装好的工具跑在旧版本上,很容易出现语法不支持或依赖安装失败的情况。我实际遇到过一次:某台机器上node是14,安装过程没报错,但首次执行转换命令时直接报了个关于正则语法的错误,查了半天才发现是Node版本太老。
2.2 三种安装方式的取舍
这个工具有三条常见的安装路径,我在不同机器上都试过,感受差别挺大:
| 安装方式 | 命令 | 适合场景 | 需要注意的点 |
|---|---|---|---|
| npm全局安装 | npm install -g md2wechat-skill | 单人使用、临时执行 | 全局目录权限问题,升级需要手动 |
| 克隆源码 | git clone 仓库地址后全局安装 | 想改源码、二次开发 | 需要保证源码与依赖版本匹配 |
| 二进制/打包版 | 按Release页面下载 | 不想碰Node环境 | 更新和维护由发布方负责 |
我个人最常用的是npm全局安装。因为它够简单,升级时一条命令就能完成:
npm install -g md2wechat-skill安装完成后,验证一下版本号,同时确认命令被正确注册到了PATH中:
md2wechat --version md2wechat --help如果出现"命令找不到"的错误,多半是npm全局bin目录没有加入系统PATH。这时用npm prefix -g查看全局目录,再把对应的bin路径export进shell配置文件,一般就能解决。这个问题在macOS和Linux上偶尔出现,Windows上则要注意是否用了管理员权限安装。
2.3 安装成功后先看一眼目录结构
装完后别急着转换,先搞清楚文件装到了哪里。npm的全局安装通常会把可执行文件放到一个bin目录,实际的功能代码放在lib/node_modules/md2wechat-skill下面。如果你之后想微调内置主题样式,就得去这个目录里找模板文件和预设主题。
我用真实的路径举个例子:在macOS上,全局模块通常在/usr/local/lib/node_modules或者$(npm prefix -g)/lib/node_modules下。打开md2wechat-skill目录,一般能看到bin/、dist/、themes/这几个关键子目录。themes目录里放的就是内置的主题模板,dist目录里是打包后的核心逻辑。这些目录结构看起来琐碎,但后面排查样式问题时要经常和它们打交道。
到这里环境、安装链路就算全部打通了。不过装好工具和跑通一次转换之间,还有一个容易让人迷糊的环节——命令行参数的用法。
3. 安装完成只是开始:从命令行跑通一次最小转换
3.1 构造一份最小测试文档
正式选择配置之前,我强烈建议先用一个极小的Markdown文档跑通全流程,避免带着一堆自定义设置去排错。新建一个test.md:
# 测试标题 这是一段普通正文,包含**加粗**和`行内代码`。 - 列表项一 - 列表项二 ```javascript const hello = "world"; console.log(hello);| 姓名 | 项目 |
|---|---|
| A | 项目X |
注意里面同时放了标题、正文、列表、代码块和表格——这几类元素恰好是公众号排版中问题最集中的部分。如果连这份最小文档都转换正确,后面加再多内容心里也有底。 ### 3.2 最基本的转换命令与输出产物 执行最简单的转换命令: ```bash md2wechat -i test.md -o test_out.html-i指定输入文件,-o指定输出文件。执行完,当前目录下会多出一个test_out.html文件。用浏览器打开这个文件,你会看到一份带样式的排版预览,标题有合适字号和间距,代码块有背景色,表格有边框。这一步看着简单,实际上背后做了不少事情:解析Markdown语法,把标准的HTML结构生成出来,再把对应主题的CSS规则全部转成内联样式,最后输出一份"微信后台不会乱过滤"的HTML。
这里有个关键点是,不要手动去编辑输出的HTML。因为微信后台会丢弃页面顶部的style块,只认标签上的内联style属性,所以工具的职责就是保证所有样式都进了style属性。你一旦自己手改,很容易把这份兼容性破坏掉。
3.3 浏览器预览后粘贴到公众号后台
在浏览器里确认样式正确后,进入公众号后台的图文编辑器,新建一篇图文,直接在正文区域用Ctrl+A全选、Ctrl+V粘贴(macOS上是Command+A、Command+V)刚才的HTML内容。粘贴后你会看到编辑器里出现了和预览基本一致的排版效果,包括代码块背景色和表格样式。这时候再做两件小事:一是检查图片是否都正常,二是看一眼有没有多余的空行。确认没问题就可以继续往下写内容了。
为什么这一步能成立,背后的原理是微信编辑器在粘贴时会保留大部分内联样式,尤其是颜色、字号、边框、背景这类基础属性。而它同时会去掉外部样式表、class引用和部分高级CSS属性,比如flex布局、部分伪元素。工具设计时考虑了这个过滤规则,输出的内联样式基本都是能被保留的属性。
以上算是"最小闭环"跑通了。但从最小闭环到真正用得顺手,中间还隔着一个大头:配置文件。很多人装完工具就直接开始转,结果发现正文宽度、字体、代码高亮色、图片策略都不是自己想要的,只好每次手动加参数。下一节把配置逐项拆开说清楚。
4. 配置文件逐项拆解:主题、代码高亮、图片策略这些到底该怎么填
4.1 配置文件的格式和加载规则
md2wechat-skill支持在项目目录下放一个md2wechat.config.json(也支持.yaml),命令执行时它会自动查找并加载;也可以在执行时用--config手动指定路径:
md2wechat -i test.md -o test_out.html --config ./my_wechat.json如果找不到配置文件,工具会使用内置的默认配置。默认配置重在"能跑",而不是"好用",所以生产级使用一定要显式写配置。
下面是一份我常用的配置示例,先贴出来,后面逐项解释:
{ "title": "未命名文章", "theme": "github", "contentWidth": 677, "fontSize": 15, "lineHeight": 1.8, "enableCodeHighlight": true, "highlightTheme": "atom-one-dark", "imageMode": "local", "imageDir": "./assets", "convertImageToBase64": false, "enableTaskList": true, "enableTable": true, "tableAutoFit": true, "watermark": { "enabled": false, "text": "我的水印" }, "extraCss": "./custom.css" }4.2 主题与内联样式的生成机制
theme字段决定文章整体的视觉风格。内置主题里有偏向技术文章的github风格,有偏新闻阅读的news风格,也有更紧凑的wechat风格。它们之间的差异体现在标题颜色、引用块样式、代码块背景、链接颜色等方面。选择主题时要记住一件事:主题的作用不是生成一个class再让微信去读,而是作为"样式计算器"的输入,最终都会变成每一行标签上的内联style。所以你在工具自带主题里看到的CSS,和你最终在微信里得到的效果,基本是一一对应的。
如果内置主题都不满意,可以通过extraCss引入自己的样式文件。工具会在转换时把自定义CSS合并进主题样式,再统一内联到HTML里。这里有个小陷阱:不是写了extraCss就万事大吉,CSS的优先级、选择器写法都会影响最终效果,我后面踩坑部分会专门讲。
4.3 代码高亮的选择逻辑
代码高亮是技术类公众号的刚需。enableCodeHighlight设为true后,工具会为代码块生成带高亮颜色的HTML。highlightTheme决定具体配色,比如atom-one-dark、github-dark、xcode等。我个人的建议是:如果你的文章经常展示深色背景的终端输出,代码块配色可以选深色主题;但如果整篇文章是浅色背景,深色代码块会显得很突兀,这时候选浅色主题如github或xcode更协调。
代码高亮的原理,本质上是工具把代码按token切分,给不同的token类型赋予不同的颜色,然后同样以内联样式输出,微信编辑器照样能保留这些颜色。所以转换后的HTML里,代码块的每一行可能包含许多带有color、font-weight等内联样式的span标签。这会显著增加输出文件体积,但为了在微信里保持高亮效果,这个代价是值得的。
4.4 图片处理的三种模式:local、remote和base64
图片是公众号文章里的重头戏,也是最容易出问题的环节。imageMode有三个可选值,我分别解释一下:
- local:保留图片的相对路径,输出HTML里img标签的src保持为本地路径。这种模式只适合你在本地预览,一旦粘贴到公众号后台,图片必然裂掉,因为微信根本访问不到你电脑上的文件。
- remote:把图片路径替换成指定的远程URL前缀。适合你已经把图片传到图床或对象存储的场景,src会变成完整的http(s)地址,粘贴过去能正常显示。
- base64:转换时直接把图片内容读取并编码成data URI,嵌进img标签的src里。这种模式最省事,图片跟着HTML走,粘贴后依然能显示,但缺点是文件体积会变大。公众号编辑器对粘贴内容的总大小有限制,图片太多太大时可能会出问题。
我在实际使用中最推荐remote模式,配合自己的图床或对象存储,既稳定又不膨胀文件体积。个人临时用的话base64也完全可以接受,尤其图片数量少、单张小于几百KB时。至于local模式,基本只有调试用。
4.5 其他容易被忽略但影响体验的配置
contentWidth建议固定为677,因为公众号正文区默认宽度就是677像素,设置成这个值可以确保表格、图片不会超出显示范围。fontSize和lineHeight直接关系阅读舒适度,公众号文章一般用15px或16px字号,行高1.7到1.9比较合适,太小了手机上看着吃力。
enableTaskList控制Github风格的任务列表(- [ ] / - [x])是否转成带复选框的HTML,enableTable控制表格是否启用样式化渲染。tableAutoFit则会为表格套上一个宽度限制,避免表格太宽被手机端截断。watermark可以在每段正文后面追加水印文字,虽然我不太喜欢在技术文章里加水印,但如果你有防盗需求,这个功能比复制后再处理要省事得多。
配置写到这一步,工具才算是"我的形状"。接下来可以用一篇真实形态的文章做一次完整实战验证。
5. 实战:把一篇带代码块、表格和图片的文章完整落地到微信
5.1 准备一篇综合性的Markdown文章
我们用一个模拟场景来演示:假设你要发布一篇介绍某跨平台系统的技术文章,包含一个标题、一段背景说明、两段代码(前端和后端)、一个对比表格、若干本地图片引用。这样的文章几乎覆盖了公众号技术文的全部元素类型。
文章开头可能是这样的:
# 某跨平台系统架构解析 本文来自一次内部技术分享的整理。 ## 背景 系统早期在单机环境运行,随着业务量增长,需要拆分为多模块协作架构。 ## 系统组成 - 前端模块:负责交互与展示 - 后端模块:负责业务逻辑与数据存储 - 消息模块:负责模块间通信 ## 核心代码示例 前端请求部分: ```javascript async function fetchData() { const res = await fetch("/api/list"); return res.json(); }后端处理部分:
from flask import Flask, request app = Flask(__name__) @app.route("/api/list") def list_data(): return {"items": []}模块对比
| 模块 | 技术栈 | 部署方式 | 说明 |
|---|---|---|---|
| 前端 | JS框架 | 静态站点 | 面向用户 |
| 后端 | Python | 容器 | 核心逻辑 |
部署架构
注意图片我用了本地相对路径,这正好可以检验我们配置的图片策略。 ### 5.2 使用完整参数执行转换 假设图片已经上传到图床,图片的线上路径前缀是https://cdn.example.com/articles/arch-2024/,配置就写: ```json { "theme": "github", "contentWidth": 677, "fontSize": 16, "lineHeight": 1.8, "enableCodeHighlight": true, "highlightTheme": "github", "imageMode": "remote", "imageRemotePrefix": "https://cdn.example.com/articles/arch-2024/" }执行:
md2wechat -i article.md -o article_out.html --config md2wechat.config.json转换后打开article_out.html,你会看到整篇文章的排版已经成型:标题字号有层级差异,代码块带浅色背景和关键字高亮,表格有边框且宽度被限制在677像素,图片的src已经被替换成完整的线上地址。这一步只要配置对,基本不需要再手工调整。
5.3 从输出到公众号后台的关键动作
在公众号编辑器粘贴之前,我建议先在浏览器里做一次粘贴兼容性自检:选中整篇预览页面的内容,复制,粘贴到一个空白记事本里看看。如果粘贴过去还有清晰的样式,但又不是空白文本,说明内联样式结构是稳定可移植的。然后再正式粘贴到公众号后台。
粘贴完成后,逐一核对下面这个检查清单:
- 第一层标题是否层级清晰,字号明显大于正文
- 代码块背景色和高亮色是否存在
- 表格是否出现横向滚动条或溢出
- 图片是否正常显示,点击大图是否正常
- 段落之间空行是否合理,是否有多余的换行
我个人的经验是,90%的问题都出在图片和表格上,文字和代码块基本一次就过。所以如果时间紧张,优先检查这两个区域。
这篇文章的转换过程属于"顺利剧本"。但现实中,你很可能在第一步就遇到各种奇怪问题。下面把我这两年遇到的高频问题逐个复盘。
6. 踩坑实录:四个高频问题的完整排查链路
先说明一个通用方法论:遇到任何转换结果异常,不要先怀疑工具坏了,而要先做一个二分定位——单独转一个只包含该元素的极简文档,看问题是否仍然存在。如果单独转没问题,那就是文章内容或配置的锅;如果单独转也有问题,那才是工具或环境的锅。这套方法帮我省了很多时间。
6.1 图片粘贴后裂掉,检查后发现是local模式误用
现象:输出HTML在浏览器里看着正常,图片也显示了,但粘贴进公众号后台后全部裂开。
排查过程:先打开输出HTML的源码,看img标签的src长什么样。如果src是./assets/arch.png这类相对路径,或者file://开头的本地路径,那问题几乎就锁定了。这就是配置里imageMode误设成了local,工具没有对你的本地图片做任何处理,只是把相对路径原样保留。本地预览当然正常,但微信服务器拿不到这个文件。
修复方案:把imageMode改为remote或base64。如果图片已经传到图床,用remote并配置imageRemotePrefix;如果只是想本地快速交付,用base64,转换时长会明显增加,但图片全都嵌进HTML了。我见过有人为了省事把所有图片都转成base64塞进一篇一万字的文档,结果HTML文件到了几十兆,粘贴时公众号后台直接卡死。所以base64模式要控制图片数量和大小。
6.2 代码高亮颜色全部丢失,问题出在语言标注和高亮主题上
现象:文章里的代码块有背景色,但代码文本没有任何颜色区分,所有关键字都是黑色。
排查过程:这种表现通常是两种原因之一。第一,highlightTheme指定的主题并不存在,工具回退到plain文本;第二,代码块语言标注写错了,比如javascript写成了js,导致分词器没有正确识别,自然没有token渲染。先在配置里换一个明确的主题名,再检查代码块的语言标注。我在一个旧版本里就遇到过工具只支持完整语言名、不支持别名的情况,把所有js都改成javascript后,高亮立刻恢复正常。
这里还想多提醒一句:如果你在公众号后台看到代码块背景色还在、但没有高亮色,先别急着改工具。公众号编辑器有时会干扰span颜色,你可以试着全选代码块后,手动把文字颜色重新设置一遍再看。我遇到过多次"后台偷吃颜色"的情况,重设一遍颜色就好了。
6.3 表格超出正文宽度,在手机上被截断
现象:桌面端浏览器预览时表格正常,但用手机预览公众号文章,表格右边部分被吃掉,没有任何横向滚动提示。
排查过程:公众号正文的可用宽度是677像素,一旦表格内容列数多或单元格文字长,表格实际宽度就会超过这个值。微信对超出宽度的表格不会按比例缩放,而是直接截断可视区域。检查输出HTML中table标签的宽度相关样式,以及工具是否提供了表格限宽机制。
修复方案:在配置中开启tableAutoFit,工具会给表格外套一层容器并设置max-width: 100%。同时可以把表格单元格的white-space属性设置为normal,让长文本自动换行而不是撑宽单元格。我的经验是,超过五六列的表格在公众号里体验普遍不好,与其硬调样式,不如直接把表拆成几个小表或转成列表,排版上更稳妥。
6.4 自定义extraCss不生效,十有八九是选择器优先级踩坑
现象:在extraCss里写了blockquote { color: red; },但转换后引用块的文字还是原来的颜色。
排查过程:打开输出HTML,找到blockquote标签,看它的style属性长什么样。如果style属性里颜色不是红色,说明你的规则根本没参与计算;如果颜色存在但被其他规则覆盖,那是优先级问题。内联样式的优先级极高,主题在处理时已经把blockquote的样式写进了style属性,你的extraCss又被放置在更早阶段,自然覆盖不了已经内联的值。
修复方案:extraCss的规则要写得更具体。例如,给目标标签额外指定一个class类名,再用类名选择器定位:
.custom-blockquote { color: red; background: #fff8f0; }然后在Markdown中使用引用块时,通过工具提供的"块级自定义类名"语法(比如引用块标记后面跟一个花括号类名)把这个类挂上去。这样内联样式的来源就是你的类规则,主题的默认颜色就不会再打架了。如果工具不支持块级类名,那么更省心的办法是直接改内置主题模板文件,把所有你想自定义的规则替换成自己的,再以定制主题的方式使用。
这四个坑其实有一个共同底层逻辑:微信编辑器只保留内联样式,而工具的一切设计都在围绕"把样式安全地内联化"这件事。理解了这一点,你遇到任何样式相关的诡异问题,都能顺着链路往回找。
7. 让转换融入日常写作流:批量脚本、编辑器联动与后续扩展
跑通单篇文章转换之后,下一个自然而然的需求就是"批量"和"自动化"。毕竟作为写作者,我们希望把时间花在内容本身,而不是工具操作上。
7.1 写一个批量转换脚本
如果你同时维护多个公众号草稿,或者一篇文章有多个章节分散在不同文件里,手动逐条执行命令会非常低效。我写过最朴素的一个批量脚本,对大量文件循环转换:
#!/bin/bash for file in drafts/*.md; do base=$(basename "$file" .md) md2wechat -i "$file" -o "output/${base}.html" --config md2wechat.config.json done脚本思路很简单:遍历drafts目录下的所有Markdown文件,逐一转换到output目录。执行前记得先建好output目录。如果你用的是PowerShell,逻辑也是一样,换个命令语法而已。这种脚本的价值在于,它把重复劳动压缩成一次执行,而且因为配置文件统一,整个项目的输出风格也会保持一致。
7.2 把转换命令挂进编辑器的任务系统
很多朋友日常用VS Code写作。VS Code自带任务系统,可以把转换命令绑定成快捷键,或者绑定在保存时自动执行。在项目根目录的.vscode/tasks.json里加一个任务:
{ "version": "2.0.0", "tasks": [ { "label": "md2wechat: 转换当前文档", "type": "shell", "command": "md2wechat", "args": [ "-i", "${file}", "-o", "${fileDirname}/${fileBasenameNoExtension}.wechat.html", "--config", "./md2wechat.config.json" ], "group": { "kind": "build", "isDefault": true } } ] }配置好之后,按快捷键调出任务列表,选中这个任务,当前打开的Markdown文件就会被转换,并在同目录生成一个以.wechat.html结尾的文件。我试用下来觉得非常顺手,写完后直接转换,浏览器开预览,再复制粘贴,整体一个动作闭环就完成了。
如果你用的是其他笔记软件或编辑器,思路也一样:找到它支持的外部命令或脚本钩子,把md2wechat调用挂上去。核心目标只有一个——不要让"转换"变成一件需要记住的事。
7.3 再往后:版本管理、模板维护和个人样式沉淀
当转换变成日常动作后,我建议把下面几样东西纳入版本管理,不然换个电脑或重新克隆项目后,配置和样式又要重新折腾一遍:
- md2wechat.config.json:统一的项目转换配置
- 自定义主题目录:沉淀个人样式偏好的核心资产
- 批量脚本或编辑器任务配置:保证团队内多人写作时行为一致
把这些放进仓库后,新成员克隆下来装好工具就能直接开始转换,输出的排版风格和团队其他人完全一致,这比在微信后台手把手调样式靠谱得多。
最后再说一个我很看重的细节:图片的长期存储策略。远程图床方案虽然稳定,但一旦服务到期或域名失效,历史文章里的图片会全部裂掉。如果你愿意多花一点成本,在发布时把图片原文件归档到对象存储并绑定自有域名,实际上是最稳妥的。工具本身只负责转换,但图片资产的生命周期管理,始终是公众号内容运营里绕不开的一道题。
我从第一次折腾这个工具到现在,最大的体会是:工具解决的是频率问题——重复的排版动作只要发生一次,就值得用配置和脚本固化下来。md2wechat-skill这种本地转换工具,正是把Markdown写作习惯和公众号发布流程之间那层疲劳感消解掉的关键。如果你也正在为每周排版烦躁,不妨按这篇文章的路径从最小转换开始跑一遍,然后把配置慢慢调成你自己的样子。把转换这件事自动化之后,你写下一篇文章的体验会完全不同。