WeChat HTML to Draft
Overview
Convert an arbitrary HTML article into the restricted HTML dialect that WeChat Official Account (公众号) accepts, then publish it as a draft through the WeChat draft API (draft/add). The skill ships two scripts (a sanitizer and a draft publisher) plus a reference guide documenting WeChat's HTML filtering rules and the draft API.
When to use
- The user hands over an
.htmlfile or raw HTML and asks to "发到公众号 / 转成公众号格式 / 上传草稿箱 / 生成草稿". - The user reports that pasted HTML "lost all styles" in the WeChat editor.
- The user wants to fix WeChat-specific rendering issues (headings not bold, images missing, flex/grid layout collapsing).
Workflow
Step 1 — Sanitize the HTML
Run the sanitizer to strip everything WeChat rejects while keeping inline styles:
python scripts/convert_wechat_html.py <source.html> <output.html>
> Python 调用约定(重要):直接用 PATH 中的 python 命令(managed 版已加入 PATH),不要硬编码 python.exe 绝对路径——在 Git Bash 下绝对路径的 .exe 偶发“No such file or directory”。若 python 不可用,回退到 py 或 uv run python。
The script does these jobs: (1) removes <script>, <head>, <!DOCTYPE>, and <html>/<body> wrappers, preserving all inline style; (2) sanitizes for the 45166 invalid content error — strips <a> hyperlink tags (keep inner text; API rejects <a> even pointing to weixin), removes class/id/data-*/aria-* attributes, strips HTML comments, and deletes invisible Unicode (zero-width space \u200B, BOM \uFEFF, emoji variants \uFE0E/\uFE0F, bidi controls \u202A-\u202E) that slip in via copy-paste and trigger the undocumented 45166 check; (3) automatically injects font-weight:bold and explicit font-size into <h1>/<h2>/<h3> (h2→22px, h3→18px) to defeat WeChat's CSS reset that silently drops default heading bold; (4) converts every <div> into <section> — the single most important fix for "clicking 编辑 loses styles": WeChat's editor forces <div>→<p> on entering edit mode and drops most style, while <section> (the tag Xiuxi/135/official templates use) is kept intact; (5) unifies paragraph spacing — every <p> gets margin:1.2em 0 and every card <section> (with background/border) gets margin:20px 0, eliminating "段前段后距不一致". Pass --no-spacing to keep original margins; (6) by default minifies to a single line (no newlines/indentation) — a secondary guard against edit-mode whitespace normalization dropping inline styles. Pass --no-minify only for debugging. Write the result to a new file (e.g. <原名>_公众号版.html).
> Note on 45166 vs length: the 2万-character / 1MB content limit is a different error code (44016), NOT 45166. If draft/add returns 45166 invalid content, suspect <a> tags, class/id attributes, or invisible Unicode characters in the HTML — not length. This script auto-strips all of them.
Step 1.5 — Handle content images (only when the article has <img>)
WeChat silently filters any image whose src is NOT a WeChat-hosted URL (https://mmbiz.qpic.cn/...). That means base64-embedded, local-path, or external-link images all fail to display in the draft. They must first be uploaded via media/uploadimg to obtain a mmbiz URL, then the src replaced.
If the source HTML contains <img> with non-mmbiz src, run the image prep script before Step 1:
# 用隔离 venv 的 python(已装 Pillow 用于超图压缩;uploadimg 单图上限 ~1MB)
C:/Users/admin/.workbuddy/binaries/python/envs/default/Scripts/python.exe \
scripts/upload_content_images.py <源.html> <图片已替换.html>
This decodes/reads each image, compresses to <950KB (resize to ≤1080px wide, fall back to JPEG), uploads via media/uploadimg, and rewrites src to the mmbiz URL. Then feed <图片已替换.html> into Step 1's convert_wechat_html.py. No manual <img> editing needed.
If the article has no images (only placeholder text like 【封面图片占位】), skip this step.
Step 2 — Verify CSS-reset pitfalls (mostly automated now)
WeChat applies a CSS reset that silently drops browser-default styles. convert_wechat_html.py already auto-fixes the heading-bold issue, so do NOT manually re-add font-weight:bold (that was the earlier mistake — editing by hand missed the h1 and caused conflicts). Still verify the rest against references/wechat-html-guide.md:
- Headings: confirm the script injected bold (check the output HTML contains
font-weight:boldon allh1/h2/h3). No manual edit needed. - Rounded corners: 现代微信支持
border-radius圆角与box-shadow阴影(秀米/135 模板大量使用),圆角不会被过滤。如果看到直角,通常是看了旧的/被手动改过的草稿,而非微信清空。 - Placeholder images: sections such as
【封面图片占位】must be replaced with real<img>using WeChat-hosted URLs (https://mmbiz.qpic.cn/...) obtained after uploading to the WeChat material library. - Layout: avoid
flex/grid; use<table>or stacked<section>blocks with inline styles. - Edit-mode style loss (坑五, highest-frequency recurrence): if the article looks correct in preview but loses styles after clicking "编辑" (entering the rich-text editor), the core cause is
<div>tags — WeChat's editor forces<div>→<p>on entering edit mode and drops most style (so all styling vanishes). The secondary cause is multi-line/indented HTML (whitespace normalization).convert_wechat_html.pynow fixes BOTH automatically: all<div>→<section>, and minify to single line. Verify withgrep -c "<div" <file>(must be 0) andgrep -c $'\n' <file>(must be ~1). If a draft was made from an old div-based file, re-run conversion and regenerate.
Re-run Step 1 only if the source changed.
Step 3 — Collect credentials and a cover image
The draft API requires:
- AppID + AppSecret of a verified service account, injected as environment variables (never written to disk):
WECHAT_APPID,WECHAT_APPSECRET. - A cover image (jpg/png local path) —
thumb_media_idis mandatory for drafts. - IP whitelist: the caller's public IP must be added to 公众号后台「开发 → 基本配置 → IP白名单」, or the call fails with
40164.
If the user has not provided a cover image, ask for one before proceeding. If credentials are missing, instruct the user to export them as env vars (PowerShell: $env:WECHAT_APPID="wx..."; $env:WECHAT_APPSECRET="...").
Step 4 — Create the draft
python scripts/create_wechat_draft.py \
--html <公众号版.html> \
--cover <封面.jpg> \
--author "作者名"
The script fetches access_token, uploads the cover (material/add_material) to obtain thumb_media_id, then calls draft/add. On success it prints the media_id.
强烈建议加 --verify:创建后立即用 draft/get 把草稿读回来,逐项报告 font-weight:bold / border-radius 次数与标签保留情况,用数据证明"接口没有丢样式"。当用户反复反馈"样式不对"时,这是排除"接口丢样式"假设的一锤定音手段。
python scripts/create_wechat_draft.py \
--html <公众号版.html> --cover <封面.jpg> --author "作者名" --verify
Step 5 — Deliver the result
Return the media_id to the user and point them to 公众号后台「内容与互动 → 草稿箱」to preview. Note any remaining placeholder images or square-corner caveats. If styles still look wrong, loop back to Step 2.
Resources
scripts/convert_wechat_html.py— sanitizer: strips<script>/<head>/wrappers, strips<a>/class/id + invisible Unicode (anti-45166), keeps inline styles, auto-injects heading bold, converts<div>→<section, unifies spacing, and minifies to single line.scripts/upload_content_images.py— content-image prep: uploads base64/local/external<img>to WeChat viamedia/uploadimg, compresses >1MB images, rewritessrctommbizURL. Requires Pillow (use the managed venv python).scripts/create_wechat_draft.py— draft publisher: token → upload cover →draft/add. Pure stdlib, no pip needed.references/wechat-html-guide.md— full WeChat HTML filtering rules, CSS-reset pitfalls, draft API details, and a security checklist.
Security checklist
- Never persist
AppSecretto files, memory, or git. Use env vars only, and suggest rotating the secret after any exposure. - Verify the account is a verified service account with draft API permission before calling.
- On failure, triage in order: IP whitelist (
40164) → account verification (48001) → cover image format/size.