mcpField
W
wp-static-clone

wp-static-clone

github.com/jdevalkクラウド・DevOps10936 回インストール19 週間前に更新
リポジトリ

wp-static-clone について

WordPress サイトを静的ホスティング向けの HTML に変換するワークフローです。移行や保存を行う人向けに、XML サイトマップからのページ取得、パスの書き換え、WordPress 固有の処理の除去、デプロイ確認を扱います。

主な機能

  1. 01XML サイトマップからページを取得
  2. 02一括取得によるページ間リンクの書き換え
  3. 03アセットパスのルート相対化
  4. 04コメントフォームなどの実行時マークアップ除去
  5. 05Gravatar アバターのセルフホスト
  6. 06静的出力の検証とデプロイ

活用シーン

  1. 01WordPress サイトの静的ホスティングへの移行
  2. 02サイトページの静的 HTML 保存
  3. 03静的コピーのリンクとアセットの確認

インストール

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

$npx skills add https://github.com/jdevalk/skills --skill wp-static-clone

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

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

AGENTS.md
README.md
scripts/
insert-banner.py
rewrite-paths.py
selfhost-gravatars.py
strip-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 の検証チェックリストを実行します。

  1. urls.txt 内のすべての URL がファイルに解決されること(ページの見落としがないこと)。
  2. canonical/og:url/JSON-LD の許可リスト以外に https://<source-domain>/ が残っていないこと。
  3. wget --spider で内部リンク切れがないこと。
  4. ホームページと下層ページをブラウザーで抜き取り確認すること。srcset 画像、サイドバーウィジェット、ヘッダーバナーを確認してください。見落とすと、これらはエラーを出さずに壊れます。
フェーズ 9 — デプロイする

ホスティングサービスごとの手順は AGENTS.md を参照してください。

  • Cloudflare Pages — _redirects、_headers、「ビルドコマンドなし、出力ディレクトリなし」のデフォルト設定。
  • Netlify — 同じ _redirects/_headers の構文に加えて、netlify.toml。
  • Vercel — redirects/headers を含む vercel.json。
  • 汎用 — nginx の try_files、Apache の Options +MultiViews。

注意点

私たちが実際に困った点です。同じ失敗を繰り返さないでください。

  1. Cloudflare のボット対策は、デフォルトの Wget/1.x UA を 403 で拒否します。 必ず実際のブラウザーの UA と Accept/Accept-Language ヘッダーを設定してください(レシピを参照)。リクエストが集中した後に 403 Forbidden が表示された場合は、これが原因です。間隔を空け、UA を切り替えて再試行してください。

  2. ページ間のリンク書き換えは、1 回の wget 呼び出しでのみ機能します。 wget の -k は、現在の実行で検出したローカルパスにのみ書き換えます。別の呼び出しでダウンロードしたページ(たとえば、ある URL の 403 を解消するためにダウンロードした場合)は、他のページへのリンクが絶対パスのままになります。解決策:適切な UA が使えるようになったら、スクレイピング全体をやり直してください。部分的に実行しないでください。大規模(URL が 10K 以上)で、1 回の実行に収まらない場合は、バッチに分けてスクレイピングし、その後 scripts/rewrite-paths.py を正規の処理として再実行してください。そうすれば -k の出力は不要になります。

  3. ホストごとのデフォルトの公開ディレクトリ。 ビルドコマンドが設定されていない場合、Cloudflare Pages はリポジトリのルートを配信します。Netlify と Vercel もデフォルトではルートを使用します。output/ にスクレイピングした場合は、ファイルをリポジトリのルートに移動する(git mv output/* .)か、ホストの公開先を output/ に設定してください。Pages で設定を誤った場合の症状:Pages ブランドの 404 ではなく、R2 形式のヘッダー(access-control-allow-origin: *、cache-control: no-store)を伴って、すべての URL が 404 になります。

  4. WordPress の Offload Media プラグインは、/wp-content/uploads/ を R2/S3 バケットに振り分けます。wget で画像を正常に取得できても、後から直接アクセスすると 404 になることがあります(バケットの同期が断続的、または不完全な場合)。ローカルコピーを信頼してください。だからこそ、スクレイピングしてセルフホストします。

  5. サイトマップと Yoast XSL は HTML からリンクされていません。 wget の -p では見つかりません。Phase 1 で明示的に取得してください。

  6. ?ver=... クエリ文字列を含むファイル名。 wget はこれを文字どおりのファイル名として保持し、HTML では %3F にエンコードされます。一般的なサーバー(Pages、Netlify、Vercel、python -m http.server)は URL をデコードして正しく配信します。実際に問題が起きない限り、「整理」しようとしないでください。

  7. og:url、canonical、JSON-LD は絶対パスのままにします。 これらは正規のリソースを識別するもので、同じドメインに再デプロイする場合はそのままで正しい状態です。ドメインを変更する場合にのみ書き換えてください。

  8. 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 ホストにプッシュし、ビルドコマンドなし、ビルド出力ディレクトリなしで静的ホストに接続してください。デフォルト設定のままで動作します。

コメント