mcpField
S
static-spa-conversion

static-spa-conversion

github.com/kevintsai1202開発5265 回インストール24 週間前に更新
リポジトリ

static-spa-conversion について

static-spa-conversionは、章立てのMarkdownを静的なコース用シングルページアプリに変換するスキルです。フレームワークやバンドラーを使わず、コースデータと表示を分離し、進捗などをlocalStorageに保存します。

主な機能

  1. 01Markdownのコース内容をSPAに変換
  2. 02フレームワーク不要のHTMLとJS構成
  3. 03window.COURSEでコースデータを管理
  4. 04localStorageで学習進捗を保存
  5. 05教材名と種類からファイルURLを解決

活用シーン

  1. 01章立ての教材をウェブサイト化
  2. 02コースサイトのデータとページ構成を作成
  3. 03教材ファイルをコース内容にリンク
  4. 04保守しやすいコース用SPAを設計

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/kevintsai1202/teaching-site-skills --skill static-spa-conversion

インストール後、Claude Code で/static-spa-conversionを入力して読み込まれたか確認します。

同梱ファイルtemplates/course-data.template.jstemplates/index.html

詳細

静的 SPA への変換

スキーマの正本:すべての window.COURSE フィールド名は _shared/domain-primitives.md に準拠します。以下のスキーマ例がこのファイルと異なる場合は、このファイルを優先します。

スターターテンプレート(再構成せず、これらをコピーしてください):

リファレンス実装:d:/GitHub/ai-workshop/index.html(4125 行、すべて設定済み)。ビジュアルとレンダリングパターンの参考にしてください。コース固有の内容はコピーしないでください。

ファイル名の規則(英語を優先):course-package/、day{n}/outline.md、day{n}/content.md、materials/、overview.md。従来の中国語名(完整課程包/、課程大綱.md、教學素材/ など)は非推奨です。完全な対応表については _shared/domain-primitives.md §0 を参照してください。

このスキルは、章立てされた Markdown コンテンツを、動作するシングルページアプリに変換します。アーキテクチャ上の方針は意図的に定められており、作業を始める前に理解しておく価値があります。

3 つのアーキテクチャ上の方針

  1. バニラ HTML + JS、フレームワークもバンドラーもなし。 React も Vite も TypeScript のビルドも使いません。理由:コースのウェブサイトは何年も使われることが多く、開発者ではない講師に引き継がれる場合もあります。いつでも開いて編集できる .html のほうが、node_modules の墓場より優れています。また、価値があるのは技術ではなくコンテンツです。

  2. データとレンダリングは別々のファイルに置きます。 course-data.js には、コースのすべてを単一の JS オブジェクト(window.COURSE)として格納します。index.html には、コースの表示方法を記述します。講師が翌日のレッスンを編集する際に、HTML に触れる必要はありません。

  3. 永続化には localStorage を使い、バックエンドは使いません。 進捗、テーマ、ズームはすべてクライアント側で管理します。ホスティングが簡単になり、プライバシーの問題も避けられます。代償として、状態はブラウザーごとになります。それを受け入れてください。

標準のファイル構成

