course-ebook-publishing について
完成済みの教育サイトから `window.COURSE` のデータを読み込み、PDF、必要に応じて DOCX の電子書籍に変換するサイト公開後のスキルです。共通の Markdown を元に両形式を作成し、内容をそろえながらクイズや対話専用要素を除外します。
主な機能
- 01`window.COURSE` の読み込み
- 02共通 Markdown 原稿の構成
- 03Playwright による PDF 生成
- 04pandoc による 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を入力して読み込まれたか確認します。
詳細
コースの電子書籍化
スキーマの基準:このスキルは、
_shared/domain-primitives.mdで形状が定義されている実行時のwindow.COURSEオブジェクトを読み取ります。クイズ/事前テスト/事後テストの項目は、§10 のクイズ項目ルールと電子書籍のコンテンツポリシーに従い、電子書籍から除外されます。ファイル名の規則(英語優先):出力先は
dist/{name}.pdf/dist/{name}.docxです。ソースの Markdown はdist/master.mdに作成します。
このスキルは、完成した教育用ウェブサイトを書籍に変換します。主な出力形式は PDF で、編集レビューやその後の執筆用に DOCX も任意で出力できます。これはサイト完成後の工程です。ライブサイトの window.COURSE を読み込み、コンテンツを再作成することはありません。
呼び出すタイミング
- サイトの機能が完成し(ステージ 1~5 が完了)、コンテンツが確定している場合。
- 印刷可能な配布資料、保存用の記録、またはウェブ以外のチャネル向けの成果物を関係者から求められた場合。
- クライアント向けの成果物一式を作成するため、
course-corporate-editionの後に呼び出すこともあります。
サイトの開発中は呼び出さないでください。電子書籍のパイプラインは window.COURSE を読み込むため、サイトが日々変更されると、電子書籍も日々古くなります。
アーキテクチャ:単一のソースから 2 種類の出力
window.COURSE ─┐
├─→ compose-ebook.mjs ─→ master.md ─→ pandoc ─→ ebook.docx
asset folders ─┤ └─→ Playwright page.pdf() ─→ ebook.pdf
materials/ ─┘
Markdown を 1 つの中間ファイルにまとめる理由:PDF と DOCX のコンテンツを同一に保てます。Pandoc は Markdown から DOCX への変換を適切に処理し、Playwright は同じ Markdown を HTML ラッパー経由で PDF にレンダリングします。2 種類の出力を、1 つの信頼できるソースから作成できます。
ファイル構成(標準)
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;
}
eval ではなく vm サンドボックスを使う理由: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 は、章の種類ごとに 1 つの関数をエクスポートします。
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に追加するべきです。 - PDF の生成に Chrome CLI を使う — 早い段階で Playwright に切り替えてください。いずれフッターが必要になります。
- 画像パスをハードコードする — 特に法人向けエディションのフォールバックに対応する場合は、複数のルートを指定して
pickAsset()を使ってください。 @page :first { margin: 0 }を忘れる — 表紙のフッターに余計なページ番号が表示されます。- サイトが変更されるたびに電子書籍を再ビルドする — 安定したチェックポイントでのみビルドしてください。中間の
dist/フォルダーは大きくなります。
引き継ぎ
このスキルの完了時には、次の状態になります。
dist/{course-name}.pdfと、必要に応じて.docxが生成されます。- 検証スクリプトが成功します。
- クライアント向けの場合、ファイルが納品物のバンドルに含まれます。
ユーザーには次のように伝えてください。「電子書籍の準備ができました。サイトと電子書籍の内容は同期されています。今後はサイトを正本として扱い、リリースの節目にのみ電子書籍を再ビルドしてください。電子書籍を直接編集しないでください。再ビルドすると上書きされます。」