从默认 Logo 到每期专属封面
我每两天发一期 AI 资讯日报。推微信草稿箱时需要封面图——但很长一段时间里,那个封面就是一张默认的 logo 图片,所有日报共用同一张。
也不是没想过做专属封面。日报标题、日期、摘要都是现成的,每期手动做图太麻烦。用 AI 文生图?成本倒不高,但每 48 小时调一次文生图 API,总感觉太重了。
我想要的是:读 Markdown frontmatter → 套模板 → 出图 → 推微信,全自动,零人工介入。
最后做成了一套 Swiss IKB Blue 风格方案:法国艺术家 Yves Klein 的群青色(#002FA7)作为品牌色,白底排版,21:9 主封面 + 1:1 方封面各一张,Playwright 渲染 HTML 截图,PIL 压缩后自动传给发布流程。
架构:四步走
整套流程拆成四个环节:
- 从日报 Markdown 文件读取 frontmatter(title, date, description)
- 替换到预设的 HTML 模板中(4 个占位符)
- Playwright 加载本地 HTML,截图两个指定容器
- PIL 把 PNG 压缩为 JPEG,输出到 covers 目录
python3 generate-digest-cover.py digests/2026-06-17-ai-digest.md
跑完这条命令,covers/ 下就有 ai-digest-2026-06-17.jpg(21:9)和 ai-digest-2026-06-17-1x1.jpg(1:1)两张图。

如果脚本失败(网络超时、字体加载失败),降级到纯 PIL 生成的默认封面——画面上有品牌色底色 + 标题 + 日期,虽然不如专属封面精致,但至少比空着强。
HTML 模板:Swiss 风格的设计系统
模板是整套方案的核心。色调、字体、布局三个维度:
色调:纸白背景(#fafaf8)+ IKB Blue 强调色(#002FA7)+ 两种灰色辅助(#d4d4d2, #737373)。
字体:英文用 Inter,中文用 Noto Sans SC,数字/代码用 IBM Plex Mono。全部通过 Google Fonts 在线加载。
尺寸:21:9 版(2100×900)左右两栏——左栏标题 + 日期 + 描述,右栏抽象几何图形。1:1 方封面(1080×1080)纯色背景 + 居中文字。
模板里留了 4 个占位符:__MAIN_TITLE__、__DESCRIPTION__、__DATE_LABEL__、__TITLE_SHORT__。脚本读取 Markdown frontmatter 后逐项替换。
设计时有个取舍:字体文件要不要打包进模板?不打包 → 依赖 Google Fonts 在线加载,需要网络 + 等待时间。打包 → 模板从 10KB 膨胀到 500KB+。我选了在线加载方案,后面会说它带来了什么问题。

Playwright 渲染的关键参数
Playwright 的配置有几个关键点:
page = await browser.new_page(viewport={"width": 2400, "height": 1600})
await page.goto(f"file://{tmp_path}", wait_until="networkidle", timeout=30000)
await asyncio.sleep(4) # 等字体加载
- Viewport 比实际截图尺寸大一些(2400×1600),避免截不全
- 用
file://协议加载本地 HTML,不走 HTTP 服务器 wait_until="networkidle"确保 Google Fonts 下载完成- 额外
sleep(4)是因为字体加载后的渲染还需要时间
截图后还有一个步骤:PIL 验证实际尺寸。
from PIL import Image
img = Image.open("temp.png")
if img.size != (2100, 900):
img = img.crop((0, 0, 2100, 900))
img.convert("RGB").save("cover.jpg", "JPEG", quality=90, optimize=True)
quality=90 的 JPEG 压缩下来,21:9 主封面约 100KB,远低于微信的 2MB 限制。
踩了五个坑
1. 中文字体渲染成方框
SVG 生成封面依赖系统字体路径。换一台机器,或者同一台机器上没装对应的中文字体,渲染出来的中文就是一排方框。
解决:PIL 方案硬编码字体路径(NotoSansCJK-Bold.ttc),Playwright 方案用 Google Fonts CDN。两个方案各自指向明确的字体文件,不依赖系统回退。
2. Google Fonts 首次加载慢
截早了字体没加载完,文字渲染成 fallback 字体。截晚了浪费几秒。
解决:wait_until="networkidle" + asyncio.sleep(4)。首次加载约 4 秒,后续由于浏览器缓存,速度快很多。如果部署在无外网环境,需要把字体文件 base64 内联到 HTML 中——模板会膨胀,但能离线工作。
3. 截图尺寸不对
Playwright 的 page.screenshot() 默认截取整个页面。如果 HTML 渲染高度和预期不符,可能多出白边或缺少底部内容。
解决:截图后用 PIL 检查尺寸,不匹配就裁剪。或者用 Playwright 的 element.screenshot() 定位到具体容器元素。
4. 短标题在方封面上放不下
日报标题 “2026年6月17日 AI编程工具/Agent资讯精选” 共 17 个字。方封面空间有限,硬塞进去要么字体小到看不清,要么超出边界被裁掉。
解决:先按标点分割取前半句,超过 12 字再截取前 12 字。17 字的标题变成 “2026年6月17日”,刚好放下。
5. 旧封面文件不同步
替换默认封面后,发布脚本跑在另一台服务器上,旧的封面文件还在那里。本地预览是新封面,推送出去是旧封面。
解决:新封面生成后用 rsync 同步到发布服务器。或者更好的方案——把封面生成脚本部署到发布服务器上一起跑,不需要跨机同步。
集成到发布流程
cron 定时任务每 2 天执行一次:
# 先生成封面
python3 generate-digest-cover.py digests/2026-06-17-ai-digest.md
# 再推微信(带封面)
python3 publish-wechat.py digests/2026-06-17-ai-digest.md \
--cover covers/ai-digest-2026-06-17.jpg
两步之间不需要人工检查。如果第一步失败,第二步不传 --cover,自动降级到默认 logo 封面。
适合谁
如果你的项目也有定时发布需求,封面图一直是 logo 凑合:
- 内容简单:只需从标题/描述自动生成封面 → 直接拿这个模板改配色
- 内容复杂:需要每期不同视觉风格 → HTML 模板多准备几套,轮换使用
- 不想碰 Playwright:纯 PIL 方案也可以,排版灵活度不如 HTML,但零浏览器依赖
总结
封面自动化的核心不是”用什么工具渲染”,而是”怎么把渲染放进流程”。
PIL 和 Playwright 都能出图,区别在维护层面:PIL 改排版要改 Python 代码,Playwright 渲染 HTML 改排版就是改 CSS。对经常调封面的场景,后者省事得多——编辑 CSS 比修改 Python 门槛低,甚至可以交给非技术人员维护。
至于 Swiss IKB Blue 风格本身,选择它不是因为它最花哨——是因为它足够简单。9 个 CSS 变量控制全部配色,4 个占位符控制全部文字内容。这种窄接口的设计对自动化流程来说,比花哨的模板可靠得多。