Mode d’emploi
- Copiez la commande et examinez le paquet avant de l’installer dans votre environnement agent.
- Consultez le document original pour les outils, identifiants et environnements requis.
- Décrivez les entrées et le livrable souhaité ; comparez la sortie aux sources.
npx skills add https://github.com/kevintsai1202/teaching-site-skills --skill course-ebook-publishingAvant de commencer
- L’installation n’inclut ni abonnement au modèle, ni quota API, ni accès aux fichiers.
- Étoiles et installations reflètent l’activité, pas un test indépendant de qualité ou de sécurité.
Rendu depuis le dépôt source en conservant titres, exemples, code, tableaux, liens et images.
Course Ebook Publishing
Schema authority: this skill reads the livewindow.COURSEobject whose shape is defined in `_shared/domain-primitives.md`. Quiz / pre-test / post-test items are filtered OUT of the ebook (per §10 quiz item rules + ebook content policy). Filename convention (English-first): outputs land indist/{name}.pdf/dist/{name}.docx. Source markdown is composed underdist/master.md.
This skill turns a finished teaching website into a book — PDF (primary) and optionally DOCX (for editorial review or further authoring). It is a post-site step: it consumes window.COURSE from the live site, never re-authoring content.
When to Invoke
- Site is feature-complete (Stages 1–5 done) and content is stable.
- Stakeholders ask for a printable hand-out, an archivable record, or a deliverable for non-web channels.
- Sometimes invoked after
course-corporate-editionto produce a client deliverable bundle.
Do NOT invoke when site is mid-development. The ebook pipeline reads window.COURSE; if the site changes daily, the ebook drifts daily.
Architecture: Single Source → Two Outputs
window.COURSE ─┐
├─→ compose-ebook.mjs ─→ master.md ─→ pandoc ─→ ebook.docx
asset folders ─┤ └─→ Playwright page.pdf() ─→ ebook.pdf
materials/ ─┘Why a single composed markdown intermediate: keeps PDF and DOCX content identical. Pandoc handles markdown → DOCX cleanly; Playwright renders the same markdown via an HTML wrapper → PDF. Two outputs, one source of truth.
File Layout (Standard)
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.docxLoading Source Data: window.COURSE from index.html
When the site uses external course-data.js, just import it. When it uses inlined COURSE (corporate edition), extract via regex + vm sandbox:
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;
}Why vm sandbox not eval: vm isolates the script's globals from the build process; an accidental side-effect can't leak.
PDF Rendering: Playwright page.pdf() (not Chrome CLI)
Original implementation: pandoc → HTML → msedge --headless --print-to-pdf. Switch to Playwright — Chrome CLI doesn't support footerTemplate (no page numbers).
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();Cover page without page number
The cover should be number-less. CSS:
@page { margin: 20mm; }
@page :first { margin: 0; } /* full bleed cover */Playwright respects @page :first — the footer doesn't print on it.
Book-Style Layout (style-ebook.css)
The example workshop's "book-grade typography" commit was driven by aesthetics, not just formality. Key rules worth porting:
/* 章首跨頁分頁 */
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;
}DOCX via pandoc + reference template
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` is a one-time-generated template with your chosen fonts (e.g. Microsoft JhengHei UI for Chinese), heading colors, paragraph spacing. Generate via scripts/gen-reference-docx.mjs — pandoc copies the styles.
Caveat: raw HTML support in DOCX is partial. <figure-grid> blocks won't render as grids in Word — they degrade to vertical stack. Use markdown  syntax for images so they survive both formats.
Compose Module Anatomy
compose-ebook.mjs exports one function per chapter type:
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 */ }Each returns a markdown string. build-ebook.mjs concatenates them with \n\n---\n\n.
Asset Path Strategy
Inside the composed markdown, images use paths relative to the markdown file (./assets/illustrations/foo.png). Pandoc's --resource-path resolves them; Playwright's file:// HTML loader does too. No absolute paths.
For corporate editions with asset fallback, resolve the actual file location at compose time and embed the resolved path:
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
}What to Filter Out
Some content lives on the website but should not be in the ebook:
- Quiz items: leaking exam questions defeats the assessment. The example workshop filters out the entire quiz chapter.
- Pre-test / post-test sections: same reason.
- Interactive-only features: "點擊複製" buttons make no sense in print. Render the raw prompt text, not the button.
- Dynamic illustrations: anything generated client-side via canvas/JS won't render.
Add filter conditions to the compose functions; don't try to render-then-strip.
Verification
After generating, verify:
// 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)For DOCX, open in Word once and visually check: TOC linked, fonts applied, no missing-image squares.
CLI Conventions
The example workshop's build:ebook script supports:
| Flag | Behavior |
|---|---|
| (no flag) | Build both PDF + DOCX |
--md-only | Stop after composing markdown (debug content) |
--no-docx | Skip DOCX (faster iteration on PDF) |
--no-pdf | Skip PDF (faster iteration on DOCX) |
--keep-html | Don't delete intermediate HTML (debug CSS) |
--output PATH | Custom PDF output path |
Provide these from day one — you'll iterate the layout many times.
Anti-Patterns
- Re-authoring content in the ebook builder — the ebook is a derivative. If you find yourself adding new prose in the compose functions, that prose belongs in
course-content-authoringinstead. - Using Chrome CLI for PDF — switch to Playwright early; you'll want footers eventually.
- Hardcoding image paths — use
pickAsset()with a list of roots, especially when supporting corporate edition fallbacks. - Forgetting `@page :first { margin: 0 }` — cover ends up with a stray page number in the footer.
- Re-running the ebook build every time the site changes — only build at stable checkpoints; the intermediate
dist/folder is large.
Hand-off
When this skill finishes:
dist/{course-name}.pdfand optionally.docxare produced.- Verify scripts pass.
- If for a client, the file goes into the deliverable bundle.
Tell the user: "ebook ready. The site and ebook are now in sync; from now on, treat the site as canonical and rebuild the ebook only at release milestones — don't edit the ebook directly, it'll get overwritten."

