4475 文字
22 分
WordPress → Astro移行で得たもの、失ったもの、学んだこと

概要#

20年以上運用してきたWordPressブログ(661記事、2,515画像)をAstroに移行して、Cloudflare Pagesで配信するようにしました。ホスティング費用はゼロになり、デプロイは git push だけで完了するようになりました。

この記事では移行の背景、技術スタック、実際の作業手順、苦労した点をまとめます。

WordPressの何が問題だったのか#

静的コンテンツなのにDBが必要#

ブログの記事は基本的に静的コンテンツです。それなのにMySQLが必要で、DBのバックアップやメンテナンスが発生します。

スナップショットが面倒#

WordPressのデータは3箇所に分散しています。

  1. MySQL - 記事本文、メタデータ
  2. wp-content/uploads/ - 画像ファイル
  3. テーマ・プラグイン - PHP設定

バックアップするには mysqldump + rsync が必要で、復元するにも両方を揃えないとサイトが壊れます。Gitで丸ごと管理できない。

画像のサムネイル地獄#

WordPressは画像を1枚アップロードすると、自動的にサムネイルを大量生成します。

IMG_1234.jpg ← オリジナル
IMG_1234-150x150.jpg ← thumbnail
IMG_1234-300x200.jpg ← medium
IMG_1234-768x512.jpg ← medium_large
IMG_1234-1024x683.jpg ← large
IMG_1234-1536x1024.jpg
IMG_1234-2048x1365.jpg
...

2,515枚のアップロードに対して、サムネイルまで含めると数万ファイルになります。実際にこのブログでは参照されていない画像ファイルが3,250個(数 GB)も蓄積していました。

LLMとの相性が悪い#

LLMで記事を書いたり校正したりすることが増えてきましたが、WordPressだとDBの中にあるコンテンツを取り出してLLMに渡す必要があります。Markdownファイルならエディタやターミナルからそのまま渡せます。Claude Codeにディレクトリを渡すだけで記事を書いてもらえるのは圧倒的に楽です。

フロントエンドのアセットパイプラインがない#

WordPressにはビルドパイプラインがないので、画像の最適化(WebP変換、リサイズ)をビルド時に自動実行する仕組みがありません。プラグインで対応できますが、PHPベースの処理なので遅いし制約が多い。

リンク切れ・ゴミファイルが検出しづらい#

10年以上運用していると、記事から参照されなくなった画像やファイルがどんどん溜まっていきます。WordPressのメディアライブラリは「どの記事からも参照されていないファイル」を検出する機能がありません。

実際にこのブログを調査したところ、参照されていないファイルが 3,250個(数 GB) も蓄積していました。WordPressのDB内のHTMLと wp-content/uploads/ のファイルを突き合わせないと検出できないので非常に面倒です。

Astroに移行した後は、Markdownファイルからの参照をgrepで簡単に調べられるので、不要ファイルの検出がシンプルになりました。

移行先の検討#

MicroCMS + Next.js#

最初はHeadless CMSの MicroCMSNext.js の組み合わせを検討しました。しかし、Vercelのホスティング費用がかかります。個人ブログにそこまでコストをかけたくないです。

Astro + Cloudflare Pages#

Astroを試してみたところ、これがドンピシャでした。

まず小さめのWordPressサイトを5つほどAstroに移行してみて、問題がないことを確認してからメインブログの移行に取り掛かりました。トータルで10サイト近くAstroに移行しましたが、最後に残ったのが3サイトで、このメインブログはそのうちの1つです。

今回の移行では Claude Code(Opus 4.5)を全面的に活用しました。Opus 4.5は賢くて、移行作業の8割方は半自動で進められました。コンテンツ変換、frontmatter生成、リダイレクトルール作成、プラグイン実装といった定型作業は指示を出すだけでほぼ自動で完了し、半日で一通り動く状態になりました。ただし、残りの2割(UIの微調整、デザインの仕上げ、コンテンツの整理)にはもう半日かかりました。LLMが苦手なのは「見た目の良し悪し」や「コンテンツとして何を残すか」の判断で、そこは人間の目が必要です。

