stitch::extract-static-html について
Webアプリから単独で表示できる静的HTMLを抽出する開発者向けスキルです。手法の選択をユーザーに求め、Puppeteerによるスナップショット、ブラウザー操作、静的な代替手順を案内します。
主な機能
- 01Puppeteerでスナップショットを作成しスタイルと画像をインライン化
- 02ブラウザー操作後にDOMを抽出
- 03認証スクリプトと複数ルートに対応
- 04ビューポート、待機時間、ページ整理を設定
- 05アプリを実行できない場合の静的代替手段
活用シーン
- 01ローカルのWebページを単独HTMLとして保存
- 02ログイン後に保護されたページを取得
- 03ページを操作して特定状態をキャプチャ
- 04アプリが起動できない場合にページスナップショットを手動作成
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ google-labs-code/ stitch-skills --skill stitch::extract-static-htmlインストール後、Claude Code で/stitch::extract-static-htmlを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
scripts/post_process.tsextract_inline_html.tssnapshot.ts詳細
静的 HTML の抽出
あらゆるウェブアプリケーションから、単独で動作する静的 HTML ファイルを抽出します。
使用する方法の選択
作業を進める前に、必ずユーザーに使用する方法を選んでもらってください。選択肢を明確に提示し、既定の推奨方法として方法 A を推奨し、十分な情報に基づいて選択できるよう、各方法のメリットとデメリットを簡潔にまとめてください。
| 方法 A(Puppeteer) | 方法 B(ブラウザーサブエージェント) | |
|---|---|---|
| 用途 | アプリがローカルで実行され、認証の壁がない場合 | 先にページを操作する必要がある場合(クリック、フォーム入力など) |
| 忠実度 | 最高 — 計算済みスタイルを解決 | 高 — レンダリング済み DOM |
| セットアップ | 不要 — モックも不要 | 不要 — モックも不要 |
| フレームワーク | 任意 | 任意 |
| 出力 | ファイルに書き込むため、サイズ制限なし | エージェントのコンテキスト内で切り詰められる場合がある |
警告
チェックポイント — ユーザーの確認が必要です。 作業を進める前に、必ずユーザーにどちらの方法を希望するか尋ねてください。 上記の比較表を提示し、既定の方法として方法 A を推奨したうえで、 明示的な承認を待ってください。ユーザーの確認なしに、自分で決定したり 作業を進めたりしてはいけません。
方法 A:Puppeteer スナップショット(推奨)
ヘッドレス Chrome を起動して、完全にレンダリングされた DOM を取得し、すべての CSS をインライン化し、画像を base64 として埋め込んだ、単独で動作する HTML ファイルを生成します。あらゆるフレームワークで動作し、MockPage.jsx は不要です。
前提条件
- アプリがローカルで実行されていること(例:
npm run dev) puppeteerを利用できる Node.js(確認方法:node -e "require('puppeteer')")
ワークフロー
-
アプリを起動し、ポート番号を控えます。
警告
チェックポイント — ユーザーの確認が必要です。 ローカルサーバーを起動した後、スナップショットスクリプトの実行や ブラウザーサブエージェントの起動前に、必ず一時停止してユーザーに 確認を求めてください。アプリが正しく実行され、レンダリングされているか 確認できるよう、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 | ネットワークがアイドル状態になった後の追加待機時間(ms)。遅延読み込みを行うアプリでは値を増やしてください。 |
--viewport | 1280x800 | WIDTHxHEIGHT 形式のビューポートサイズ |
--html-class | — | <html> 要素に設定するクラス(例:dark) |
--remove-fixed | false | 固定/スティッキー要素(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 から CSSOM を取得し(ステップ 0)、同一オリジンのアイコンフォント(@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) {
// Example 1: Fill and submit a login form
await page.type('#username', 'admin');
await page.type('#password', 'password123');
await page.click('#login-button');
await page.waitForNavigation({ waitUntil: 'networkidle2' });
// Example 2: Inject cookies/localStorage directly
// await page.evaluate(() => {
// localStorage.setItem('token', 'mock-jwt-token');
// });
// Example 3: Call the app's own login API via module injection (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警告
ページが大きいと出力が途中で切れる場合があります。対処するには:
- 抽出前に
<style>タグを削除します:document.querySelectorAll('style').forEach(el => el.remove()) - スタイルを静的に再追加します(Tailwind CDN リンク、ソース CSS)
- 抽出前に
-
ファイルに保存します。
付録:静的フォールバック(MockPage.jsx)
メモ
この方法は、アプリをローカルで実行できない場合(依存関係の破損、バックエンドの不足、回避手段のない認証制限)の最終手段です。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>