Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
kevintsai1202 avatar

Course Ebook Publishing

  • 51 installs
  • 47 repo stars
  • Updated May 15, 2026
  • kevintsai1202/teaching-site-skills

Helps with ai & agent building tasks.

About

course-ebook-publishing is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.

  • course-ebook-publishing
  • AI & Agent Building
  • AI-coding skill

Course Ebook Publishing by the numbers

  • 51 all-time installs (skills.sh)
  • +4 installs in the week ending Aug 2, 2026 (Skillselion tracking)
  • Ranked #7,219 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
  • Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/kevintsai1202/teaching-site-skills --skill course-ebook-publishing

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs51
repo stars47
Last updatedMay 15, 2026
Repositorykevintsai1202/teaching-site-skills

What it does

Helps with ai & agent building tasks.

Files

SKILL.mdMarkdownGitHub ↗

Course Ebook Publishing

Schema authority: this skill reads the live window.COURSE object 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 in dist/{name}.pdf / dist/{name}.docx. Source markdown is composed under dist/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-edition to 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.docx

Loading 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 ![alt](path) 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:

FlagBehavior
(no flag)Build both PDF + DOCX
--md-onlyStop after composing markdown (debug content)
--no-docxSkip DOCX (faster iteration on PDF)
--no-pdfSkip PDF (faster iteration on DOCX)
--keep-htmlDon't delete intermediate HTML (debug CSS)
--output PATHCustom 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-authoring instead.
  • 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}.pdf and optionally .docx are 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."

Related skills

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.