技術スタック#

最終的に採用した構成です。

レイヤー技術用途
SSGAstro 5.18静的サイト生成
テーマFuwariブログテンプレート
CSSTailwind CSS 4スタイリング
UISvelte 5インタラクティブコンポーネント
Markdown拡張MDX, remark, rehypeカスタムコンポーネント
画像最適化sharpビルド時にWebP変換・リサイズ
全文検索Pagefindクライアントサイド検索
コードハイライトExpressive Codeシンタックスハイライト
数式KaTeXLaTeX数式レンダリング
コメントGiscusGitHub Discussions連携
ホスティングCloudflare PagesCDN配信(無料枠)
CI/CDGitHub Actions自動ビルド・デプロイ
LinterBiomeフォーマット・lint

Remark/Rehypeプラグイン構成#

Markdownの拡張にはremark/rehypeのプラグインを多数使っています。

astro.config.mjs
markdown: {
remarkPlugins: [
remarkMath, // LaTeX数式
remarkReadingTime, // 読了時間計算
remarkExcerpt, // 記事の抜粋生成
remarkDirective, // カスタムディレクティブ
remarkSectionize, // セクション分割
remarkMdxGlobalComponents,
],
rehypePlugins: [
rehypeKatex, // KaTeX数式レンダリング
rehypeSlug, // 見出しにID付与
rehypeUnwrapImages, // 画像のリンクラッピング解除
rehypeImageSize, // 画像サイズ自動取得
[rehypeComponents, { // カスタムコンポーネント
components: {
github: GithubCardComponent,
amazon: AmazonCardComponent,
note: (x, y) => AdmonitionComponent(x, y, "note"),
tip: (x, y) => AdmonitionComponent(x, y, "tip"),
},
}],
[rehypeAutolinkHeadings, { behavior: "append" }],
],
},

コンテンツスキーマ#

Astroの Content Collections でfrontmatterのスキーマを定義しています。

src/content/config.ts
const postsCollection = defineCollection({
schema: z.object({
title: z.string(),
published: z.date(),
updated: z.date().optional(),
draft: z.boolean().optional().default(false),
description: z.string().optional().default(""),
image: z.string().optional().default(""),
tags: z.array(z.string()).optional().default([]),
category: z.string().optional().nullable().default(""),
}),
});

WordPressの wp_posts テーブルにバラバラに格納されていたメタデータが、frontmatterとして1ファイルにまとまりました。

---
title: ブログのCMSをWordPressからAstroに移行した
published: 2026-03-10
category: "Programming"
tags: ["WordPress", "Astro", "Cloudflare"]
image: "../../assets/uploads/2026/03/cover.png"
---
本文はMarkdownで書く。これが全て。

移行作業の実際#

移行は74コミットで完了しました。Gitのログから主要な作業フェーズを振り返ります。

Phase 1: 初期移行#

43a2057 feat: switch to Fuwari blog template with migrated content
be433be feat: add blog uploads (images) via Git LFS
f2f02b0 feat: set featured images from WordPress thumbnail metadata
13c2cfd fix: correct featured image URLs using wp_get_attachment_url
797fe8f fix: convert WordPress/Cocoon shortcodes to standard Markdown/HTML

Claude Codeに「WordPressのディレクトリをAstro Fuwariテーマで動くように移行して」と指示しました。WordPressのHTMLをMarkdownに変換して、frontmatterを生成する作業を自動でやってくれます。

ただし、一発でうまくいくわけではないです。WordPress/Cocoonテーマ固有のショートコードやブロックパターンの変換が必要でした。

Phase 2: 機能追加#

