静的HTMLの抽出
任意のウェブアプリケーションから自己完結型の静的HTMLファイルを抽出します。
使用する戦略
先に進む前に、ユーザーに使用する戦略を選択してもらう必要があります。以下の選択肢を明確に提示し、デフォルトとして戦略Aを推奨し、各オプションの簡単なメリット/デメリットの要約を提供して、ユーザーが情報に基づいた決定を下せるようにしてください。
| 戦略A (Puppeteer) | 戦略B (ブラウザサブエージェント) | |
|---|---|---|
| いつ使うか | アプリがローカルで動作し、認証がない場合 | ページと先に対話する必要がある場合(クリック、フォーム入力) |
| 忠実度 | 最高 — 計算済みスタイルが解決済み | 高 — レンダリングされたDOM |
| セットアップ | 不要 — モック不要 | 不要 — モック不要 |
| フレームワーク | 任意 | 任意 |
| 出力 | ファイルに書き込み — サイズ制限なし | エージェントコンテキストで切り詰められる可能性あり |
[!WARNING] チェックポイント — ユーザー確認が必要です。 先に進む前に、ユーザーにどちらの戦略を希望するかを必ず尋ねてください。 上記の比較表を提示し、デフォルトとして戦略Aを推奨し、 明示的な承認を待ってください。ユーザーが確認するまで、ご自身で決定したり先に進んだりしないでください。
戦略A: Puppeteerスナップショット (推奨)
ヘッドレスChromeを起動し、完全にレンダリングされたDOMをキャプチャし、すべてのCSSがインライン化され、画像がbase64として埋め込まれた自己完結型のHTMLファイルを生成します。任意のフレームワークで動作します — MockPage.jsxは不要です。
前提条件
- アプリがローカルで実行されていること(例:
npm run dev) - Node.jsと
puppeteerが利用可能であること(確認:node -e "require('puppeteer')")
ワークフロー
-
アプリを起動し、ポート番号をメモします。
[!WARNING] チェックポイント — ユーザー確認が必要です。 ローカルサーバーを起動した後、スナップショットスクリプトを実行したりブラウザサブエージェントを起動したりする前に、必ず一時停止してユーザーに確認を求めてください。URLとポートをユーザーに報告し、アプリが実行中で正しくレンダリングされていることを確認できるようにします。ユーザーが確認するまで、スナップショットのステップに進まないでください。
-
スナップショットスクリプトを実行:
npx tsx <SKILL_DIR>/scripts/snapshot.ts \ --url http://localhost:5173 \ --output .stitch/home.html \ --wait 2000 -
複数ページ — ルートごとに1回実行:
npx tsx <SKILL_DIR>/scripts/snapshot.ts \ --url http://localhost:5173 --output .stitch/home.html --wait 2000 npx tsx <SKILL_DIR>/scripts/snapshot.ts \ --url http://localhost:5173/pricing --output .stitch/pricing.html --wait 2000 npx tsx <SKILL_DIR>/scripts/snapshot.ts \ --url http://localhost:5173/dashboard --output .stitch/dashboard.html --wait 2000 --html-class dark -
開発サーバーの後片付け: スナップショット抽出専用にローカル開発サーバーを起動した場合は、抽出が完了したら必ずサーバープロセスを停止するか、バックグラウンドタスクを終了してください。
スクリプトフラグ
| フラグ | デフォルト | 説明 |
|---|---|---|
--url | (必須) | キャプチャするURL |
--output | (必須) | 出力ファイルパス |
--wait | 1000 | ネットワークアイドル後の追加待機時間(ミリ秒)。遅延読み込みアプリの場合は増やしてください。 |
--viewport | 1280x800 | ビューポートサイズを WIDTHxHEIGHT で指定 |
--html-class | — | <html> 要素に追加するクラス(例:dark) |
--remove-fixed | false | fixed/sticky要素(Cookieバナー、チャットウィジェット)を削除 |
--full-height | false | ビューポートをスクロール全体の高さにリサイズ |
--title | — | ページタイトルを上書き(ルートパス、例:/dashboard や /settings/profile に設定) |
--auth-script | — | 認証用の async (page) => void 関数をデフォルトエクスポートするJS/TSモジュールへのパス |
--inline-canvas | false | <canvas> 要素(ECharts、Chart.js、D3)をbase64の <img> タグに変換 |
自動的に行われる処理
document.styleSheetsからすべてのCSSOMルールをキャプチャ(動的なVite/Tailwind開発スタイルとCSS-in-JSを保持)- すべての
<link rel="stylesheet">を<style>ブロックにインライン化 <img>のsrcおよびsrcsetをbase64データURIに変換(外部フォントはスキップ)- 同一オリジンおよび相対パスのアイコンフォントファイル(
@font-face)をbase64データURIとしてインライン化し、リガチャがASCIIテキストとしてレンダリングされないようにする <source srcset>URLをbase64としてインライン化- 失敗/無効な
srcsetエントリを削除し、ブラウザがインライン化されたsrcにフォールバックするようにする <script>タグ、Vite HMR開発スタイルブロック(createHotContext、import.meta.hot)、開発オーバーレイを削除- インライン化の前に相対的なCSSの
url()パスを解決
フレームワーク別の注意点
| フレームワーク | 注意点 |
|---|---|
| React + Vite | そのまま動作します。--wait 1000。 |
| Next.js | SSRハイドレーションには --wait 3000。URL: http://localhost:3000。/_next/image からの <img srcset> は自動的にbase64としてインライン化されます。 |
| Angular (@angular/cli / v17+) | ng serve でそのまま動作します(デフォルトURL: http://localhost:4200)。Angular Material / PrimeNGのアニメーションハイドレーションと遅延読み込みルートには --wait 2000。 |
| Vue / Nuxt | そのまま動作します。 |
| Svelte / SvelteKit | そのまま動作します。 |
| Storybook | ストーリーURLを使用: --url http://localhost:6006/?path=/story/... |
| SSR (Webpack) | より長い --wait が必要になる場合があります。 |
トラブルシューティング
| 問題 | 解決策 |
|---|---|
| 画像が表示されない | --wait を増やす |
| サーバー停止後に画像が壊れて表示される | srcset がインライン化されたか確認する — ログで「Inlined N images」を確認。srcset URLが失敗した場合、自動的に削除され src(インライン化済み)が使用されます。 |
| アイコンがテキスト/セリフ体のスタイルなしフォントで表示される | snapshot.ts が document.styleSheets(ステップ0)からCSSOMをキャプチャし、同一オリジンのアイコンフォント(@font-face)がbase64データURIとしてインライン化されていることを確認。 |
Next.jsの /_next/image がインライン化されない | スナップショット実行時に開発サーバーが実行中であることを確認 — スクリプトは実行中のサーバーから最適化された画像を取得します。 |
| ダークモードが適用されない | --html-class dark |
| 出力にCookieバナーがある | --remove-fixed |
| ログインが必要なページ | --auth-script ./auth.ts を使用(下記「認証が必要なページ」を参照) |
| チャート/グラフが空白のボックスになる | --inline-canvas を使用して <canvas> をbase64の <img> にシリアライズ |
Cannot find module 'puppeteer' | npm install -g puppeteer |
認証が必要なページ
ログインガード(Vue Routerの beforeEach、Reactの ProtectedRoute など)があるアプリの場合、Puppeteerセッション内で実行される小さな認証スクリプトを作成します:
// auth-myapp.ts
import type { Page } from 'puppeteer';
export default async function authenticate(page: Page) {
// 例1: ログインフォームに入力して送信
await page.type('#username', 'admin');
await page.type('#password', 'password123');
await page.click('#login-button');
await page.waitForNavigation({ waitUntil: 'networkidle2' });
// 例2: Cookie/localStorageを直接注入
// await page.evaluate(() => {
// localStorage.setItem('token', 'mock-jwt-token');
// });
// 例3: モジュールインジェクションを介してアプリ独自のログインAPIを呼び出す (Vue/Vite)
// await page.evaluate(() => {
// return new Promise((resolve) => {
// const script = document.createElement('script');
// script.type = 'module';
// script.textContent = `
// import { useUserStore } from '/src/store/modules/user.ts';
// import { fetchLogin } from '/src/api/auth.ts';
// const res = await fetchLogin({ userName: 'Admin', password: '123456' });
// useUserStore().setToken(res.token, res.refreshToken);
// window.dispatchEvent(new CustomEvent('auth-done'));
// `;
// document.head.appendChild(script);
// window.addEventListener('auth-done', () => resolve(true), { once: true });
// });
// });
}
そして次のように使用します:
npx tsx <SKILL_DIR>/scripts/snapshot.ts \
--url http://localhost:5173/#/dashboard \
--output .stitch/dashboard.html \
--auth-script ./auth-myapp.ts \
--inline-canvas \
--wait 5000
スクリプトは最初に --url に移動し(ログインページにリダイレクトされる可能性あり)、認証関数を実行し、認証済みセッションで元の --url に再度移動します。
戦略B: ブラウザサブエージェントによるキャプチャ
キャプチャ前にページと対話する必要がある場合(ボタンのクリック、フォームの入力、タブの移動など)に使用します。ブラウザサブエージェントは完全な制御を提供しますが、大きなページでは出力が切り詰められる可能性があります。
ワークフロー
-
アプリをローカルで起動します。
-
ブラウザサブエージェントを使用して移動します。
-
必要に応じて対話します(クリック、スクロール、フォーム入力)。
-
DOMを抽出:
document.documentElement.outerHTML[!WARNING] 大きなページは切り詰められる可能性があります。これに対処するには:
- 抽出前に
<style>タグを削除:document.querySelectorAll('style').forEach(el => el.remove()) - スタイルを静的に再追加(Tailwind CDNリンク、ソースCSS)
- 抽出前に
-
ファイルに保存します。
付録: 静的フォールバック (MockPage.jsx)
[!NOTE] この方法は最後の手段です。アプリをローカルで実行できない場合(依存関係の破損、バックエンドの欠落、回避不可能な認証壁など)に使用します。Reactコンポーネントを手動で単一のJSXファイルにフラット化する必要があります。可能な限り戦略Aを優先してください。
使用する場合
- アプリをローカルでまったく実行できない場合
- モックや回避策なしでは認証が必要なページの場合
- ナビゲーションでは到達不可能な特定のUI状態(エラー画面、空の状態など)が必要な場合
クイックリファレンス
npx tsx <SKILL_DIR>/scripts/extract_inline_html.ts \
--index-css src/css/App.css \
--extra-css index.html \
--outdir .stitch \
--page src/MockPage.jsx:Page.html:"Page Title"
主要なフラグ: --no-tailwind(Tailwind以外のアプリ)、--html-class dark(ダークモード)、--css-files(追加のCSSファイル)。
自動検出: Tailwind設定は自動検出されます。@apply ディレクティブは自動的に <style type="text/tailwindcss"> を使用します。
MockPage.jsx ルール
- 完全なレイアウトを含める — ヘッダー、サイドバー、フッター(最初に
App.jsを読む) - すべての条件分岐をフラット化する — 1つの状態を選択し、すべての三項演算子と
&&ガードを削除する - すべてのデータをハードコードする —
{variable}を具体的な値に置き換え、.map()ループを展開する - ロゴを保持する — ローカルパスで
<img>を使用する(後処理でインライン化されます) - 浮動要素を削除する — Cookieバナー、チャットウィジェット、フィードバックボタン
後処理
ローカル画像をインライン化:
npx tsx <SKILL_DIR>/scripts/post_process.ts \
.stitch/Page.html --base-dir <app-directory>