关于 course-ebook-publishing
用于网站完成后的课程电子书制作技能,可将 `window.COURSE` 数据转换为 PDF,并可选生成 DOCX。它以同一份 Markdown 源内容构建两种格式,保持内容一致,并排除测验及仅适用于交互的内容。
主要功能
- 01读取 `window.COURSE` 课程数据
- 02合成共用的 Markdown 源文件
- 03通过 Playwright 生成 PDF
- 04可选用 pandoc 生成 DOCX
- 05过滤测验和仅交互内容
- 06提供成品验证指南
适用场景
- 01制作可打印的课程讲义
- 02归档课程内容
- 03网站完成后制作客户交付文件
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ kevintsai1202/ teaching-site-skills --skill course-ebook-publishing安装后在 Claude Code 中输入/course-ebook-publishing确认已加载。
详细介绍
课程电子书出版
架构权威来源:此技能读取实时的
window.COURSE对象,其结构定义于_shared/domain-primitives.md。测验/前测/后测题目会从电子书中筛除(遵循第 10 节测验题目规则及电子书内容政策)。文件名约定(英文优先):输出文件位于
dist/{name}.pdf/dist/{name}.docx。源 Markdown 在dist/master.md中编排。
此技能会将已完成的教学网站转换为书籍——以 PDF 为主,也可选择 DOCX(供编辑审阅或进一步创作)。这是一个建站后步骤:它使用实时网站中的 window.COURSE,不会重新编写内容。
何时调用
- 网站功能已完备(第 1–5 阶段已完成),且内容稳定。
- 利益相关方需要可打印的讲义、可归档的记录,或用于非 Web 渠道的交付物。
- 有时会在
course-corporate-edition之后调用,以生成客户交付包。
网站处于开发中时,切勿调用。电子书流水线会读取 window.COURSE;如果网站每天都在变化,电子书也会每天偏离。
架构:单一来源 → 两种输出
window.COURSE ─┐
├─→ compose-ebook.mjs ─→ master.md ─→ pandoc ─→ ebook.docx
asset folders ─┤ └─→ Playwright page.pdf() ─→ ebook.pdf
materials/ ─┘
为何使用单一的 Markdown 中间文件:确保 PDF 和 DOCX 内容一致。Pandoc 可以顺畅地将 Markdown 转换为 DOCX;Playwright 则通过 HTML 包装器将相同的 Markdown 渲染为 PDF。两种输出,一个事实来源。
文件布局(标准)
scripts/
├── build-ebook.mjs ← CLI entry point (--md-only, --no-docx, --no-pdf, --output, --keep-html)
├── render-pdf.mjs ← markdown → HTML → PDF via Playwright
└── lib/
├── compose-ebook.mjs ← loadSources + composeXxx functions (cover, TOC, chapters, appendix)
└── reference.docx ← pandoc style template (generated by gen-reference-docx.mjs)
style-ebook.css ← @page rules, cover, page-number footer, print-only styles
dist/ ← output: ebook.md, ebook.pdf, ebook.docx
加载源数据:从 index.html 获取 window.COURSE
网站使用外部 course-data.js 时,直接导入即可。使用内联 COURSE(企业版)时,则通过正则表达式和 vm 沙箱提取:
import vm from 'node:vm';
async function loadCourseFromIndexHtml(htmlPath) {
const html = await fs.readFile(htmlPath, 'utf8');
const match = html.match(/<script>\s*window\.COURSE\s*=\s*\{[\s\S]*?\};\s*<\/script>/);
if (!match) throw new Error('window.COURSE 區塊找不到');
const code = match[0].replace(/<\/?script>/g, '');
const sandbox = { window: {}, console };
vm.createContext(sandbox);
vm.runInContext(code, sandbox);
return sandbox.window.COURSE;
}
使用 vm 沙箱而非 eval:vm 会将脚本的全局变量与构建进程隔离,避免意外副作用造成影响。
PDF 渲染:使用 Playwright page.pdf()(而非 Chrome CLI)
原始实现:pandoc → HTML → msedge --headless --print-to-pdf。改用 Playwright——Chrome CLI 不支持 footerTemplate(无法添加页码)。
import { chromium } from 'playwright';
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto(`file://${path.resolve(htmlPath)}`, { waitUntil: 'networkidle' });
await page.pdf({
path: outputPath,
format: 'A4',
printBackground: true,
displayHeaderFooter: true,
headerTemplate: '<div></div>',
footerTemplate: `
<div style="font-size:9pt; width:100%; text-align:center; color:#666;">
<span class="pageNumber"></span> / <span class="totalPages"></span>
</div>`,
margin: { top: '20mm', right: '20mm', bottom: '20mm', left: '20mm' },
});
await browser.close();
不显示页码的封面页
封面不应显示页码。CSS:
@page { margin: 20mm; }
@page :first { margin: 0; } /* full bleed cover */
Playwright 会遵循 @page :first——封面页不会打印页脚。
书籍风格布局(style-ebook.css)
示例工作坊中“书籍级排版”的提交,主要由美观需求驱动,而非仅为正式。以下关键规则值得移植:
/* 章首跨頁分頁 */
h1.chapter { page-break-before: always; }
/* 圖文並排(Grid 排版) */
.figure-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12mm;
}
.figure-grid .three-up { grid-template-columns: repeat(3, 1fr); }
/* 提示詞區塊:橘色 PROMPT 徽章 + 暗底 code */
.prompt-block {
background: #1a1a1a; color: #e0e0e0;
border-left: 4px solid #ff8c00;
padding: 12pt; margin: 8pt 0;
}
.prompt-block::before {
content: 'PROMPT'; display: inline-block;
background: #ff8c00; color: white;
padding: 2pt 8pt; border-radius: 3pt;
font-size: 8pt; margin-bottom: 6pt;
}
/* 任務勾選方框(純印刷) */
.task::before {
content: '☐'; margin-right: 8pt;
font-size: 14pt; color: #888;
}
/* 學習目標綠色徽章 */
.goal::before {
content: '✓'; margin-right: 6pt;
color: #2e7d32; font-weight: bold;
}
通过 pandoc + 参考模板生成 DOCX
const args = [
mdPath,
'-f', 'gfm+attributes+raw_html',
'-t', 'docx',
'--standalone',
'--toc', '--toc-depth=2',
'--resource-path', path.dirname(mdPath),
'--reference-doc', 'scripts/lib/reference.docx',
'-o', outPath,
];
spawn('pandoc', args);
reference.docx 是一次性生成的模板,可设置所选字体(例如中文使用 Microsoft JhengHei UI)、标题颜色和段落间距。通过 scripts/gen-reference-docx.mjs 生成——pandoc 会复制其中的样式。
注意:DOCX 对原始 HTML 的支持不完整。 <figure-grid> 块在 Word 中无法呈现为网格——会退化为纵向排列。图像请使用 Markdown  语法,以确保两种格式都能保留图像。
编排模块结构
compose-ebook.mjs 会为每种章节类型导出一个函数:
export async function loadSources() { /* read COURSE + materials + assets */ }
export async function composeCover(meta) { /* big title, day list, meta card */ }
export async function composeOverview(meta) { /* TOC + course overview */ }
export async function composeChapter(day, assetIndex) { /* hero image + units */ }
export async function composeAppendixMaterials(materials) { /* full-text material appendix */ }
export async function composeAppendixSkills(skills) { /* QR-coded skill links */ }
每个函数都会返回一个 Markdown 字符串。build-ebook.mjs 会用 \n\n---\n\n 将它们串接起来。
资源路径策略
在编排后的 Markdown 中,图像使用相对于 Markdown 文件的路径(./assets/illustrations/foo.png)。Pandoc 的 --resource-path 可以解析这些路径;Playwright 的 file:// HTML 加载器也可以。不要使用绝对路径。
对于带资源回退机制的企业版,应在编排时解析实际文件位置并嵌入解析后的路径:
async function pickAsset(name, roots) {
for (const root of roots) {
const full = path.join(root, name);
try { await fs.access(full); return path.relative(mdDir, full); }
catch { continue; }
}
return null; // caller decides whether to drop the image or fall back to placeholder
}
需要筛除的内容
网站上有些内容不应放入电子书:
- 测验题目:泄露考题会破坏评估效果。示例工作坊会筛除整个测验章节。
- 前测/后测部分:原因相同。
- 仅限交互的功能:“点击复制”按钮在印刷品中毫无意义。请呈现原始提示词文本,而非按钮。
- 动态插图:任何通过 canvas/JS 在客户端生成的内容都无法渲染。
请在编排函数中添加筛选条件;不要尝试先渲染再删除。
验证
生成后,进行验证:
// scripts/verify-ebook.mjs
// - PDF file exists and is > 100KB
// - First page (cover) has no page number
// - TOC pages have entries
// - Every image reference resolved (no broken-image placeholders)
对于 DOCX,请用 Word 打开一次并进行目视检查:目录已链接、字体已应用、没有缺失图片方块。
CLI 约定
示例工作坊的 build:ebook 脚本支持:
| 标志 | 行为 |
|---|---|
| (无标志) | 同时生成 PDF + DOCX |
--md-only | 在合成 markdown 后停止(用于调试内容) |
--no-docx | 跳过 DOCX(加快 PDF 迭代) |
--no-pdf | 跳过 PDF(加快 DOCX 迭代) |
--keep-html | 不删除中间 HTML(用于调试 CSS) |
--output PATH | 自定义 PDF 输出路径 |
从第一天起就提供这些选项——你会多次迭代版式。
反模式
- 在电子书构建器中重新创作内容——电子书是衍生内容。如果你发现自己在合成函数中添加新文案,这些文案应该放在
course-content-authoring中。 - 使用 Chrome CLI 生成 PDF——尽早改用 Playwright;你最终会需要页脚。
- 硬编码图片路径——使用带有多个根目录的
pickAsset(),尤其是在支持企业版回退时。 - 忘记
@page :first { margin: 0 }——封面页脚上会多出一个页码。 - 网站每次更改都重新构建电子书——只在稳定的检查点构建;中间的
dist/文件夹很大。
交接
此技能完成后:
- 已生成
dist/{course-name}.pdf,并可选择生成.docx。 - 验证脚本通过。
- 如果面向客户,文件会放入交付包中。
告诉用户:“电子书已就绪。网站和电子书现在已同步;从现在起,以网站为准,仅在发布里程碑时重新构建电子书——不要直接编辑电子书,重新构建时会覆盖这些修改。”