ad849b0 feat: add Amazon MDX component, OGP, GTM, redirects, 404 page
a064cc3 feat: generate OG images for 39 posts without featured images
6046ae5 feat: add Giscus comment system powered by GitHub Discussions
88fea73 feat: add related posts, Japanese nav, remove GitHub link from header

WordPressで使っていた機能をAstroで再実装しました。

Amazonアフィリエイトコンポーネント#

WordPressではAmazonリンクをプレーンなHTMLやプラグインで書いていましたが、Astroでは自作のrehypeプラグインでカスタムコンポーネントとして処理しています。Markdownで ::amazon{asin="..."} と書くだけで商品カードがレンダリングされます。

::amazon{asin="B0CKWS1VWR"}

商品データ(タイトル、価格、画像URL、レビュー数)はAmazon PA-APIから事前にフェッチして amazon-products.json に保存しておき、ビルド時に読み込みます。

// src/plugins/rehype-component-amazon.mjs(抜粋)
let amazonProducts = {};
try {
const dataPath = join(process.cwd(), "src", "data", "amazon-products.json");
amazonProducts = JSON.parse(readFileSync(dataPath, "utf-8"));
} catch (error) {
console.warn("[AMAZON] Failed to load amazon-products.json:", error.message);
}
export function AmazonCardComponent(properties, children) {
const asin = properties.asin;
const productData = amazonProducts[asin];
const affiliateUrl = productData?.detailPageUrl
|| `https://www.amazon.co.jp/dp/${asin}?tag=${AFFILIATE_TAG}`;
const title = productData?.title || "";
const imageUrl = productData?.imageUrl || "";
// ... hastscriptでカード要素を構築
return h("a", { class: "card-amazon", href: affiliateUrl, target: "_blank" }, [
nImage, h("div", { class: "az-info" }, [nTitle, ...priceElements, nButton]),
]);
}

MDXファイルでは <Amazon asin="..." /> というJSXコンポーネントも使えるように、remarkプラグインで自動importを挿入しています。

実際のレンダリング結果はこんな感じです。

Astro フロントエンド開発の教科書

Astro フロントエンド開発の教科書

🛒 Amazonで購入

URLの自動埋め込み(astro-embed)#

YouTubeやX(Twitter)のURLをMarkdownにそのまま貼るだけで、自動的にembedカードに変換されます。astro-embedを使っています。

<!-- こう書くだけでYouTubeが埋め込まれる -->
https://www.youtube.com/watch?v=9TVkrzxjNRo
<!-- Xのポストも同様 -->
https://x.com/matsubokkuri/status/1893168256317563150

WordPressのoEmbedと同じ体験がMarkdownで実現できます。

Phase 3: 画像最適化#

7bf0764 feat: move images to src/assets for build-time optimization
7214b99 refactor: move OG images from public/og to src/assets/og
318eb07 chore: remove 3250 unreferenced upload files (566 MB)

これが一番大変でした。WordPressの wp-content/uploads/ にあった画像を src/assets/uploads/ に移動することで、Astroのビルドパイプライン(sharp)による最適化対象にしました。

画像をpublicディレクトリからsrc/assetsに移すことで、ビルド時に自動的にWebP変換とリサイズが行われます。

参照されていない画像ファイルが3,250個(数 GB)もあったので、全て削除しました。さらにGit履歴からも git-filter-repo で完全に消して、リポジトリサイズを3.7GBから2.4GBに縮小しました。

Phase 4: URL互換性#

1e74d38 refactor: rename multibyte post filenames to ASCII
9a54209 fix: update redirects for renamed Japanese slug posts

WordPressは /YYYY/MM/DD/slug/ というURL構造でしたが、Astroでは /posts/slug/ になります。SEO上の影響を最小化するため、973件の301リダイレクトルールを生成しました。

public/_redirects
/2003/06/05/php/ /posts/php/ 301
/2024/08/02/stop-using-url-shortener-services/ /posts/stop-using-url-shortener-services/ 301
...

