wp-static-clone について
WordPress サイトを静的ホスティング向けの HTML に変換するワークフローです。移行や保存を行う人向けに、XML サイトマップからのページ取得、パスの書き換え、WordPress 固有の処理の除去、デプロイ確認を扱います。
主な機能
- 01XML サイトマップからページを取得
- 02一括取得によるページ間リンクの書き換え
- 03アセットパスのルート相対化
- 04コメントフォームなどの実行時マークアップ除去
- 05Gravatar アバターのセルフホスト
- 06静的出力の検証とデプロイ
活用シーン
- 01WordPress サイトの静的ホスティングへの移行
- 02サイトページの静的 HTML 保存
- 03静的コピーのリンクとアセットの確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ jdevalk/ skills --skill wp-static-cloneインストール後、Claude Code で/wp-static-cloneを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
AGENTS.mdREADME.mdscripts/insert-banner.pyrewrite-paths.pyselfhost-gravatars.pystrip-wp-runtime.py詳細
wp-static-clone
稼働中の WordPress サイトを、任意の静的ホストにデプロイできる静的 HTML クローンに変換します。サイトの XML サイトマップをもとに処理します。Cloudflare のボット対策、スクレイピング中のリンク書き換え、プロキシ経由のアナリティクス、R2 にオフロードされたアップロードファイル、コメントフォームの実行時処理、Yoast のクレジット表記、Gravatar のプライバシーなど、単純な wget の実行では見落としがちな WordPress 固有の注意点に対応します。
レシピは AGENTS.md、再利用可能なスクリプトは scripts/ にあります。 このファイルでは、ワークフロー、注意点、出力構成について説明します。
使用する場面
次のようなリクエストを受けた場合に使用します。
- 「この WordPress サイトを [ホスト] 向けにスクレイピングして」
- 「[ドメイン] を静的 HTML として固定して」
- 「このサイトマップからすべてのページを取得して、静的ファイルにして」
- 「ビルド手順なしで、この WP サイトを [ホスト] に移行して」
大まかな流れ(サイトマップ → wget → ルート相対パス → 静的ホスト)は、標準的な XML サイトマップを出力するあらゆる CMS に応用できます。実行時のクリーンアップ(コメントフォーム、Plausible プロキシ、Gravatar、Yoast)は WordPress 固有です。
ワークフロー
フェーズ 0 — 意図を確認する
スクレイピングの前に、次の点を確認します。
- ソース URL(稼働中のサイト)。
- ターゲットホスト — Cloudflare Pages、Netlify、Vercel、汎用の静的ホスト。フェーズ 9 の処理が決まります。
- 移行先のドメインが同じか異なるか。
og:url、<link rel="canonical">、JSON-LD の@idを絶対 URL のままにするか(同じドメイン — SEO 上適切な動作)、書き換えるか(異なるドメイン)が決まります。 - アナリティクスとフォームの扱い。どちらも WP プラグインは静的サイトでは動作しません。Plausible は標準トラッカーに置き換えます(レシピは
AGENTS.md)。お問い合わせフォームと検索フォームは削除するか、Pages Functions / Formspree / Netlify Forms を使って接続します(ホストによって異なります)。
フェーズ 1 — URL を検出して XML サイトマップを取得する
サイトマップインデックスを取得します。まず <root>/sitemap_index.xml(Yoast の標準形式)を試し、次に <root>/sitemap.xml を試します。インデックスがサブサイトマップ(page-sitemap.xml、post-sitemap.xml、…)を参照している場合は、それぞれを取得し、<loc> の値を連結して urls.txt に保存します。画像サイトマップの項目はスキップします。
また、この段階で XML サイトマップ本体と Yoast の XSL スタイルシートも取得します(レシピは AGENTS.md)。これらは HTML からリンクされていないため、後から wget の -p では見つかりません。
フェーズ 2 — 1 回の実行でスクレイピングする
重要: すべての URL を 1 回の wget 呼び出しでスクレイピングしてください。こうすれば --convert-links の処理でダウンロード済みの全ファイルが認識され、ページ間リンクを正しく書き換えられます。複数回に分けてスクレイピングすると、先に、または最後に取得したページに絶対リンクが残ります。レシピは AGENTS.md にあります。
フェーズ 3 — ページ要件の取得処理で漏れたアセットを取得する
アセットの中には、og:image、apple-touch-icon、JSON-LD の image / logo、msapplication-TileImage、<link rel="modulepreload"> にしか登場しないため、-p では追跡されないものがあります。残りのアセットを監査して取得します。AGENTS.md のレシピでは、3 つのアセットルート(uploads、themes、plugins)すべてを扱います。
フェーズ 4 — パスをルート相対に変換する
wget -k は ../wp-content/...(深さ相対)と wp-content/...(ホームページ)の両方を生成します。どちらもローカルでは動作しますが、ページを移動した途端に壊れます。すべてのパスを /wp-content/... のようなルート相対パスに変換します。
python3 scripts/rewrite-paths.py output/ urls.txt --source-domain example.com
このスクリプトは、ディレクトリを走査するのではなく urls.txt からページのスラッグ一覧を取得します。そうしないと、wget が取得した category/、feed/、author/、wp-json/ などのアーカイブディレクトリがページとして誤分類され、ページ間リンクが誤って書き換えられます。
スクリプトは WordPress のアセットルート(wp-content、wp-includes)をデフォルトで使用します。WordPress 以外のソースの場合は --asset-roots で上書きします。例:Drupal では --asset-roots sites/default/files,sites/default/themes、Ghost では --asset-roots content/images を指定します。パス書き換えスクリプトのその他の部分は CMS に依存しません。
フェーズ 5 — 静的出力に識別情報を付ける
将来の自分(またはページソースを確認する人)が、これが稼働中の WP サイトではなく静的クローンだとすぐに分かるようにします。
python3 scripts/insert-banner.py output/
すべてのページで <!DOCTYPE html> の後に HTML コメントを挿入します。繰り返し実行しても安全です。続いて、wp-content/plugins/wordpress-seo/css/main-sitemap.xsl 内の「Generated by Yoast SEO」というクレジット表記を置き換えます。レシピは AGENTS.md にあります。
フェーズ 6 — WP のランタイムフックを置き換える
バックエンドを削除すると機能しなくなる、WP 固有のマークアップは 3 種類あります。
1. コメントフォーム、返信リンク、不要な head タグ。 以下の処理を 1 回実行します。
python3 scripts/strip-wp-runtime.py output/
<div id="respond"> ブロック(コメントフォーム)、ブロックテーマ版とクラシックテーマ版の両方にある comment-reply-link アンカー、comment-reply-js スクリプトタグとその参照先ファイル、不要な <head> タグ(REST API の検出、RSD、oEmbed の代替 URL、RSS の代替 URL、アーカイブの next リンク)を削除します。リンクテキストの言語に依存しないよう、全体を通してクラス名で判定します。
2. Plausible のアナリティクスプロキシ。 WP プラグインはスクリプトを /wp-content/uploads/<hash>/pa-XXX.js 経由でプロキシし、イベントを /wp-json/... に送信します。どちらのエンドポイントもなくなります。2 つのスクリプトブロックを標準トラッカーに置き換えます。手順は AGENTS.md を参照してください。
3. Gravatar のアバター。 一意な (hash, size) の組み合わせごとにローカルに保存し、サードパーティへの ?s=N&d=mm リクエストをなくします。
python3 scripts/selfhost-gravatars.py output/
avatars/ に保存し、すべての参照を書き換えます。レスポンスのバイト列から拡張子を判定し(PNG はフォールバック、実際のアバターは JPEG)、サイズ違いのファイル(?s=40 と ?s=80)を別々に保持します。
スクリプトの実行後、残っている絶対パスのソースドメイン URL を監査し(手順は AGENTS.md を参照)、ケースごとに対応を判断します。著者アーカイブは <a> のラッパーを削除し、サーバーレンダリングされた iframe はそれを囲む <p> を削除し、Gravity Forms のスクリプトブロックは gform への言及を条件に削除する、といった具合です。
フェーズ 7 — robots.txt をコピーする
HTML からリンクされていないため、明示的に取得してください。デプロイ先のサイトマップのパスがソースと異なる場合は、Sitemap: の参照を調整してください。
フェーズ 8 — ローカルで検証する
output/ を python3 -m http.server で配信し、AGENTS.md の検証チェックリストを実行します。
urls.txt内のすべての URL がファイルに解決されること(ページの見落としがないこと)。- canonical/
og:url/JSON-LD の許可リスト以外にhttps://<source-domain>/が残っていないこと。 wget --spiderで内部リンク切れがないこと。- ホームページと下層ページをブラウザーで抜き取り確認すること。srcset 画像、サイドバーウィジェット、ヘッダーバナーを確認してください。見落とすと、これらはエラーを出さずに壊れます。
フェーズ 9 — デプロイする
ホスティングサービスごとの手順は AGENTS.md を参照してください。
- Cloudflare Pages —
_redirects、_headers、「ビルドコマンドなし、出力ディレクトリなし」のデフォルト設定。 - Netlify — 同じ
_redirects/_headersの構文に加えて、netlify.toml。 - Vercel —
redirects/headersを含むvercel.json。 - 汎用 — nginx の
try_files、Apache のOptions +MultiViews。
注意点
私たちが実際に困った点です。同じ失敗を繰り返さないでください。
-
Cloudflare のボット対策は、デフォルトの
Wget/1.xUA を 403 で拒否します。 必ず実際のブラウザーの UA とAccept/Accept-Languageヘッダーを設定してください(レシピを参照)。リクエストが集中した後に403 Forbiddenが表示された場合は、これが原因です。間隔を空け、UA を切り替えて再試行してください。 -
ページ間のリンク書き換えは、1 回の wget 呼び出しでのみ機能します。 wget の
-kは、現在の実行で検出したローカルパスにのみ書き換えます。別の呼び出しでダウンロードしたページ(たとえば、ある URL の 403 を解消するためにダウンロードした場合)は、他のページへのリンクが絶対パスのままになります。解決策:適切な UA が使えるようになったら、スクレイピング全体をやり直してください。部分的に実行しないでください。大規模(URL が 10K 以上)で、1 回の実行に収まらない場合は、バッチに分けてスクレイピングし、その後scripts/rewrite-paths.pyを正規の処理として再実行してください。そうすれば-kの出力は不要になります。 -
ホストごとのデフォルトの公開ディレクトリ。 ビルドコマンドが設定されていない場合、Cloudflare Pages はリポジトリのルートを配信します。Netlify と Vercel もデフォルトではルートを使用します。
output/にスクレイピングした場合は、ファイルをリポジトリのルートに移動する(git mv output/* .)か、ホストの公開先をoutput/に設定してください。Pages で設定を誤った場合の症状:Pages ブランドの 404 ではなく、R2 形式のヘッダー(access-control-allow-origin: *、cache-control: no-store)を伴って、すべての URL が 404 になります。 -
WordPress の Offload Media プラグインは、
/wp-content/uploads/を R2/S3 バケットに振り分けます。wget で画像を正常に取得できても、後から直接アクセスすると 404 になることがあります(バケットの同期が断続的、または不完全な場合)。ローカルコピーを信頼してください。だからこそ、スクレイピングしてセルフホストします。 -
サイトマップと Yoast XSL は HTML からリンクされていません。 wget の
-pでは見つかりません。Phase 1 で明示的に取得してください。 -
?ver=...クエリ文字列を含むファイル名。 wget はこれを文字どおりのファイル名として保持し、HTML では%3Fにエンコードされます。一般的なサーバー(Pages、Netlify、Vercel、python -m http.server)は URL をデコードして正しく配信します。実際に問題が起きない限り、「整理」しようとしないでください。 -
og:url、canonical、JSON-LD は絶対パスのままにします。 これらは正規のリソースを識別するもので、同じドメインに再デプロイする場合はそのままで正しい状態です。ドメインを変更する場合にのみ書き換えてください。 -
sed -i ''は macOS/BSD 専用です。 GNU sed ではsed -i(空文字列の引数なし)が必要です。AGENTS.mdのレシピには macOS 固有の記述が示されています。選択肢がある場合は、移植性の高い Python スクリプトをデフォルトで使用してください。
出力構成
<repo-root>/
index.html ← homepage
<slug>/index.html ← one per URL from sitemap
wp-content/ ← assets (themes, uploads, plugins)
wp-includes/ ← block library CSS, et al.
avatars/ ← self-hosted Gravatars (Phase 6)
sitemap_index.xml
page-sitemap.xml ← + any other child sitemaps
wp-content/plugins/wordpress-seo/css/main-sitemap.xsl
robots.txt
_redirects ← optional, host-specific
_headers ← optional, host-specific
git ホストにプッシュし、ビルドコマンドなし、ビルド出力ディレクトリなしで静的ホストに接続してください。デフォルト設定のままで動作します。