project-root/
├── index.html              ← the single SPA (CSS + JS inline; ~3000 lines is normal)
├── course-data.js          ← window.COURSE = { ... }
├── instructor-data.js      ← optional: window.INSTRUCTOR (if scraped/external)
├── tools-data.js           ← optional: window.TOOLS (if showcasing tools)
├── package.json            ← just `{"scripts": {"serve": "npx serve ."}}` and scraping tooling
├── assets/                 ← images, illustrations
└── course-package/             ← markdown source (read-only from SPA's perspective)
    └── materials/           ← files linked from the SPA via getMaterialUrl()

index.html は通常の <script src="course-data.js"></script> でデータファイルを読み込みます。モジュールのインポートも fetch も使いません。ブラウザーはファイルを順番に読み込み、init() の実行前に window.COURSE を設定します。

window.COURSE スキーマ

最も重要な成果物です。すべてのレンダリング関数がこれに依存するため、慎重に設計してください。標準的な形式:

window.COURSE = {
  meta: {                              // top-level course metadata
    title: '...',
    audience: '...',
    hoursTotal: 24,
    days: [                            // index drives sidebar chapter list
      { id: 'day1', title: 'Day 1: ...', hours: 6, schedule: '...' },
      ...
    ],
    classroom: { name: '...', mapUrl: '...' }
  },
  sharedCase: { /* persistent fictional scenario */ },
  day1: {
    hero: { title, lead, illustration },
    units: [
      {
        id: 'u-1',                     // stable, never renamed
        title: '...',
        timeRange: '14:00–14:50',
        goals: [...],                  // learning outcomes
        tasks: [                       // ← these IDs are localStorage keys
          { id: 'd1-u1-t1', text: '...', done: false }
        ],
        prompts: [                     // optional: copyable templates
          { id: '...', title: '...', body: '...' }
        ],
        materials: [                   // optional: per-unit material refs
          { id: '...', name: '...', type: 'PDF 文件' }
        ],
        illustrations: [               // 1–3 entries, see Stage 5 Coverage Floor
          { name: 'day1-u1-hero.png',  kind: 'hero',    alt: '...', spec: '...' },
          { name: 'day1-u1-flow.svg',  kind: 'diagram', alt: '...', spec: '...' }
        ]
        // legacy single `illustration: 'day1-u1.png'` still accepted; treat as
        // illustrations: [{ name, kind: 'hero' }] and migrate when convenient
      },
      ...
    ]
  },
  day2: { /* same shape */ },
  ...
  materials: [                         // cross-day material index for the "all materials" tab
    { id: 'm-1', name: '...', type: '...', desc: '...' }
  ],
  quiz: [                              // optional: end-of-course assessment
    { id: 'q1', question: '...', options: [...], answer: 0, sourceUnit: 'day1.u-2' }
  ]
};

レンダリングパイプライン

index.html の末尾に大きな <script> が 1 つあります。その構成:

// 1. Utilities (top, ~50 lines)
const el = (tag, attrs, children) => { /* tiny DOM helper */ };
const store = {
  load() { /* read localStorage */ },
  save(state) { /* write localStorage */ },
  reset() { /* clear */ }
};

// 2. Render functions, each takes data → returns/appends DOM
function renderOverview(meta) { ... }
function renderSharedCase(case_) { ... }
function renderDay(day) { ... }
function renderUnit(unit, dayId) { ... }
function renderMaterials(materials) { ... }
function renderToolbox(tools) { ... }    // optional
function renderQuiz(quiz) { ... }        // optional

// 3. Sidebar + scrollspy + theme — see static-spa-interactions

// 4. Entry point
function init() {
  renderOverview(window.COURSE.meta);
  renderSharedCase(window.COURSE.sharedCase);
  for (const dayKey of ['day1', 'day2', 'day3', 'day4']) {
    renderDay(window.COURSE[dayKey]);
  }
  renderMaterials(window.COURSE.materials);
  renderQuiz(window.COURSE.quiz);
  // ... then interactivity wiring (see static-spa-interactions)
}
document.addEventListener('DOMContentLoaded', init);

各レンダリング関数は純粋に保ちます。データを受け取り、DOM を生成します。パラメーター以外のグローバル変数は読み込みません。こうすることで、新しいセクションを簡単に追加できます。

素材 URL ルーター(getMaterialUrl)

最も複雑な部分です。SPA では、素材の name と type をディスク上の実際の URL に対応付ける必要があります。ルーターはスクリプト部分の先頭近くに置きます。

function getMaterialUrl(name, type) {
  const base = 'course-package/materials';
  if (type === 'PDF 文件') {
    // PDFs trigger download instead of inline view
    if (name.includes('員工差勤')) return `${base}/pdf/員工差勤辦法.pdf`;
    if (name.includes('客訴SOP')) return `${base}/pdf/客訴處理SOP.pdf`;
    // ... one rule per PDF
    return null;  // unknown PDF → caller hides the link
  }
  // Other types open in new tab
  if (name.includes('FAQ')) return `${base}/FAQ官方版.md`;
  if (name.includes('差勤')) return `${base}/員工差勤辦法.md`;
  // ...
}

3 か所の同期ルール — 素材を追加するときは、次のすべてを更新します。

  1. ファイルを course-package/materials/ に配置する
  2. course-data.js:materials[](および各ユニットの materials[])にエントリーを追加する
  3. getMaterialUrl() に name.includes(...) ルールを追加する

いずれかを忘れるとリンクが壊れます。ファイルシステム、データ、ルーターの差分を確認する CI チェックを追加することも検討してください。

タスク ID のライフサイクル

// Save
function toggleTask(taskId) {
  const state = store.load();
  state.tasks[taskId] = !state.tasks[taskId];
  store.save(state);
  // re-render checkbox only, not whole page
}

// Load on render
function renderTask(task) {
  const state = store.load();
  const checked = !!state.tasks[task.id];
  return el('label', {}, [
    el('input', { type: 'checkbox', checked, onchange: () => toggleTask(task.id) }),
    task.text
  ]);
}

公開後に task.id を変更してはいけません。 localStorage のキー tasks[task.id] が、訪問をまたいで進捗を保持します。名前を変更すると、すべての学生の進捗が消去されます。

ローカル開発

# Required: file:// blocks localStorage in Chrome — always serve via HTTP
npx serve .
# → http://localhost:3000

これを package.json に追加します。

{
  "scripts": {
    "serve": "npx serve ."
  }
}

どの README でも、これについて目立つように記載してください。index.html をダブルクリックすると、「進捗が保存されない」という分かりにくい問題が発生し、何時間も無駄になることがあります。

移行:Markdown → course-data.js

既存の .md の概要とコンテンツを course-data.js に移行する場合:

  1. まず概要を解析して、日/単元の骨組みを把握します。
  2. 各単元のコンテンツ .md から、grep のような方法で tasks、prompts、materials の参照を抽出します(course-content-authoring のマーカーパターンを探します)。
  3. 大きなオブジェクトリテラルとして course-data.js を生成します。プログラムで見栄えを整えようとせず、prettier/eslint で整形するか、そのままにしておきます。
  4. 手作業で確認します。Markdown 内のすべての単元 ID が course-data.js に存在すること、日数と時間数が meta と一致することを確認します。

このスキルから引き継ぐ際

ユーザーに次のように伝えます:

  • "SPA のスキャフォールドが完成しました。npm run serve を実行して、http://localhost:3000 を開いてください。"
  • "次に、進捗の永続化、サイドバー、テーマ、RWD のために static-spa-interactions を呼び出してください。"
  • "この時点から course-data.js のパスやタスク ID の名前を変更しないでください。学生の localStorage がこれらに依存しています。"

コメント