canonical URLも旧WordPress形式で出力するようにして、Googleの検索インデックスを引き継ぎました。

src/pages/posts/[...slug].astro
const pubDate = entry.data.published;
const yyyy = pubDate.getFullYear().toString();
const mm = (pubDate.getMonth() + 1).toString().padStart(2, "0");
const dd = pubDate.getDate().toString().padStart(2, "0");
const canonicalUrl = new URL(
`/${yyyy}/${mm}/${dd}/${entry.slug}/`, Astro.site
).href;

Phase 5: Tailwind CSS v3 → v4#

36bfa03 feat: migrate Tailwind CSS v3 to v4
d3f9e66 fix: move text-* utilities from @layer components to @utility for TW4

Fuwariテーマ自体がTailwind CSS v3ベースだったので、v4への移行が必要でした。

Tailwind 4では @layer components の中に書いたスタイルの優先度が大きく変わりました。@layer 内のスタイルは @layer 外のスタイルより常に優先度が低いため、開発サーバーでは動くのに本番ビルドでスタイルが崩れるという厄介なバグにハマりました。

/* BAD: @layer内のスタイルは優先度が低い */
@layer components {
.meta-icon {
@apply mr-1.5; /* 本番ビルドで効かない! */
}
}
/* GOOD: @layer外に書く */
.meta-icon {
margin-right: 0.375rem;
background-color: var(--btn-regular-bg);
color: var(--btn-content);
}

Phase 6: 仕上げ#

d49c8a2 feat: add social sharing, sponsor button, and improve Amazon cards
1d10317 feat: add PA-API product data fetcher for Amazon affiliate cards
20a7448 refactor: update 121 internal links from old WordPress URLs to new paths
16ca3bf feat: add internal blog post link card component
9185ee9 refactor: remove dead rehypeImageSize plugin, fix tweet script duplication

最後の仕上げとして、以下の作業をしました。

  • SNSシェアボタン: X(Twitter)シェアとはてなブックマークボタンを記事ページに追加
  • Amazonアフィリエイト強化: Amazon PA-APIから商品データ(タイトル、価格、画像、レビュー数)を自動取得するスクリプトを作成
  • 内部リンクカード: 自サイトへのURLを自動的にブログカード形式で表示するremarkプラグインを実装。OGPフェッチ不要で、ビルド時にfrontmatterから直接データを取得
  • 旧URLの一括更新: 記事中の121箇所の旧WordPress URL(/YYYY/MM/DD/slug/形式)を新しいパス(/posts/slug/形式)に書き換え
  • 画像のフルサイズ配信: ライトボックス(PhotoSwipe)でオリジナル解像度の画像を表示できるように、画像幅の800px制限を撤廃
  • OGP画像の正常化: 記事のfeature imageを og:image に使用するように修正。フォールバック用のデフォルト画像も配置
  • X(Twitter)埋め込み: widgets.js を動的ロードして、ツイートの画像・メディアが正しく表示されるように対応

デプロイパイプライン#

GitHub Actionsでビルドして、Cloudflare Pagesにデプロイしています。

.github/workflows/deploy.yml
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22 }
- uses: pnpm/action-setup@v4
- run: pnpm install --frozen-lockfile
- run: pnpm build # astro build && pagefind --site dist
- uses: cloudflare/wrangler-action@v3
with:
command: pages deploy dist --project-name=blog
# CDNキャッシュも自動パージ
- run: |
curl -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE/purge_cache" \
-H "Authorization: Bearer $TOKEN" \
--data '{"hosts":["blog.teraren.com"]}'

git push するだけで、ビルド → デプロイ → CDNキャッシュパージまで自動で走ります。

実際のGitHub Actionsワークフローの実行画面です。Biome、textlint、Build、Deployの4ジョブが並列・直列で実行されています。

GitHub Actions Workflow

WordPress機能の代替#

「静的サイトだとWordPressの○○ができないのでは?」という懸念があると思います。主要な機能の代替方法をまとめます。

