Blog を Next.js/Vercel から Astro/Cloudflare Pages に移行した

|

しばらく Next.js/Vercel で運用していた個人ブログを、Astro/Cloudflare Pages に移行した。

移行の背景

MDX と contentlayer の苦しみ

Next.js で MDX を使ったブログを運用していたが、contentlayer のサポートが厳しい状況になっていた。 contentlayer は開発が停滞しており、Next.js のバージョンアップに追従できなくなっていた。

一方、Astro は MDX を第一級でサポートしている。 @astrojs/mdx インテグレーションを追加するだけで、すぐに使い始められる。

// astro.config.mjs
import { defineConfig } from 'astro/config';
import mdx from '@astrojs/mdx';
import sitemap from '@astrojs/sitemap';
import tailwindcss from '@tailwindcss/vite';

export default defineConfig({
  site: 'https://nabetama.com',
  integrations: [mdx(), sitemap()],
  output: 'static',
  vite: {
    plugins: [tailwindcss()],
  },
});

SSG に Next.js はリッチすぎる

正直なところ、Next.js への移行は勉強目的だった。 しかし実際に運用してみると、SSG しか使わない個人ブログには明らかにオーバースペックだった。

Server Components、App Router、Streaming、ISR… Next.js が提供する機能のほとんどを使っていなかった。 静的サイトを生成してホスティングするだけなのに、フレームワークの複雑さだけが残っていた。

Astro は「コンテンツ重視のウェブサイト」に特化している。 SSG がデフォルトであり、必要なときだけ Islands Architecture でインタラクティブな要素を追加できる。 このブログにはぴったりだった。

Cloudflare への統一

ドメインも DNS も Cloudflare で管理している。 ホスティングも Cloudflare Pages に統一することで、管理が楽になった。 無料枠も十分で、エッジネットワークによる配信速度も申し分ない。

Astro の Content Collections

Astro の Content Collections は、型安全なコンテンツ管理を実現してくれる。 Zod でスキーマを定義すると、frontmatter の型チェックが効く。

// src/content.config.ts
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';

const posts = defineCollection({
  loader: glob({ pattern: '**/*.mdx', base: './src/content/posts' }),
  schema: z.object({
    title: z.string(),
    date: z.string(),
    lastModDate: z.coerce.string().nullable().optional(),
    description: z.string().optional(),
    tags: z.array(z.string()).optional(),
    thumbnail: z.string().optional(),
  }),
});

export const collections = { posts };

記事の取得も直感的だ。

---
// src/pages/posts/[...slug].astro
import PostLayout from '@/layouts/PostLayout.astro';
import { getCollection, render } from 'astro:content';

export async function getStaticPaths() {
  const posts = await getCollection('posts');
  return posts.map((post) => ({
    params: { slug: post.id },
    props: { post },
  }));
}

const { post } = Astro.props;
const { Content } = await render(post);
---

<PostLayout title={post.data.title} date={post.data.date}>
  <Content />
</PostLayout>

RSS フィードも @astrojs/rss で簡単に生成できる。

// src/pages/feed.xml.ts
import rss from '@astrojs/rss';
import type { APIContext } from 'astro';
import { getCollection } from 'astro:content';

export async function GET(context: APIContext) {
  const posts = await getCollection('posts');

  return rss({
    title: 'nabetama.com',
    description: 'nabetama が日々の生活をメモするウェブサイト',
    site: context.site!,
    items: posts.map((post) => ({
      title: post.data.title,
      pubDate: new Date(post.data.date),
      description: post.data.description,
      link: `/posts/${post.id}/`,
    })),
  });
}

移行作業

移行作業は Claude Code にほぼ任せた。

既存の Next.js プロジェクトから Astro プロジェクトへの書き換え、コンテンツの移行、ビルド設定の調整などを Claude Code に任せた。 MDX ファイルの frontmatter は若干の調整が必要だったが、本文はほぼそのまま移行できた。

一方、新しいデザインの調整はほぼ自分で書いた。 デザインの「良し悪し」は主観的なものであり、ほぼ自分しか見てないような零細個人サイトだし、 何より自分がCSSスキルが低いので、ここで少しでも勉強したい。

移行して良かったこと

これまで実装していた機能はすべて満たせている。

  • MDX による記事執筆
  • RSS フィード生成
  • サイトマップ生成
  • OGP 画像の設定
  • ダークモード対応
  • レスポンシブデザイン

また、移行を機にパンくずリストを追加した。 記事ページでの現在位置がわかりやすくなった。

さらに、frontmatter に tags フィールドを用意しておきながら全く活用していなかったので、今回トップページの記事カードにタグを表示するようにした。

<!-- src/pages/index.astro -->
{post.data.tags && post.data.tags.length > 0 && (
  <span class="inline-block text-[10px] font-medium px-2 py-0.5 rounded-full bg-indigo-100 dark:bg-indigo-900 text-indigo-800 dark:text-indigo-200 mb-2 self-start">
    {post.data.tags[0]}
  </span>
)}

加えて、以下のメリットがあった。

  • ビルドが速い: 開発サーバーの起動もビルドも高速(Vite が速い?よくわかってない)
  • シンプルな構成: 余計な設定がなく、見通しが良い
  • 型安全なコンテンツ: Content Collections による frontmatter の型チェック
  • インフラの統一: Cloudflare でドメイン・DNS・ホスティングを一元管理

個人ブログという用途には、Astro は最適解の一つだと思う。