はじめに

このブログ「C#もぐもぐ」は、もともとWordPressで運営していました。

今回、Astro 5 + MDXによる静的サイトへ全面移行し、ホスティングもCloudflare Pagesに切り替えました。

この記事では、移行の動機から技術選定、変換スクリプトの実装、デプロイまでの一連の作業を記録として残します。

移行の動機

WordPressを使い続ける中で、いくつかの課題を感じていました。

  • PHPやデータベースの管理コスト(アップデート対応、セキュリティパッチなど)
  • 表示速度の改善が難しい
  • Markdownで記事を書きたい
  • 静的ページが多いのに動的CMSを使う疑問

静的サイトジェネレーターに移行すれば、サーバー管理から解放され、CDN配信による高速な表示が実現できます。

記事もMarkdownで管理でき、Gitでバージョン管理できるようになります。

そのため、今回、Claudeと相談しながら静的CMSに移行することにしました。

移行元技術スタック

移行元の技術スタックは、下表のとおりです。

項目技術
CMSWordPress
コンテンツ形式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点です。

  1. コンテンツ主体のサイトに最適化されていること
  2. MDXによる柔軟な記述が可能なこと
  3. ビルド時に静的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製の変換スクリプトを自作しました。

処理の流れは、次のとおりです。

  1. WordPressのエクスポートXML(WXR形式)をパース
  2. 各記事のタイトル、本文、日付、カテゴリ、excerptを抽出
  3. HTML/GutenbergブロックをmarkdownifyライブラリでMarkdownに変換
  4. Astro互換のfrontmatter付きMarkdownファイルとして出力
  5. 本文中の画像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.logdebuggerの自動削除
  • ソースマップの無効化
  • 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の形式やファイル名の規則を自由にコントロールでき、結果的にスムーズな移行ができました。

同様の移行を検討している方の参考になれば幸いです。

参考サイト