wjs-converting-wp-to-hugo について
wjs-converting-wp-to-hugoは、WordPressのエクスポートデータをHugoの静的サイトへ移行するスキルです。MarkdownとGitでの管理を目指す人向けに、WXRとuploadsフォルダーをオフラインで変換し、既存の記事URLを維持します。
主な機能
- 01WXRからMarkdownへの変換
- 02ローカル画像と添付ファイルのコピー
- 03既存記事URLの維持
- 04Hugoテーマとビルド手順の生成
- 05変換テストとビルド後リンクの検証
- 06機密入力とコンテンツ外ページの除外
活用シーン
- 01WordPressサイトの静的サイトへの移行
- 02GitとMarkdownによる記事管理
- 03検索や外部参照で使われる既存リンクの維持
- 04GitHub Pagesを通じたHugoサイトの公開
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ jianshuo/ claude-skills --skill wjs-converting-wp-to-hugoインストール後、Claude Code で/wjs-converting-wp-to-hugoを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
assets/gitignorehugo.tomlworkflow-hugo.ymlassets/layouts/index.htmlassets/layouts/_default/archives.htmlbaseof.htmllist.htmlsingle.htmlterms.htmlassets/layouts/partials/style.htmlscripts/verify_build.pywxr_to_hugo.pytests/test_wxr.py詳細
wjs-converting-wp-to-hugo
任意の WordPress サイトを Hugo + Markdown + git の静的サイトに移行し、GitHub Pages にデプロイします。 必要な入力は 2 つだけで、全工程をオフラインで実行でき、サードパーティへの依存はありません。
- WXR エクスポート — WordPress 管理画面の
ツール → エクスポート → すべてのコンテンツから取得した*.xml(すべての投稿/ページ/カテゴリー/タグの HTML 本文を含みます)。 uploads/フォルダー — サイトのwp-content/uploads/(年/月単位でディレクトリ分けされた画像と添付ファイル)。
生成物:content/*.md + static/wp-content/uploads/ + 手書きのシンプルなテーマ。Hugo でビルドし、GitHub Actions で公開します。
サイト全体の URL は /archives/<数字>/ のまま維持され、古いリンクは 100%そのまま使えます。
使用する場合
- WordPress サイトの動的機能/コメント/データベースをなくし、git + Markdown で管理したい場合。
- WXR
.xmlとuploads/を提供できる(または入手できる)場合。 - 古いリンクを維持する必要がある場合(SEO/外部からの参照)。
使用しない場合
- WXR がなく、オンラインのサイトしかない → まず WP 管理画面でエクスポートするか、REST API で JSON を取得してください(このスキルでは、より移植性の高い WXR を使います)。
- コメント/会員機能/検索などの動的機能を維持したい場合 → 静的サイトでは実現できないため、適していません。
- サイトがごく小規模(数件の記事)→ 手作業のほうが速いです。
基本原則
WXR を唯一の信頼できる情報源とし、uploads/ はそのまま静的リソースとして扱います。
画像はダウンロードも名前変更もしません。uploads/ を static/wp-content/uploads/ にコピーし、本文中の画像 URL をルート相対の /wp-content/uploads/... に変更するだけで、そのまま参照できます。記事の URL は <link> の内容をそのまま維持します。変換器は純粋関数 + 単体テストで構成し、テストを先に書きます。
パイプライン
WXR .xml + uploads/ → wxr_to_hugo.py → content/*.md + static/wp-content/uploads/ → hugo build → GitHub Actions → Pages
必ずユーザーに確認する 2 つの判断(黙って決めないこと)
WordPress には、静的サイトでは扱えないコンテンツが 2 種類あります。必ずユーザーに確認し、勝手に公開しないでください。
- パスワード保護された投稿(
<wp:post_password>が空でないもの)。静的サイトにはパスワード保護機能がないため、公開すると誰でも閲覧できます。 選択肢:除外(デフォルト。最も安全ですが、URL は 404 になります)/公開する/draftにする。 件数の確認には必ず ElementTree(つまりparse_items)を使い、grep をそのまま使わないでください:<wp:post_password>の値は CDATA で囲まれているため(<![CDATA[secret]]>)、grep '<wp:post_password>[^<]*</...>'を使うとすべて空として扱われ、「パスワード保護された投稿は 0 件」と誤判定し、実際にパスワードが設定された投稿を見落とします(maggiacito.com での実例。1 件を公開してしまうところでした)。 - WordPress のひな形ページ(
sample-page、プラグインのショートコードを含むlogin/register/findpasswordなどのページ、空ページ、ログインデザイナーのプレビュー用ページ)。 デフォルトでは除外します。これらはコンテンツではありません。is_real_page()は「本文が空/ショートコードが 1 つだけ/デフォルトの slug ブラックリスト」に基づいてフィルタリングします。
この 2 種類はいずれも変換器で除外するよう実装済みです。AskUserQuestion で確認してから全件を変換してください。
手順
1. 入力を配置し、プロジェクトを作成する
mkdir -p ~/code/<site> && cd ~/code/<site> && git init
# 把 WXR 拷进来(注意:WXR 含密码文章正文 + 作者邮箱,勿提交!见「安全」)
cp /path/to/<site>.WordPress.*.xml .
# uploads/ 放到工程根(含子目录 年/月)。注意它可能含 wordpress_db.sql —— 勿提交!
cp -R /path/to/uploads ./uploads
mkdir -p scripts tests content/posts layouts/_default layouts/partials static
このスキルのアセットをコピーします(ディレクトリ構成を維持してください)。
SK="$HOME/.claude/skills/wjs-converting-wp-to-hugo"
cp "$SK"/scripts/*.py scripts/ # wxr_to_hugo.py, verify_build.py
cp "$SK"/tests/test_wxr.py tests/ # 单元测试(须放 tests/,与 scripts/ 同级)
cp -R "$SK"/assets/layouts/. layouts/ # 手写主题
cp "$SK"/assets/hugo.toml . # 改 title / baseURL / 菜单
mkdir -p .github/workflows && cp "$SK"/assets/workflow-hugo.yml .github/workflows/hugo.yml
cp "$SK"/assets/gitignore .gitignore
printf '%s' '<你的域名,如 huixianju.cn>' > static/CNAME # 自定义域名
2. まずテストを実行する(変換器は TDD で開発します)
python3 tests/test_wxr.py # 期望 ALL PASS;改任何转换逻辑都先加失败测试
3. 件数を確認してから、全件を変換する
python3 scripts/wxr_to_hugo.py <site>.WordPress.*.xml
レポートに posts / pages / images / uploads_copied / warnings が出力されます。記事数が WP 管理画面の件数と一致することを確認してください。
warnings には、本文が空の記事、スキップされたひな形ページ、外部画像が表示されます。
4. ビルドし、古いリンクがすべてヒットすることを検証する
hugo --gc --minify # 没装:brew install hugo(要 extended)
python3 scripts/verify_build.py <site>.WordPress.*.xml # checked N posts, missing 0
5. ローカルで目視確認する
hugo server -p 1313
オンラインのサイトと照らし合わせて 5 件を確認します(画像付き記事 1 件、リンクが複数ある記事 1 件を含めます)。タイトル、リスト、リンク、画像、動画が正しく表示されるか確認してください。
重要:リンクはページ相対(../../...)であること、画像はオンラインからではなくローカルの /wp-content/uploads/ から読み込まれることを確認してください。
6. GitHub にプッシュする(公開リポジトリについては「セキュリティ」を参照)
gh repo create <site> --public --source=. --remote=origin
git push -u origin main
7. Pages → Actions を有効にする。先に有効にしてから実行する
gh api -X POST repos/<owner>/<site>/pages -f build_type=workflow
注意点:Pages を有効にする前に push すると、最初の Action は configure-pages で 404 エラーになり失敗します。Pages を有効にしたら、再実行してください。
gh workflow run "Deploy Hugo site to Pages" --repo <owner>/<site>
gh run watch <run-id> --repo <owner>/<site> --exit-status
仮 URL https://<owner>.github.io/<site>/ を検証します。ホーム、一つの記事、カテゴリー、index.xml、画像 1 枚がすべて 200 を返すことを確認してください。
(デプロイ直後、画像が一時的に 301 を返す場合があります。CDN のウォームアップによるもので、リダイレクトをたどれば最終的に 200 になります。)
8. DNS を切り替える(操作者が手動で実施。検証に合格してから行う)
まず仮 URL でサイト全体に問題がないことを確認します。WP は引き続きオンラインのままなので、リスクはありません。 その後、DNS プロバイダー(Cloudflare など)でドメインを Pages に向けます。
- apex:A レコード →
185.199.108.153 / 109.153 / 110.153 / 111.153、または CNAME →<owner>.github.io。 - Cloudflare のオレンジ雲プロキシを使う場合、SSL/TLS を Full に設定します。初回の証明書発行時は、一時的に灰色の雲(DNS のみ)にできます。
- DNS の反映後、Pages で Enforce HTTPS をオンにします。
- 数日間オンラインで安定してから、古い WP を停止します(まず停止してバックアップを残し、ロールバックが不要だと確認してから完全に削除します)。
変換器で遭遇した問題(wxr_to_hugo.py で修正済み。元に戻さないこと)
| 問題 | 現象 | 修正方法 |
|---|---|---|
| ハイパーリンクの href が失われる | <a href> がテキストだけになり、URL が失われる | <a> 内のテキストを蓄積し、閉じタグで [テキスト](href) を出力する |
ギャラリーに余分な - が付く(figure 版) | 画像付き記事で各画像の前に空のリスト記号が付く | figure スタックで wp-block-gallery を判定し、ギャラリー内の <li> では - を出力しない |
ギャラリーに余分な - が付く(ul 版) | 初期の Gutenberg は <figure> で囲まず、<ul> に wp-block-gallery を指定するため、前の行の figure 判定で検出できず、画像ごとに孤立した - が残る | _ul_stack でも <ul class=wp-block-gallery> を判定し、ギャラリー内の <li> では - を出力しない(maggiacito.com での実例) |
| CJK パーマリンクのエンコード | パーマリンクが URL エンコードされた中国語になっている(/sculpting-in-time/%e4%ba%8c…/)。そのまま維持すると、Hugo は文字どおり %e4%.. のディレクトリを作成します。サーバーはリクエスト内の %xx をデコードするため一致せず、古いリンクがすべて切れます | _norm_url() で unquote() を使ってパスをデコードし、中国語のディレクトリを Hugo に作成させます。静的ホストは受信した %xx をデコードして一致させるため、エンコード版とデコード版の両方の古いリンクが機能します。数字の /archives/<id>/ には影響しません(maggiacito.com での実例) |
| 画像がオンラインから読み込まれる | 本文中の画像が絶対 URL https://サイト/wp-content/... になっている | _root_relative() で自サイト上の画像を /wp-content/... に変更し、外部画像は絶対 URL のまま維持する |
| 動画/埋め込みが失われる | 本文中の <video>/<iframe> が空になる | HTML のまま透過し、hugo.toml で goldmark unsafe=true を有効にする |
| lastmod が空になる | WXR に wp:post_modified がない場合がある | 存在しない場合は wp:post_date にフォールバックする |
| クラシックエディターのソフト改行 | 本文中の裸の \n が無視される | handle_data で裸のテキストの改行を保持する |
| エンティティがデコードされない | タイトルに & が残る | html.unescape() を使用し、タイトル内の引用符をシングルクォートに置き換える |
| 新しい記事の投稿番号が飛ぶ | URL 番号を手動で付けると重複する可能性がある | next_archive_id() で既存の最大番号を調べ、+1 する |
リンクの移植性
hugo.toml で relativeURLs = true + canonifyURLs = false を設定すると、Hugo はすべてのリンクをページ相対
(../../archives/123/)で出力します。そのため、public/ 内のコンテンツは file://、サブパス(github.io/<repo>/)、カスタムドメインのいずれでもリンクをクリックできます。
画像 URL は変換器でルート相対に変更済みです。Hugo がさらに相対パスに変換するため、ローカルでもオンラインでも参照できます。
安全(公开仓库必读)
WXR 和原始 uploads/ 含敏感数据,绝不进 git:
- WXR
.xml含密码保护文章的正文(正是你从站点排除的内容)和作者邮箱。 uploads/wordpress_db.sql是整库 dump(用户、密码哈希)。转换器拷贝时已跳过.sql/.DS_Store。assets/gitignore用根锚定/uploads/(不能写uploads/,否则会连static/wp-content/uploads/一起忽略,图片就传不上去)。它还忽略*.WordPress.*.xml、public/、resources/。- 若 WXR 之前已被 commit:gitignore 对已跟踪文件无效,要先
git rm --cached <xml>。 - 推公开仓库前,若历史里有 WXR、含基础设施细节的设计文档(实例 ID / IP / 云命令)→ 重建干净历史:
确认git checkout --orphan _clean && git add -A && git commit -m "..." && git branch -D main && git branch -m maingit diff --cached --name-only | grep -iE 'WordPress.*xml|wordpress_db|\.sql$'为空再推。
主题
assets/layouts/ 是手写极简主题,零外部依赖(不用 submodule / PaperMod,省掉外来构建代码与权限麻烦)。
CJK 友好、首页文章列表、分类页、按年归档、文章页上下篇、/feed/ → /index.xml 老 RSS 兼容。
改 static/CNAME 为目标域名。
以后写新文章
ID=$(python3 -c "import sys;sys.path.insert(0,'scripts');import wxr_to_hugo;print(wxr_to_hugo.next_archive_id('content'))")
# 建 content/posts/$ID.md,front matter 写 url: /archives/$ID/,与老文章同一号段
git push # Action 自动构建上线
验收
- 文章数与 WP 后台一致;每个
/archives/<id>/在public/命中(verify_build.py) - 图片全本地化,无外链
wp-content;视频/iframe 透传可播 - 密码文章、脚手架页已按用户决定处理
- 链接页面相对,
file:///子路径/域名都可点 - WXR / db.sql / 原始 uploads 未进 git;公开仓库历史无敏感数据
- 临时 Pages 地址全站 200,DNS 切换后老链接 200