全文検索 → Pagefind#

WordPressの検索はMySQL上で LIKE クエリを実行していましたが、静的サイトでも全文検索はできます。Pagefindはビルド時に静的なインデックスファイルを生成し、クライアントサイドでFTS(Full Text Search)を実行します。

package.json
{
"scripts": {
"build": "astro build && pagefind --site dist"
}
}

ビルドの最後に pagefind を実行するだけで、日本語対応の検索インデックスが自動生成されます。検索UIはSvelteで実装して、インクリメンタルサーチに対応しています。DBを使わない検索がこんなに手軽にできるとは思っていませんでした。

コメント → Giscus#

WordPressの組み込みコメント機能の代わりにGiscusを使っています。GitHub Discussionsをバックエンドにしたコメントシステムで、記事のパス名にマッピングされます。

src/components/Giscus.astro
<script
src="https://giscus.app/client.js"
data-repo="matsubo/blog-giscus"
data-mapping="pathname"
data-theme="preferred_color_scheme"
data-lang="ja"
data-loading="lazy"
async
></script>

コメントデータはGitHubに保存されるので、自前でDBを管理する必要がありません。ダークモードの切り替えにも postMessage で連動させています。

Before / After#

WordPressAstro
記事数661(MySQL内)664(.md/.mdx)
画像2,515 + サムネイル数万1,632(最適化済み)
ホスティングVPS (Docker)Cloudflare Pages
デプロイ手動 or プラグインgit push
バックアップmysqldump + rsyncgit clone
コスト~$10/月$0
全文検索MySQL LIKEPagefind(クライアントサイド)
コメントWordPress組み込みGiscus(GitHub Discussions)
ビルド出力349 MB(静的HTML)

苦労した点#

304記事のMarkdownフォーマット修正#

WordPressからの変換で壊れたMarkdownを手作業で修正しました。インデントのずれ、HTMLタグの残骸、Cocoonショートコードの変換漏れなど。304記事のフォーマットを一括修正するコミットが生まれました。

画像パスの移行#

WordPressは画像を wp-content/uploads/YYYY/MM/ に格納しますが、Astroで画像最適化を有効にするには src/assets/ 配下に移す必要があります。パスの書き換えと、参照されていない画像の特定が大変でした。

日本語スラッグの扱い#

WordPressは日本語スラッグを許容しますが、ファイル名に日本語が入っているとビルドやデプロイで問題が起きます。マルチバイトのスラッグを全てASCIIにリネームして、リダイレクトルールを追加しました。

良い点#

  • コストゼロ: Cloudflare Pagesの無料枠で十分。月10ドルの節約
  • 速い: 静的サイトでCDN配信。体感で明らかに速い
  • Gitで管理: git log で記事の変更履歴が全て追える
  • LLMフレンドリー: ディレクトリごとClaude Codeに渡すだけで記事を書いてもらえる
  • アセット最適化: sharpによるビルド時WebP変換・リサイズ

悪い点#

  • WYSIWYGエディタがない: WordPressのJetpackアプリのように、スマホからサクッと記事を書けない。Markdownエディタが必要
  • ビルド時間: 664記事のビルドはそれなりに時間がかかる。GitHub Actionsのランナーで数分

Lighthouseスコア#

移行後のLighthouseスコアです。

Lighthouse Scores

メトリクススコア
Performance85
Accessibility93
Best Practices81
SEO100
First Contentful Paint2.6s
Largest Contentful Paint3.4s
Total Blocking Time200ms
Cumulative Layout Shift0.001

SEOは満点。静的HTMLをCDNから配信しているので、CLSはほぼゼロです。PerformanceはSwupのページ遷移アニメーションやWebフォントの読み込みがあるので85ですが、体感は十分速いです。

個人ブログとZennの棲み分け#

Astroで自前ブログを構築したものの、技術記事の発信先としてZennも併用しており、棲み分けは悩みどころです。

