9.8 KiB
PDF 输出(可选)
把交付的单页 HTML(article/article.html)转成 PDF。这是 Phase 8 Delivery 里的可选步骤,
由 Checkpoint 3 用户选 "通过 · 同时导出 HTML + PDF" 时触发;不选则不动。
HTML 仍然是主交付物:它能离线打开、可分享、可在浏览器里完整体验 Raw 交互。PDF 是给"需要 归档 / 打印 / 邮件附件 / 不联网阅读"场景的补充,Raw 交互在 PDF 里只能渲染为初始态。
快速用法
# 工作区根目录
npm run html # 先确保有 article/article.html
bash <path-to-beautiful-article>/scripts/html-to-pdf.sh # 默认 article/article.html → article/article.pdf
bash <path-to-beautiful-article>/scripts/html-to-pdf.sh in.html out.pdf # 自定义路径
也可以把这个 bash 调用放到工作区 package.json 的 scripts 里,让用户 npm run pdf 就跑
(路径是用户机器上 Skill 的绝对路径,不在脚手架模板里固化,避免硬编码)。
前提条件
本机已装 chromium-family 浏览器之一(脚本会自动探测,按下列顺序):
chromium / chromium-browser / google-chrome / google-chrome-stable / chrome
brave-browser / microsoft-edge
/Applications/Google Chrome.app/...
/Applications/Chromium.app/...
/Applications/Microsoft Edge.app/...
/Applications/Brave Browser.app/...
/Applications/Arc.app/...
/usr/bin/chromium / /snap/bin/chromium
找不到任何浏览器 → 脚本不会爆,会给出回退指引:注入了打印 CSS 的临时 HTML 路径会被
打印出来,用户可以用任意浏览器手动 Cmd+P / Ctrl+P → "另存为 PDF"。
不依赖 Node / npm 包 / puppeteer / playwright / weasyprint —— 故意只用系统已装的浏览器, 零环境配置。
设计原理(重要 · 调样式前先读)
1. 为什么要注入 print CSS(不是改 reacticle)
reacticle 的 TOC 在桌面是左右栅格(.ra-article-layout--with-toc 是 display: grid,
两列:TOC | 文章),在 mobile(≤999px)才塌成单列 display: block。
PDF 阅读习惯是上下排布(TOC 先 / 正文后),跟 mobile 体验对齐。最干净的实现:在 PDF
生成时注入一段 @media print CSS,强制 display: block,等价于复用 mobile 分支。
这样:
- 不动 reacticle:任何版本的 reacticle 都能用这个脚本生成合理 PDF。
- 不动用户的 Article.tsx:用户的源码完全不受影响。
- CSS 只在打印生效:浏览器里看 HTML 还是左右栅格。
2. 注入了哪几条规则
CSS 抽到了独立文件 scripts/pdf-print-overrides.css(与脚本同目录),分三组:
A · TOC 排版(让 TOC 上 / 正文下)
A1) .ra-article-layout--with-toc { display: block } → 塌成单列
A2) .ra-toc { position: static; page-break-after: always } → 解 sticky + 独占首页
A3) .ra-toc__list { column-count: 2 } → 长 TOC 双列省纸
A4) .ra-toc__item { break-inside: avoid } → TOC 项不被列间撕开
A5) .ra-article-layout--with-toc > .ra-article → 正文在 TOC 后自然流
A6) .ra-toc a / .ra-article a { text-decoration: none } → 关闭打印链接下划线
B · 分页行为(修长文章的大块空白页)
B1) .ra-section / .ra-subsection { break-inside: auto !important }
→ 撤销 reacticle print.css 里的 break-inside: avoid-page。
对长 Section 来说,那条规则会把整节推到下一页、留前一页大半空白。
B2) h1-h4 + .ra-section__head + .ra-subsection__head { break-after: avoid }
→ 标题不被孤儿化(不会孤零零卡在页底,下面是空白)
B3) .ra-hero / .ra-lead / .ra-conclusion { break-inside: avoid }
→ 短的开场 / 收束块原子化,不撕开
B4) p / li / blockquote { orphans: 3; widows: 3 }
→ 段落不留 1-2 行寡行
B5) figure / .ra-table / .ra-codeblock / .ra-formula / .ra-image / .ra-raw
{ break-inside: avoid }
→ 图表、表格、代码块、Raw 块尽量整块;过高时浏览器仍会自动 fallback 分页
C · 封面(若文章有 3:4 封面,让它独占 PDF 首页)
C1) .ra-cover { break-after: always; break-inside: avoid }
→ 屏幕和 PDF 都保持 3:4 书封;打印时 TOC 从第二页开始
→ 详见 references/cover.md
完整带注释的 CSS 直接看 scripts/pdf-print-overrides.css。要调样式(比如换页面 break 策略
/ 改双列阈值 / 加打印水印),直接改这个文件就行,不用动 bash 脚本。
为什么 CSS 是独立文件:macOS 自带 BSD awk 对
-v inject="$INLINE_CSS"这种多行字符串 报newline in string错(GNU awk 没事)。把 CSS 抽成文件,awk 用getline < file读取, 两个 awk 都吃得下。顺带也让 CSS 变成可独立编辑 / lint / diff 的真正资源。
reacticle 自带的 print.css 仍然生效(白底黑字 / 隐藏 export bar / break-inside: avoid
等),本脚本只补 TOC 排版那一块。
3. 渲染流程
article.html ──── awk 注入 print CSS ──── /tmp/article-print.html
│
▼
探测的浏览器 --headless --print-to-pdf
│
▼
article.pdf
Chrome 标志:
--headless=new(旧版 fallback 到--headless):无窗口模式。--no-pdf-header-footer/--print-to-pdf-no-header:去掉浏览器自带的 URL / 日期 / 页码 (颠倒了 colophon 的角色)。--virtual-time-budget=5000:给页面 JS 5 秒初始化时间,让 Raw 组件、KaTeX、Prism 等 渲染完再截屏。--hide-scrollbars/--disable-gpu/--no-sandbox:清洁渲染。
4. Raw 交互在 PDF 里会怎样?
PDF 是静态文档,所有 Raw 交互(滑块、按钮、动画、canvas、视频)只能渲染初始状态。 设计建议:
- interactive-explainer 类型:PDF 价值有限(核心是"操作"),用户可在 Plan 阶段不开 PDF 导出。
- 其它类型:Raw 通常是辅助图解(流程图 / SVG / 趋势图),初始态已经够看,PDF 没问题。
- Raw 内的内容若依赖 hover / click 才显示:在写 Raw 时考虑"是否打印友好",如默认露出
关键内容、用
print:风格让 hover 状态在打印时强制展开。
故障排除
找不到浏览器
脚本会打印临时 HTML 路径(已经注入打印 CSS),手动 Cmd+P 即可。
PDF 里 TOC 没分页 / 跟正文挤在一起
某些 Chromium 旧版本对 page-break-after: always 的支持有差异。可以:
- 升级 Chrome 到当前主版本。
- 或手动 Cmd+P 时在打印对话框选"两面 / 缩放 / 自定义边距"。
PDF 分页奇怪 / 出现大块空白页 / 标题孤零零卡在页底
通常是某些"原子块"被强制不分页导致整块被推到下一页。检查 pdf-print-overrides.css:
- B1 是否生效(看 DevTools Print Preview 里
.ra-section的break-inside值)。 reacticleprint.css那条没!important,理论上我们的!important一定能赢。 - 自己写的 Raw 块里有没有 inline
style={{ breakInside: 'avoid' }}或类似 CSS —— 删掉。 - 如果还想"标题永远不卡页底",把 B2 里
break-after: avoid改成break-before: avoid; break-after: avoid(更激进,但偶尔会牺牲一些纸面利用率)。
PDF 里 Raw 没渲染完整 / 图表是空白
- 增加
--virtual-time-budget(编辑脚本,从 5000 改到 10000+)。 - 检查 Raw 的 JS 是否在 DOMContentLoaded 内同步渲染(异步加载的远程图片 / 数据可能没等到 截屏就开拍)。
- 改造 Raw:用 SSR-friendly 的初始态 + 客户端 hydrate 增强,不要"完全靠 JS 后才能看"。
PDF 字体跟浏览器看不一样
主题用了 @font-face 远程字体?headless Chrome 默认不等远程字体加载完。可以:
- 用 system font fallback(推荐,多数主题已经做了)。
- 或在 HTML 里把字体
data:内联(vite-plugin-singlefile 已经处理静态资源,但 woff 字体 要看主题怎么写的)。
想自定义页面留白 / 纸张大小
Chromium 的 --print-to-pdf 不支持命令行页面尺寸 / 边距参数。当前 CSS 用
@page { margin: 0 } 让主题纸色铺满整页,再用 .ra-root { padding: 0.45in }
提供内容留白。如果需要改留白,优先调整 scripts/pdf-print-overrides.css 里的
.ra-root print padding。
如果用户真的要自定义,建议:
- 用浏览器 GUI 打印(Cmd+P)—— 那里有边距 / 纸张 / 缩放选项。
- 或者派生一个
html-to-pdf-puppeteer.sh脚本,单独支持自定义。当前脚本默认按 Chrome 默认页面尺寸(Letter 美区 / A4 其它区)输出。
与 Skill 流程的关系
| 阶段 | 触发 | 动作 |
|---|---|---|
| Phase 8 Delivery | 用户在 Checkpoint 3 选 "通过 · 同时导出 HTML + PDF" | 跑 npm run html → 跑 bash <skill>/scripts/html-to-pdf.sh → 交付 article.html + article.pdf |
| Checkpoint 3 其它选项 | 用户选 "通过 · 导出 HTML 交付" | 不跑 PDF |
| 用户事后想补 PDF | 任何时刻 | 在工作区根目录手动跑 bash <skill>/scripts/html-to-pdf.sh |
不做的事
- ❌ 不在脚手架强行装任何 PDF 相关 npm 包(保持脚手架轻量)。
- ❌ 不在 Checkpoint 3 默认勾选 "导出 PDF"(PDF 不是主交付物)。
- ❌ 不替用户判断"要不要 PDF" —— 这是 Checkpoint 3 用户独立选择项。
- ❌ 不改 reacticle 来支持 PDF(CSS 注入更轻量、跟 reacticle 版本解耦)。