把 HTML 网页转为公众号图文,保留原网页的版面。
- 原网页中随屏宽缩放的部分,在读者的手机上同样按屏宽等比缩放。
- 网页中的 SVG 动画,粘贴后照常播放。
- 手机上没有的字体可以转为图形(转曲),手机上显示的字形与原网页相同。
- 网页文件夹中的图片,粘贴后由微信从本工具下载(需要先设置取图地址)。
- 复制之前,可以在模拟的手机页面中预览转换结果,包括深色模式;本工具还会在多个屏宽下把转换结果与原网页逐元素比对,列出需要处理的问题。
第一次使用请从快速上手开始。自己写网页或让 AI 生成网页时,能够保留的效果列出了粘贴后与原网页相同的效果。转换的原理、安全措施与开发说明见技术说明。
需要 Node.js 26 或更新版本,以及 Chrome 或 Edge 146 或更新版本。浏览器没有安装在默认位置时,用环境变量 CHROME_PATH 指定它的可执行文件。
- 安装 Node.js,以及 Chrome 或 Edge。
- 用 git clone 下载本仓库;不用 git 时,在本页上方点击“Code”,再选择“Download ZIP”,下载后解压。
- 在本工具的文件夹中打开命令提示符(在资源管理器的地址栏中输入 cmd 并回车),运行
npm ci安装依赖。只需要运行一次。
以下以 Windows 为例;其他系统用 node server.ts <网页文件夹> 代替 serve.cmd。
- 把网页文件夹(或其中的 HTML 文件)拖放到
serve.cmd上。浏览器会打开http://127.0.0.1:38961/并开始转换;第一次启动时要先构建页面,需要稍等片刻。默认浏览器不是 Chrome 或 Edge 时,把这个地址复制到 Chrome 或 Edge 中打开。也可以先双击serve.cmd,再在页面中拖入文件夹或粘贴路径。 - 核对“原网页”与“公众号文章(模拟)”两栏预览,以及侧栏中的“检查结果”。“必须处理”中的问题如不处理,草稿可能无法保存。
- 网页中有本地图片时,在“取图地址”中设置微信服务器访问本机的地址,见本地图片与取图地址。
- 点击“复制到剪贴板”,在公众号后台编辑页面的正文中按 Ctrl+V。如果弹出“内容结构检测”提示,见常见问题。
- 页面显示微信已下载全部图片后,点击两次“保存为草稿”(提示已保存后再点击一次)。只保存一次时,后台的“预览”中不显示图片。
- 用后台的“预览”功能在手机上检查。
网页引用的本地图片与样式表要与 HTML 放在同一个文件夹中(可以有子文件夹),文件夹以外的本地文件不会被读取。网络上的图片与样式表(以 http 开头的地址)可以直接引用。文件夹中有多个 HTML 文件时,把要转换的文件命名为 index.html,或直接给出它的路径。用路径打开时,原文件修改后会自动重新转换;拖入的文件夹修改后需要重新拖入。
前一张表中的效果,粘贴后与原网页相同;后一张表中的效果无法保留,或者只能近似。让 AI 生成网页时,可以把这两张表一并交给它,作为生成网页的要求。
| 能够保留 | 范围 |
|---|---|
| 样式表中的样式 | 包括外链样式表、@import、CSS 变量、::before/::after;@media 规则按所选屏宽对应的正文宽度判断是否生效 |
| 定位与层叠 | 绝对定位、相对定位、transform,多数情况下的 z-index |
| 等比缩放 | cqw、vw 等随屏宽变化的尺寸 |
| SVG | <use> 与 <symbol>;渐变、滤镜、蒙版等效果(连同用到它们的图形绘制为位图,图形只做整体的移动、旋转、缩放、显隐或透明度变化时,动画照常播放);SVG 内的 <image> |
| SVG 动画 | SMIL 动画;SVG 中的 CSS 动画,包括写在最外层 svg 上的透明度、显隐、填充与整体的移动、旋转、缩放;同一元素上的点击、触摸触发 |
| 其他 | 不换行的文字、空的色块与线条 |
| 无法保留或只能近似 | 结果 |
|---|---|
| HTML 元素上的 CSS 动画 | 停留在没有动画时的状态 |
| 交互样式 | :hover、:focus 等不生效 |
| @container 规则 | 转换时忽略,相关样式缺失 |
| 互动元素 | 表单、音视频、iframe、canvas 会被删除,需要用后台自带的组件重新插入 |
| 站外链接 | 公众号正文只允许链接公众号文章,站外链接改为普通文字 |
| APNG 与动图 WebP | 公众号文章中只显示第一帧:APNG 转存后,读者拿到的是只有第一帧的 PNG;动图 WebP 转存为静态的 JPEG。动图只有 GIF 能保留动画 |
| position: fixed | 随正文滚动 |
| 最外层 svg 上的部分 CSS 动画 | 动画改变 svg 的尺寸或显示方式,或者 svg 自身带有背景、边框、阴影、滤镜、变换时,不会播放;把动画移到内层的 <g> 上即可 |
| 跨元素的 SVG 动画触发 | 由其他元素按 id 触发或衔接的动画(如 begin="btn.click")不会播放 |
| 带动画的 SVG 图形上的渐变与图案 | 图形自身的动画改变形状、颜色等属性,或外层元素的动画改变颜色、描边时,渐变改为各色标的平均色,图案改为备用颜色 |
| SVG 渐变与图案中的动画 | 渐变、图案中的动画(如色标颜色的变化)不会播放,用到这些渐变、图案的图形按初始外观绘制为位图 |
| 部分 SVG 动画 | <animate> 与 <set> 只有改变 x、y、width、height、r、points、d、fill、opacity、stroke-width、stroke-dashoffset、visibility 或 transform 时,才会被公众号保留;<animateTransform> 与 <animateMotion> 不受此限制。改变其他属性(如 stroke、rx、font-size)的动画,除少数可以改写为上述属性的以外,保存草稿时会被公众号删除 |
| 由固定值与比例值合成的尺寸 | 相当于 calc() 的尺寸只在所选屏宽下完全精确,其他屏宽下按比例近似 |
| iOS 16 以前的 iPhone | 不支持随屏宽等比缩放的尺寸(cqw),版面与原网页不同 |
粘贴后,公众号后台把正文中的图片地址交给微信服务器,由微信服务器下载图片、转存到微信的服务器上,所以网页文件夹中的图片必须能从外网访问。本工具在“取图地址”的本机端口上提供这些图片,关闭本工具后即停止提供。网页中只有网络图片(以 http 开头的地址)时,微信服务器直接从原网址下载,不需要设置取图地址。
| 设置项 | 含义 |
|---|---|
| 本机端口 | 本工具提供图片的端口,默认 38960 |
| 公网地址 | 微信服务器访问的域名或 IP。留空时自动检测当前网络的公网 IP |
| 公网端口 | 微信服务器访问的端口。留空时,协议为 http 则与本机端口相同,为 https 则使用 443 |
| 协议 | http 或 https。本工具只提供 http,https 由内网穿透工具或反向代理提供;自动检测公网 IP 时只能使用 http |
| 情形 | 做法 |
|---|---|
| 宽带有公网 IP | 在路由器上把“公网端口”转发到运行本工具的电脑的“本机端口”;“公网地址”可以留空。Windows 防火墙弹出提示时,允许 Node.js 在当前网络中通信 |
| 没有公网 IP | 用 frp、cpolar 等内网穿透工具把“本机端口”映射到外网,再把内网穿透工具提供的地址、端口与协议填入对应的设置项。以 http:// 或 https:// 开头的完整地址(如 https://abc.example.com)可以整体粘贴到“公网地址”,将自动拆分为协议、公网地址与公网端口 |
保存设置后,本工具访问一次取图地址,检测能否访问;页面打开期间每 5 分钟重新检测。检测从局域网内发起,结果不一定准确,以粘贴后显示的已下载图片数为准。修改设置后,请删除已粘贴的正文,重新复制、粘贴。图片全部下载完成后,正文中的图片地址已替换为微信的地址,此时可以关闭本工具。
微信服务器下载每张图片最多等待约 0.5 秒,超时后隔 0.5 秒重试,连续三次超时即放弃,编辑器中这张图片的位置显示“系统繁忙”,草稿无法保存。能否在时限内下载完,取决于图片大小、同时下载的张数与网络的上传速度;通过内网穿透提供图片时,下载速度还受内网穿透服务的带宽限制,通常更慢。因此超过“单张上限”(默认 1000 KB,在“复制到剪贴板”下方设置;使用内网穿透时可以调得更低)的本地图片会列入检查结果;粘贴后连续三次超时的图片,会在“复制到剪贴板”下方列出。
“压缩大图”默认勾选。公众号文章中的图片最宽为 1080px,更宽的图片由微信自己缩小并重新压缩,失真明显。勾选后,宽度大于 1080px 的图片缩小到 1080px 宽,文件大小超出上限的压缩到上限以内,并尽可能保留画质;其余图片不做处理。处理的是提供给微信服务器的副本,网页文件夹中的原文件不变。动图、SVG 与 BMP 不处理。
压缩的具体做法
实测 6 张 1100–1200px 宽的照片,以 Lanczos 缩到 1080px 的结果为参照,微信给的 1080px 版峰值信噪比为 33–38 dB;1080px 宽的图片微信只重新压缩、不缩小,为 41.6 dB。因此本工具用 Lanczos 算法缩小,再按以下顺序压缩:
- 从高到低尝试画质,取不超过上限的最高一档(原图本来就不超过上限时,也尽量不超过原图的大小):照片存为 JPEG(质量 92 至 70);文字、图标、截图一类的图形存为 PNG,或者存为色度不降采样(4:4:4)的 JPEG;有透明部分的图片存为 PNG。
- 最低一档仍超过上限时,尺寸不变,继续降低画质(JPEG 最低到质量 60);还超过上限时,使用其中最小的结果,并在检查结果中列出。
电脑与手机自带的字体不同。原网页使用手机上没有的字体时,手机会改用其他字体显示;用 @font-face 引用的网页字体,在公众号正文中无法加载。“字体”卡片把文中用到的字体、字重与字号的组合逐行列出:
| 选项 | 效果 |
|---|---|
| 勾选 | 转为 SVG 路径(转曲):保证手机上的字形与原网页相同,但在公众号后台无法再编辑;需要修改时,修改原网页后重新复制、粘贴 |
| 不勾选 | 保留为文字:按“备用字体规则”在字体栈中添加 iOS 与安卓自带的相近字体;手机上没有原网页所用的字体时,显示为这些备用字体 |
- 表中的字号是文字在屏宽 375px 的 iPhone 上(正文宽度 335px)显示的大小,可以按字号批量勾选。勾选按行记录:之后转换的文章中出现相同的字体、字重与字号组合时,沿用这一勾选。
- 同一段落的文字全部转为 SVG 路径或全部保留为文字,由段落中大部分文字所在的行是否勾选决定。某一行的结果与勾选不一致时,行下方显示说明,鼠标悬停可查看转为路径的字数,以及其余的字保留为文字的原因。
- 网页字体可以使用网页文件夹中的字体文件、data: 内嵌的字体,以及允许跨域读取的站外字体(如 Google Fonts)。字体文件无法读取时,用到它的段落保留为文字。
- 竖排(writing-mode: vertical-rl、vertical-lr)的文字与从右到左书写的文字(如阿拉伯文、希伯来文)也可以转为 SVG 路径。字体中以彩色绘制的字(如表情符号)不转换,所在段落保留为文字。
- 转为 SVG 路径的字,以及 SVG 中与渐变、滤镜等效果一起绘制为位图的字,都变为图形,读者无法选中、复制。本工具默认在这些字上方放置一层内容相同的透明文字,读者仍可选中、复制(沿曲线排列的文字除外)。不需要时,在“字体”卡片中取消勾选“允许选中、复制转为路径的字”。
字形的生成方式与笔画粗细
- 字形由浏览器绘制:本工具另外启动一个不显示窗口的浏览器,按原网页的样式重新排出要转换的段落,打印为 PDF,再从 PDF 中读取每个字的轮廓与位置。因此系统字体、网页字体、连字、可变字体、小型大写字母等,都与原网页中的字形相同。转换前,本工具逐字核对这个浏览器排出的位置与当前页面中的位置,对不上的段落保留为文字;当前浏览器不是 Chrome 或 Edge 时,通常会出现这种情况。
- 宋体等没有粗体、斜体字形的字体,屏幕上的粗体、斜体由系统模拟生成,打印出的 PDF 中没有这一效果,本工具按实测的加粗量与倾斜角度补上,笔画粗细与屏幕上的可能相差约 1 个物理像素。
- Windows 上的 Chrome、Edge 绘制文字时按字色加重笔画边缘的像素,浅色字加得更多;填充同样的轮廓时不加。为了不让转为 SVG 路径的字比原网页细,本工具在每条路径下方垫一层同色、不偏移、不模糊的阴影,加重的程度按 Windows 上的 Chrome 实测确定:在 1 至 3 倍屏上,转换前后笔画墨量的中位数相差不超过 3%。阴影按物理像素加重,屏幕越清晰,加重的量越小。半透明的字不加阴影。
- iPhone 上的微信使用 WebKit 内核,SVG 内部图形上的 CSS 滤镜不生效,看不到这层阴影。安卓上的微信使用与 Chrome 相同的内核,会绘制阴影,但它绘制文字时加重得比 Windows 上少,转为路径的字可能比原网页略粗:在 3 倍屏上,阴影使笔画墨量增加约 2% 至 6%。手机上的效果未经实测。
“公众号文章(模拟)”一栏模拟在手机微信中打开公众号文章时看到的界面,包括手机外壳、标题与作者行、底栏,以及正文区左右各 20px 的页边距。正文套用公众号文章页面的样式,并运行该页面中调整图片尺寸的脚本。原网页有底色时,两侧页边距仍显示公众号文章页面自身的底色,这一底色无法通过正文的样式改变。
深色模式由微信公众平台前端团队开源的 mp-darkmode 生成。经 2026-10-22 核对,本工具所用的 mp-darkmode 版本与调用参数都与公众号文章页面相同,正文的深色效果与公众号生成的一致。
预览与读者手机上的显示效果可能在两方面不同:
- 预览由电脑上的浏览器渲染,使用电脑上安装的字体,字形、字宽与断行可能与手机上不同。
- 预览显示的是转换后、粘贴进公众号之前的内容。正文粘贴进公众号并保存时,编辑器与服务器会删除或改动其中的部分 HTML 元素、属性与 CSS 样式。本工具已通过实测掌握大部分删改规则,转换时改用效果相同、能够保留下来的代码;少数没有测到的规则,以及公众号日后对规则的修改,仍可能使保存下来的正文与预览不一致。
因此粘贴并保存后,请用公众号后台的“预览”在手机上确认。
检查结果中各元素的位置、尺寸“与原网页相差均不超过 1.5px”,表示转换本身没有改变版面:原网页与转换结果用电脑上的同一套字体,在多种正文宽度下分别渲染,逐个比对元素的位置与尺寸。读者手机上的文字换用其他字体后,字宽与断行可能随之改变,这项比对无法发现这类变化,见“字体”一节。
| 现象 | 原因与处理 |
|---|---|
| 粘贴时弹出“内容结构检测”,其中只列出“文字对齐异常” | 这一项针对的是 start、end 两种对齐方式,目前的微信客户端与浏览器几乎都已支持。点击“继续插入”即可 |
| 图片的位置显示“系统繁忙”,草稿无法保存 | 微信服务器连续三次没能在时限内下载这张图片。调低“单张上限”,或改善网络的上传速度,然后删除已粘贴的正文,重新复制、粘贴。见“本地图片与取图地址” |
| 第一次保存草稿后,后台的“预览”中不显示图片 | 第一次保存时,编辑器还没有为图片添加尺寸信息。再保存一次即可 |
| 手机上的字形、断行与原网页不同 | 手机上没有原网页所用的字体,换用了其他字体。在“字体”卡片中把这些字转为 SVG 路径,见“字体” |
| iPhone 上的版面与原网页不同 | iOS 16 以前的 iPhone 不支持随屏宽等比缩放的尺寸(cqw),版面与原网页不同 |
| 保存后在手机上看到的与本工具的预览不一致 | 公众号保存正文时删除或改动了部分内容,其中有本工具没有测到或公众号后来修改的规则。请在 Issues 中反馈,并附上能重现问题的 HTML 片段 |
公众号后台的编辑器保存正文时,会删除 <style>、class、position、CSS 变量、calc()、SVG 的 id 等内容,只留下 HTML 与 CSS 的一个子集,因此直接复制、粘贴网页,版面会错乱。html2gzh 只用这个子集重新实现原网页,使粘贴后的显示效果与原网页尽可能一致。主要的做法有:
- 样式表中的样式逐个写入元素的 style 属性
- 绝对定位改写为浮动与位移
- 随屏宽变化的尺寸改写为 cqw
- SVG 与 SVG 动画改写为公众号能够保存的结构;依赖 id 的效果连同用到它们的图形绘制为位图
- 可选:把指定的文字转为 SVG 路径
转换后,本工具在多个屏宽下把转换结果与原网页逐元素比对,列出不一致的地方。
- 页面端口只监听 127.0.0.1,并核对请求头中的 Host;改动状态的请求(打开文章、上传图片、修改取图地址等)只接受本页面发出的请求。
- “取图地址”中的本机端口对外网开放(微信服务器需要访问),不设认证,只提供已载入文章中引用的图片;文件名为内容的哈希值,外部难以猜测。关闭本工具后即停止提供。
- 文章中的 SVG 带有脚本时,直接打开它的图片地址,脚本也不会运行(响应带有 CSP sandbox)。
- 深色模式的转换在只允许脚本、与本页不同源的隐藏 iframe 中进行,文章内容无法访问本工具的接口。
- 字形服务的无头浏览器拦截所有网络请求,只返回已登记的字体文件。
- 为了确定公网 IP,本工具在启动时以及页面打开期间每 5 分钟访问几个查询 IP 的网站(myip.ipip.net、ip.sb 等,列表见
lib/host.ts)。外链样式表与站外字体由浏览器直接从原网址读取。
设置保存在本工具目录下的 settings.json 中。命令行参数 --port N 指定取图用的本机端口,优先于页面中的设置,此时页面端口为 N+1,也可以用 --ui 单独指定;默认页面端口为 38961。--no-open 启动时不打开浏览器。
服务端与转换器用 TypeScript 编写,只用可以直接擦除的语法:Node 直接运行 .ts 文件,不需要编译。页面使用 Vue 3、Naive UI 与 vue-i18n,由 Vite 构建到 ui/dist;启动时如果页面或转换器的源码比构建结果新,会先重新构建。也可以手动运行 npm run build。代码格式由 Prettier 统一:npm run format 格式化。npm run check 依次检查格式、对服务端与页面做类型检查(TypeScript 严格模式)、用 oxlint 做带类型信息的静态检查(如未处理的 Promise),并用 knip 查找未使用的文件、导出与依赖。
页面上的文字集中在 ui/src/locales/zh-CN.json。转换器与接口错误只返回文案的键与参数,由页面组成句子;服务端在命令行中输出的信息写在服务端代码中。
| 目录 | 内容 |
|---|---|
ui/convert/ |
转换器,在浏览器中运行;svg/ 是 SVG 与 SVG 动画的改写,webfonts.ts 确定原网页实际使用的网页字体,types.ts 是转换器、页面与服务端共用的数据结构 |
ui/src/ |
页面:store.ts 是状态与操作,components/ 是各卡片与预览区,phone.ts 模拟公众号文章页面,locales/ 是文案 |
server.ts |
本地服务的入口:绑定页面端口与取图用的本机端口 |
lib/ |
服务端:设置、文件登记、取图地址检测、页面接口;glyphs.ts 提供文字转为 SVG 路径所需的字形(无头浏览器排出段落并打印为 PDF,MuPDF 读取其中的字形轮廓,fontkit 读取字体的字重与斜体信息),shrink.ts 压缩大图 |
test/ |
端到端测试与测试页面 |
tools/ |
用于排查问题的脚本,用法写在各文件开头 |
docs/ |
README 的配图,以及截图用的示例网页 docs/demo |
npm test 先构建页面,再用 test/fixture、test/fixture-svg 与 test/fixture-webfont 运行端到端测试:启动服务、转换、检查转换结果、在多个宽度下与原网页逐元素与逐像素比对、动画采样、字体表与公众号文章页面的模拟、取图地址、下载进度、端口安全。网页字体测试页面使用的字体文件,在测试时从 Windows 自带字体截取子集生成,不随仓库分发。也可以用自己的文章运行:node test/e2e.mjs <文章路径> [--svg]。npm test 需要在 Windows 10/11 上运行:测试页面按 Windows 自带的字体校准,在其他系统上比对结果会有出入,网页字体测试页面也无法生成。
公众号在服务端过滤保存的正文,规则可能变化。tools/wxprobe.mjs 生成一页带编号的测试内容,粘贴到测试草稿并保存后,与保存结果逐项比对,用于复核转换器所依据的规则;它的 diff 子命令比对整份转换结果。tools/wxshot.mjs 把公众号预览页与原网页逐像素比对,可以指定动画时刻与点击。公众号文章页面内嵌的深色模式算法,经 2026-10-22 核对,与 npm 上发布的 mp-darkmode 1.2.3-beta.8 相同,本项目安装的是同一版本。如公众号将来更新了这个算法,可以用 tools/wxdarkmode.mjs 查找对应的开源版本;它也会列出页面调用算法时传入的参数,可与 ui/src/phone.ts 中的 DARK_OPTIONS 对照。
问题与建议请在 Issues 中反馈。

