Claude CodeでWordPressを静的サイト化:このサイトを半日で作り直した話
- カテゴリ
- AIとローカルLLM
- 公開日
- 2026年7月11日
- 著者
- Jacob Lloyd — プロジェクト完了後、AIの支援を受けて執筆
- 読了時間
- 約11分で読めます
かんたんに言うと: まさにこのウェブサイトを、AIコーディングアシスタントが半日で作り直した記録です。WordPressからシンプルなファイルの集まりに生まれ変わり、表示は速く、運用費はかからず、ハッカーに狙われるログイン画面もありません。古くなったサイトの管理に悩む人にとって、AIツールで何ができるかの実例です。
このサイトは以前WordPressで動いていました。今はMarkdownファイルの入ったフォルダと、183行のPythonスクリプトです。WordPressのエクスポートを元に、AIコーディングエージェントが半日で作り上げました。その顛末と、なぜ同じワークフローが今もこのサイトを回し続けているのかを書きます。
- 何か: WordPressを置き換えたフルスクラッチの静的サイトジェネレーター(Python + Jinja2)+旧コンテンツ用の一回きりのインポーター
- 費用: 無料。プラグインもテーマのライセンスも不要、ホスティングは前と同じ安い共有サーバー
- 必要なもの: WordPressのエクスポート(WXRファイル)、uploadsフォルダ、AIコーディングエージェント — 私はClaude Codeを使いました
- 最終的に: フラットなHTMLファイル。データベースなし、総当たり攻撃されるログインページもなし
できあがるもの
セットアップの話をする前に、まず出口側から。リビルドが完成した日の、実際のターミナル出力がこれです:
$ python3 _build/build.py
Built 15 files (7 projects, 3 pages, 4 categories).
移行時点でのサイト全体が、このHTML 15ページでした。ホーム、カテゴリページ4つ、プロジェクト記事7本、情報ページ3つ。すべて事前レンダリング済みのただのファイルで、リクエストごとに動くサーバーサイドのコードはゼロです。
元になったWordPressのエクスポートは572KB、123アイテム。記事、固定ページ、画像の添付レコード、そして山ほどの書きかけの下書き。生き残った本物の記事は7本。残りはインポーターが意図的にスキップしたゴミでした。
そして、これをビルドしている仕組み一式は表1枚に収まります:
| ファイル | 行数 |
|---|---|
| build.py(ジェネレーター本体) | 183 |
| migrate.py(一回きりのインポーター) | 166 |
| テンプレート(ページレイアウト5種) | 139 |
| パーシャル(ナビ・フッター・カード) | 51 |
| site.yaml(サイト設定すべて) | 36 |
| serve.sh(ローカルプレビュー) | 12 |
| .htaccess(サーバー設定) | 57 |
| 合計 | 644 |
これにCSS(322行)とJavaScriptファイル1個(31行 — モバイルナビの開閉だけ)を足しても、サイト全体で1,000行未満。node_modulesなし、ビルドフレームワークなし、格闘するlockfileもなし。
パート1:WordPressからの脱出
この中身、私は1行も手で書いていません。作りたいものをClaude Codeに説明したら、インポーター、ジェネレーター、テンプレートの順に書いてくれました。私の仕事は出力をレビューして「いや、そこはこうして」と言うことだけ。ファイルのタイムスタンプが証人です:エクスポートを手に入れてから、ブラウザで見られるサイトが建つまで2時間未満。週末仕事ではなく、半日仕事でした。
最終的に落ち着いたパイプラインはこうです:
エクスポートとメディアのミラー
WordPressは「ツール → エクスポート → すべてのコンテンツ」で全部書き出せます。出てくるのはWXRファイル — 記事、固定ページ、カテゴリ、添付レコードが詰め込まれたXMLです。インポーターの入力はこれだけ。
ただし、エクスポートに含まれないものがひとつ:実際の画像ファイルです。入っているのはURLだけ。なのでuploadsフォルダは別途 — SFTPでもファイルマネージャーでも素のwgetミラーでも — assets/uploads/に持ってきます。WordPressが使っていた年/月のフォルダ構造をそのまま保てば、書き換え後のURLがきちんと解決します。
migrate.pyの中身
インポーターは一回きりですが、何度実行しても安全な作りです。XMLライブラリは不使用 — WXRテキストに正規表現を当てるだけ。脆そうに聞こえますが、WordPressのエクスポートXMLは驚くほど規則的で、grepで拾うほうがフルパーサーを組むよりずっとコードが短く済みます。各記事からタイトル、スラッグ、日付、本文、カテゴリ、アイキャッチ画像を抜き出したうえで:
- ゴミをスキップ — 空の記事と「(draft)…」なタイトルはcontent/に到達しません
- カテゴリをマッピング — WordPressの雑然としたnicenameではなく、短くきれいなキーの一覧に対応付け
- アイキャッチ画像を解決 — 添付IDから引き、なければ本文中の最初のアップロード画像にフォールバック
- メディアURLを書き換え —
wp-content/uploads/…を/assets/uploads/…に - 内部リンクを書き換え — 全記事・全ページから作ったスラッグマップで、旧パーマリンクを新しいプリティURLへ
- Gutenbergブロックコメントを除去し、動的なリスト系ブロックはまるごと削除 — カテゴリ一覧は今はビルド側が生成するので、古い焼き込み一覧は座って腐っていくだけです
- 抜粋を自動生成 — タグを剥がして160文字に切り、カード文とmeta descriptionに使用
面白いのはここです:記事は「Markdownファイルの中に、掃除済みのWordPress HTMLがそのまま入った状態」で出てきます。Markdown記法への変換はしません。レンダラーは生のHTMLを素通しするので、旧コンテンツには劣化が一切起きていません。そして新しい記事は普通のMarkdownで書けばいい。どちらも同じフォルダから問題なくビルドできます。中身が9割HTMLタグの「.md」ファイルを初めて開いたときは、何かが壊れたんだと思いました。壊れていませんでした。
build.pyの仕事
こちらは永遠に動き続けるほうです。site.yamlからナビ・カテゴリ・リンクを読み、content/の中身を全部読み、各ファイルをJinja2テンプレートに通して、index.html入りの本物のフォルダとして書き出します。サーバー側のリライトルールなしできれいなURLになる仕掛けです。ページ本体のほかに生成するもの:
- データから組み立てるナビとフッター — カテゴリやページを足せば勝手に現れます
- ページごとのtitle、meta description、canonical URL、Open Graph/Twitterタグ
- sitemap.xml、robots.txt、Webマニフェスト(毎回再生成)
- 古い出力を先に消すクリーンアップパス — 記事をリネームしても死んだページが残りません
- ランタイム依存ゼロ:セルフホストのフォント、インラインSVGアイコン、例の小さなJSファイル1個。CDNからは何も取りません
記事の追加は、content/projects/にMarkdownファイルを1個置いてビルドを実行するだけ。ワークフローはそれで全部です。
プレビューしてからデプロイ
ローカルプレビューはスクリプト1本 — ビルドして配信。実サーバーに近づける前に、全ページをクリックして確認します。
./serve.sh
# builds, then serves the whole site at http://localhost:8099
デプロイは、何を送るか(HTML、CSS、JS、画像、フォント、.htaccess)と何を送らないか(Pythonスクリプト、Markdownソース、WordPressエクスポート、テンプレート)をマニフェストで明示したrsyncプッシュです。サーバー設定側では圧縮、キャッシュヘッダー、セキュリティヘッダー、そしてソースファイルへの直接アクセスを固く拒否 — ジェネレーターの存在を知っている人がいたとしても、ダウンロードできるものは何もありません。
パート2:今このサイトはどう運用されているか
リビルドをきちんとやる価値があった本当の理由はこれです:このサイトは、もう手作業でも保守されていません。サイトを建てたのと同じエージェントワークフローが更新も回しています — 今あなたが読んでいるこの改訂も含めて。
今回の改訂は監査スウォームから始まりました。ひとつの班が公開済みの全ページをファクトチェックして外部リンクを全部クリックし、別の班がスクリーンショットと、サイトの動作に関する確かな技術情報を集める。それが1つの仕様書に集約されます:これを作れ、数字をでっち上げるな、秘密は絶対に公開するな。
そこから作業がファンアウトします — プラットフォーム担当エージェント(カテゴリ構成、第二言語、検索エンジン周りの配管)、デザイナー、そして記事ごとに1体のライター。この記事も例外ではありません。速いモデルが事実ファイルから各ページの初稿を書き、より強いモデルが文体と正確さを締め直す。そのあと敵対的なパスが壊しにかかります — 数字を元の事実と突き合わせ、リンクを再クリックし、ハルシネーションくさい記述を狩る。というか正直に言えば、「AIが書いたっぽい」記述を狩る。まあ、実際AIが書いているんですが。
あなたのエージェントにも同じ仕事を
パッチ当てを続けたくない古いWordPressサイトを抱えていませんか?これが私がうちのエージェントに渡したレシピです。各ステップを説明して、コードはエージェントに書かせましょう。
- エクスポートする。WordPress管理画面 → ツール → エクスポート → すべてのコンテンツ。WXRファイルはエージェントが読める場所に置きます(例:
_build/source.xml)。 - メディアをミラーする。旧サーバーの
wp-content/uploads/をassets/uploads/にコピー。年/月の構造は保つこと。 - 移行スクリプトを頼む。WXRのアイテムをパースし、下書きと空記事をスキップし、カテゴリを短くきれいな一覧にマッピングし、アイキャッチ画像を解決し(なければ本文最初の画像にフォールバック)、アップロードURLと内部リンクを書き換え、Gutenbergブロックコメントを除去し、抜粋を自動生成して、frontmatter付きMarkdownファイルを書き出す。
- ジェネレーターを頼む。サイト設定とコンテンツフォルダを読み、テンプレート(base・home・category・post・page+共有のナビ/フッター/カードのパーシャル)でレンダリングし、プリティURLのフォルダを出力し、sitemap.xml・robots.txt・Webマニフェストを生成する。
- デザインはゼロから作り直す。旧テーマのCSSは移植しない — きれいなスタイルシート1枚、セルフホストのオープンフォント、インラインSVGアイコン、外部リクエストゼロ。
- 全ページをプレビューする。ビルドしてローカル配信する1行スクリプトを用意して、全部クリック。
- サーバー設定を書く。圧縮、キャッシュヘッダー、セキュリティヘッダー、ソースファイルの拒否ルール、拡張子なしURLのフォールバック。
- マニフェスト付きでデプロイする。ソースフォルダとスクリプトを明示的に除外して出力をrsyncし、本番URLが200を返すことを確認。
- エクスポートは捨てない。WXRファイルはプロジェクトに残します — 移行スクリプトを再実行すれば、いつでもコンテンツフォルダをきれいに再生成できます。
ランタイムの依存関係リスト、全文:
pip install jinja2 pyyaml markdown
以上です。
ハマりどころ
- uploadsフォルダはエクスポートに含まれない。WXRにあるのは画像のURLだけ。実ファイルのミラーを忘れると、ビルドは成功するのにサムネイルが全部404になります。時間をかけて学びました。
- 出力とソースが同じフォルダに同居する。ビルドされたHTMLは、PythonとMarkdownのソースと同じプロジェクトルートに落ちます。デプロイの分離はallow/denyリストがすべて。最初は気持ち悪く感じましたが、これのおかげでデプロイがただのrsyncで済んでいます。
- カテゴリのリネームで死んだフォルダが残ることがある。クリーンアップ処理にはカテゴリ名が直書きされています。設定側でリネームしてそのリストを忘れると、古いフォルダが公開されたまま孤児になります。
- 日付は日付ではなく文字列としてソートされる。全部
YYYY-MM-DDで書いてあるうちは問題なし。1個書き間違えると、最新記事が静かに一番下へ沈みます。