はじめに
このブログ「C#もぐもぐ」は、もともとWordPressで運営していました。
今回、Astro 5 + MDXによる静的サイトへ全面移行し、ホスティングもCloudflare Pagesに切り替えました。
この記事では、移行の動機から技術選定、変換スクリプトの実装、デプロイまでの一連の作業を記録として残します。
移行の動機
WordPressを使い続ける中で、いくつかの課題を感じていました。
- PHPやデータベースの管理コスト(アップデート対応、セキュリティパッチなど)
- 表示速度の改善が難しい
- Markdownで記事を書きたい
- 静的ページが多いのに動的CMSを使う疑問
静的サイトジェネレーターに移行すれば、サーバー管理から解放され、CDN配信による高速な表示が実現できます。
記事もMarkdownで管理でき、Gitでバージョン管理できるようになります。
そのため、今回、Claudeと相談しながら静的CMSに移行することにしました。
移行元技術スタック
移行元の技術スタックは、下表のとおりです。
| 項目 | 技術 |
|---|---|
| CMS | WordPress |
| コンテンツ形式 | HTML / Gutenbergブロック |
| ホスティング | さくらのレンタルサーバー |
技術スタック
移行先の技術スタックは、下表のとおりです。
| 項目 | 技術 |
|---|---|
| フレームワーク | Astro 5(output: "static") |
| コンテンツ形式 | MDX / Markdown |
| UIフレームワーク | AdminLTE 4(rc4)+ Bootstrap 5.3 |
| スタイル | SCSS(sass) |
| パッケージマネージャー | pnpm 10 |
| ホスティング | Cloudflare Pages |
| デプロイ | wrangler / GitHub連携 |
| 画像最適化 | sharp |
| 型チェック | TypeScript + astro check |
Astroを選んだ理由は、次の3点です。
- コンテンツ主体のサイトに最適化されていること
- MDXによる柔軟な記述が可能なこと
- ビルド時に静的HTMLを出力するためパフォーマンスが高いこと
今回、UIにはAdminLTE 4を採用しました。
Bootstrap 5ベースでレスポンシブ対応が容易であり、ナビゲーションやカードレイアウトなどの既成コンポーネントが充実しています。
プロジェクト構成
移行後のディレクトリ構成は、次のようになっています。
src/
├── content/
│ ├── config.ts # コンテンツコレクションのスキーマ定義
│ └── posts/ # 記事ファイル(Markdown/MDX)
│ ├── 6.md
│ ├── 7.md
│ └── ...
├── layouts/
│ └── AdminLayout.astro # 共通レイアウト(OGP・メタ情報含む)
├── pages/
│ ├── index.astro # トップページ
│ ├── about.astro # Aboutページ
│ ├── posts/
│ │ ├── index.astro # 記事一覧
│ │ └── [...slug].astro # 記事詳細(動的ルーティング)
│ ├── tags/
│ │ ├── index.astro # タグ一覧
│ │ └── [...tag].astro # タグ別記事一覧
│ └── rss.xml.js # RSSフィード
├── styles/
│ ├── global.scss # グローバルスタイル
│ └── post-content.scss # 記事本文のスタイル
├── types.ts
└── utils/
└── date.ts
記事はAstroのコンテンツコレクションで管理しています。
スキーマはZodで定義しており、型安全にfrontmatterを扱えます。
// src/content/config.ts
import { defineCollection, z } from "astro:content";
export const postsSchema = z.object({
title: z.string(),
date: z.date(),
description: z.string().optional(),
excerpt: z.string().optional(),
image: z.string().optional(),
tags: z.array(z.string()).optional().default([]),
draft: z.boolean().optional().default(false),
});
const posts = defineCollection({
type: "content",
schema: postsSchema,
});
export const collections = { posts };
WordPress記事の変換
変換スクリプト
WordPressからのコンテンツ移行には、Python製の変換スクリプトを自作しました。
処理の流れは、次のとおりです。
- WordPressのエクスポートXML(WXR形式)をパース
- 各記事のタイトル、本文、日付、カテゴリ、excerptを抽出
- HTML/Gutenbergブロックを
markdownifyライブラリでMarkdownに変換 - Astro互換のfrontmatter付きMarkdownファイルとして出力
- 本文中の画像URLを抽出し、ダウンロード対象リストをJSONで生成
ファイル名にはWordPressのpost_idをそのまま使用することにしました。
これにより、旧URLからのリダイレクトを簡潔に設定できます。
# 変換実行
python wp2md_convert.py wordpress_export.xml ./output/posts
画像の処理
画像のダウンロードとWebP変換は、別のスクリプトで並列処理しています。
python download-images.py ./output/images_to_download.json \
./public/images/posts ./output/posts
主な処理内容は、次のとおりです。
- 最大5並列でダウンロード(リトライ3回)
- 横幅1200pxを超える画像はリサイズ
- WebP形式に変換(quality 85)
- Markdownファイル内の画像パスを自動更新
- 失敗した画像はJSONファイルに記録
変換結果
| 項目 | 値 |
|---|---|
| 変換記事数 | 61件 |
| 画像URL数 | 184件 |
| 出力形式 | Astro互換frontmatter |
WordPress互換URLのリダイレクト
WordPressでは/{post_id}/というURL構造を使っていました。
Astro移行後は/posts/{post_id}/になるため、旧URLからの301リダイレクトを設定しています。
Cloudflare Pagesの_redirectsファイルで対応しました。
# public/_redirects
/6/ /posts/6/ 301
/7/ /posts/7/ 301
# ... 61件のリダイレクトルール
Cloudflare Pagesの_redirectsは正規表現に対応していないため、個別のルールを記述する必要があります。
ビルド最適化
本番ビルドでは、次の最適化をastro.config.mjsで設定しています。
チャンク分割
manualChunks: (id) => {
if (id.includes("node_modules")) {
if (id.includes("admin-lte")) return "vendor-adminlte";
if (id.includes("bootstrap")) return "vendor-bootstrap";
return "vendor";
}
},
AdminLTEとBootstrapを別チャンクに分離することで、キャッシュ効率を高めています。
圧縮
gzipとBrotliの事前圧縮を行っています。
閾値は10KBに設定し、小さなファイルは圧縮対象外としています。
plugins: [
compress({ algorithm: "gzip", ext: ".gz", threshold: 10240 }),
compress({ algorithm: "brotliCompress", ext: ".br", threshold: 10240 }),
],
その他の最適化
- terserによる
console.logとdebuggerの自動削除 - ソースマップの無効化
- CSSコード分割の有効化
- アセットファイルの種類別ディレクトリ配置(
assets/css/、assets/js/、assets/images/、assets/fonts/)
デプロイ
Cloudflare Pagesへのデプロイは、GitHub連携による自動デプロイと手動デプロイの2つの方法を用意しています。
wrangler.toml
name = "blog-223n-tech"
compatibility_date = "2024-01-01"
pages_build_output_dir = "dist"
[vars]
PUBLIC_SITE_URL = "https://blog.223n.tech/"
手動デプロイ
pnpm build
npx wrangler pages deploy dist --project-name=blog-223n-tech
最終的なビルド結果は85ページになりました。
開発環境
開発は、DevContainerで行っています。
主要なコマンドは、次のとおりです。
pnpm dev # 開発サーバー(port 4321)
pnpm build # 本番ビルド
pnpm check # 型チェック(astro check + tsc --noEmit)
pnpm format # Prettierフォーマット
移行してよかったこと
実際に移行を終えてみて、次の点でメリットを感じています。
- 表示速度の向上: 静的HTML + CDN配信により、体感速度が大幅に改善
- 管理コストの削減: データベースやPHPランタイムの管理が不要に
- 記事の書きやすさ: Markdown + VS Codeで快適に執筆できる
- バージョン管理: 記事も設定もGitで一元管理
- デプロイの簡単さ:
git pushするだけで自動デプロイ
おわりに
WordPressから61記事、184枚の画像を移行し、URL互換性も維持した状態でAstro + Cloudflare Pagesへの移行が完了しました。
変換スクリプトを自作したことで、frontmatterの形式やファイル名の規則を自由にコントロールでき、結果的にスムーズな移行ができました。
同様の移行を検討している方の参考になれば幸いです。