個人ブログZenn
読者層検索流入、既存読者Zennコミュニティ、エンジニア
SEO自分でコントロール可能Zennドメインの強さに乗れる
デザイン完全に自由Zennのフォーマット固定
収益化アフィリエイト自由バッジ(投げ銭)
所有権自分のドメイン・リポジトリプラットフォーム依存

現時点の方針としては、技術的にニッチな記事やガジェットレビュー、生活系の記事は個人ブログへ、汎用的な技術Tipsや他のエンジニアに届けたい記事はZennへ書くようにしています。ただ、明確な基準があるわけではなく、書きながら判断しているのが正直なところです。

textlintで日本語の品質を底上げ#

移行後の仕上げとして、日本語文章のlinterであるtextlintを導入しました。コードにESLintがあるように、日本語の文章にもlinterを入れることで、タイポや読みにくい表現を機械的に検出できます。

導入したプラグイン#

パッケージ用途
textlint-rule-preset-ja-technical-writing技術文書向けの日本語ルール集
textlint-plugin-mdxMDXファイルの解析対応
textlint-filter-rule-commentsHTMLコメントによるルール無効化

設定方針#

テックブログは書籍のような堅い文体を求められないため、ガチガチのフォーマットルールは無効にし、読みやすさとタイポ検出に絞りました。

有効にしたルール(読みやすさ・タイポ):

  • ja-no-successive-word — 同じ単語の連続(「にに」「をを」等のタイポ)
  • ja-no-redundant-expression — 冗長表現(「○○を行う」→「○○する」)
  • no-unmatched-pair — 括弧の閉じ忘れ
  • sentence-length — 長すぎる文(200文字超)
  • no-dropping-the-ra — ら抜き言葉
  • no-invalid-control-character — 不正な制御文字

無効にしたルール(ブログには不要):

  • no-mix-dearu-desumasu — ですます/である調の混在(ブログでは自然)
  • preset-jtf-style — JTF日本語標準スタイルガイド全般(半角/全角かっこ、箇条書き句点統一など)
  • max-ten / max-comma — 読点・カンマの数制限

効果#

661記事に対して実行したところ、初回で約300件のエラーが検出されました。内訳は冗長表現、タイポ(助詞の重複や文字の二重入力)、括弧の閉じ忘れが大半です。WordPress時代には気づけなかった問題が一括で洗い出せたのは、Markdownファイルとしてファイルシステム上にコンテンツがあるからこそです。

CIにもBiomeと並列でtextlintを実行するジョブを追加しているため、今後の記事でも品質が自動的に担保されます。

// .textlintrc.json(抜粋)
{
"rules": {
"preset-ja-technical-writing": {
"sentence-length": { "max": 200 },
"no-mix-dearu-desumasu": false,
"ja-no-successive-word": { "allow": ["。", "!", "?", "…", "・", "○"] },
"ja-no-redundant-expression": true,
"no-unmatched-pair": true,
"no-dropping-the-ra": true
},
"preset-jtf-style": false
}
}

まとめ#

WordPressからAstroへの移行は大正解でした。

20年分のコンテンツをMarkdownファイルとしてGitリポジトリに格納し、Cloudflare CDNから配信する。DBもVPSも不要。Claude Codeに移行作業を手伝ってもらったおかげで、661記事の移行も現実的な工数で完了しました。

LLMの活用が進む中で、コンテンツがMarkdownファイルとしてファイルシステム上にあることの価値はますます大きくなると思います。WordPressのDB内にコンテンツが閉じ込められている状態は、もう時代に合っていないと感じます。

WordPress → Astro移行で得たもの、失ったもの、学んだこと
https://blog.teraren.com/posts/wordpress-to-astro-migration/
作者
Yuki Matsukura
公開日
2026-03-10
ライセンス
CC BY-NC-SA 4.0
この記事が役に立ったら
GitHub Sponsorsで応援できます

コメント