onareのBlog

Next.js + Cloudflare Pagesでブログ作成 - WordPressからの乗り換え

はじめに

これまでいくつかのブログを WordPress で構築・運用してきましたが、個人で運用するには少し大げさで、使いにくさを感じることが増えてきました。特に、自宅のラズパイでホストしていた際のパフォーマンスや、記事の書き心地には課題を感じていました。

そんな中、よりモダンで開発者フレンドリーな方法を探していたところ、Next.jsによる静的サイト生成(SSG)と、Cloudflare Pagesでのホスティングという組み合わせが非常に良さそうだと感じました。

この記事では、私が WordPress からこのブログの構成に乗り換えた理由と、その具体的な技術要素や手順を紹介します。

WordPress の何が不満だったか

まず、私が感じていた WordPress の不満点を整理してみます。

  • パフォーマンスの低さ: 自宅のラズパイでホストしていたこともあり、ページの読み込みが非常に遅かったです。PHP とデータベースが動的にページを生成する仕組みは、シンプルなブログには過剰で、パフォーマンスのボトルネックになりがちです。
  • 記事の書きにくさ: 標準のブロックエディタ(Gutenberg)は高機能ですが、Markdown で書きたい私にとっては少し癖が強く、直感的に使えませんでした。
  • 多機能すぎて混乱する: WordPress は非常に多機能ですが、その分、設定項目が多岐にわたります。「設定すべき項目」と「そのままで良い項目」の区別がつきにくく、ストレスに感じることがありました。
  • バックアップ・リストアの煩雑さ: データベースとアップロードしたファイル(画像など)の両方をバックアップする必要があり、手順が煩雑です。

なぜ Next.js を選んだのか?

これらの不満を解消する技術として、Next.js は非常に魅力的でした。

  • SSG による高速化: Next.js は、next.config.tsoutput: 'export'と設定することで、ビルド時に完全に静的な HTML・CSS・JS ファイルを生成できます。これにより、ユーザーからのリクエストには既に生成済みのファイルを返すだけなので、表示が非常に高速です。高速なサイトはユーザー体験を向上させるだけでなく、SEO にも有利に働きます。
  • Markdown での記事管理: 好きなエディタで Markdown を使って記事を執筆し、それを Git リポジトリで管理できます。エンジニアにとっては最も慣れ親しんだ方法でコンテンツを作成できるのは大きなメリットです。
  • Git ベースのバージョン管理とバックアップ: 記事の Markdown ファイルも、サイトのソースコードもすべて Git で管理するため、バックアップはgit pushするだけです。変更履歴もすべて残るため、安心して運用できます。
  • React のエコシステム: 豊富な React コンポーネントやライブラリを活用して、自由にサイトをカスタマイズできます。

なぜ Cloudflare Pages を選んだのか?

ホスティング先として Cloudflare Pages を選んだのにも、明確な理由があります。

  • Git リポジトリとの連携による自動デプロイ: これが最大のメリットです。GitHub リポジトリを連携させておけば、特定のブランチ(例: main)にpushするだけで、自動的にビルドとデプロイが実行されます。
  • 高い可用性とパフォーマンス: 世界中に広がる Cloudflare の CDN 上でサイトがホストされるため、高速かつ安定した配信が可能です。自宅サーバーのように、ハードウェアの故障やネットワークの心配をする必要がありません。
  • CLI による手動デプロイ: このリポジトリのpackage.jsonにもあるように、wrangler CLI を使えばコマンド一つでデプロイが可能です。これにより、CI/CD パイプラインに組み込むなど、より柔軟なデプロイ戦略も取れます。
  • 無料枠の存在: 個人のブログであれば、無料枠で十分に運用可能です。

このブログの技術詳細

それでは、このブログが実際にどのように作られているか、具体的なライブラリに触れながら解説します。

1. Markdown のパースと HTML 変換

記事の Markdown ファイルは、src/lib/posts.tsにあるgetPostBySlug関数で読み込まれます。ここではgray-matterというライブラリを使い、ファイル冒頭のフロントマター(タイトルや日付など)と、記事本文を分離しています。

src/lib/posts.ts
// ...
import matter from "gray-matter";
// ...
const { data, content } = matter(fileContents);
// ...

そして、本文の Markdown 文字列は、zenn-markdown-htmlというライブラリを使って HTML に変換されます。これは記事ページコンポーネント(src/app/posts/[slug]/page.tsxなど)で行われます。

import markdownToHtml from "zenn-markdown-html";
// ...
const content = markdownToHtml(post.content);
// ...

2. Markdown のスタイリング

HTML に変換された記事の見た目は、zenn-content-cssを使って整えています。

記事本文を表示するPostBodyコンポーネントで CSS ファイルをインポートし、divzncというクラス名を付与するだけです。これだけで、Zenn のブログのような洗練されたデザインが適用されます。

src/app/components/post-body.tsx
import "zenn-content-css";

type Props = {
  content: string;
};

export function PostBody({ content }: Props) {
  return <div className="znc" dangerouslySetInnerHTML={{ __html: content }} />;
}

@tailwindcss/typographyのようなプラグインを使わずとも、非常に簡単に美しい記事スタイルを実現できるのが大きな利点です。

3. Google Analytics の導入

アクセス解析の Google Analytics は、Next.js 公式が提供する@next/third-partiesライブラリを使って導入しています。これにより、数行のコードを追加するだけで済みます。

まず、ライブラリをインストールします。

pnpm add @next/third-parties

次に、ルートレイアウトであるsrc/app/layout.tsxGoogleAnalyticsコンポーネントを追加します。測定 ID は.env.localなどの環境変数ファイルで管理するのが安全です。

src/app/layout.tsx
import { GoogleAnalytics } from "@next/third-parties/google";
// ...

export default function RootLayout({ children }: Readonly<{ children: React.ReactNode; }>) {
  return (
    <html lang="ja">
      <body>
        {/* ... */}
        <GoogleAnalytics gaId={process.env.NEXT_PUBLIC_GOOGLE_ANALYTICS_ID!} />
      </body>
    </html>
  );
}

以前のように_document.tsxを編集したり、Scriptコンポーネントを手動で設定したりする必要はなく、非常にシンプルになりました。

4. おまけ:ダークテーマ対応の実装

このブログのダークテーマ対応は、next-themesshadcn/uiの思想を組み合わせた、CSS 変数ベースのモダンな方法で実装されています。以下にその手順を解説します。

ステップ 1: ライブラリのインストール

テーマ管理のためにnext-themesをインストールします。

pnpm install next-themes

ステップ 2: tailwind.config.mjsの設定

Tailwind CSS に、クラス(またはdata-theme属性)を使ってダークモードを切り替えることを伝えます。

tailwind.config.mjs
/** @type {import('tailwindcss').Config} */
const config = {
  darkMode: ["class", '[data-theme="dark"]'],
  // ...
};

ステップ 3: globals.cssで色を定義

shadcn/uiの慣習に従い、色の実態は CSS 変数として定義します。src/app/globals.cssに、デフォルト(ライトテーマ)の色と、.darkまたは[data-theme="dark"]セレクタ内のダークテーマ用の色を定義します。

src/app/globals.css
@layer base {
  :root {
    --background: 0 0% 100%;
    --foreground: 240 10% 3.9%;
    /* ...その他のライトテーマ用の色 ... */
  }

  .dark, [data-theme="dark"] {
    --background: 240 10% 3.9%;
    --foreground: 0 0% 98%;
    /* ...その他のダークテーマ用の色 ... */
  }
}

Tailwind のbg-backgroundtext-foregroundといったクラスは、これらの CSS 変数を参照するようtailwind.config.mjsで設定されています。

ステップ 4: ThemeProvider の作成と適用

next-themesThemeProviderをラップしたコンポーネントを作成し、layout.tsxでアプリ全体を囲みます。

src/components/theme-provider.tsx
"use client";

import * as React from "react";
import { ThemeProvider as NextThemesProvider } from "next-themes";

export function ThemeProvider({ children, ...props }: React.ComponentProps<typeof NextThemesProvider>) {
  return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}
src/app/layout.tsx
import { ThemeProvider } from "@/components/theme-provider";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="ja" suppressHydrationWarning>
      <body>
        <ThemeProvider
          attribute="class" // このリポジトリではclassとdata-themeの両方を使っています
          defaultTheme="system"
          enableSystem
        >
          {children}
        </ThemeProvider>
      </body>
    </html>
  );
}

ステップ 5: テーマ切り替え UI の作成

最後に、ユーザーがテーマを切り替えるための UI を作成します。このリポジトリではshadcn/uiDropdownMenulucide-reactのアイコンを使っています。

useThemeフックからsetTheme関数を取得し、各メニュー項目がクリックされたときにテーマを切り替えます。

src/components/ui/dark-mode-toggle.tsx
"use client";

import { Moon, Sun } from "lucide-react";
import { useTheme } from "next-themes";
import { Button } from "@/components/ui/button";
import { DropdownMenu, ... } from "@/components/ui/dropdown-menu";

export function ModeToggle() {
  const { setTheme } = useTheme();

  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button variant="outline" size="icon">
          <Sun className="h-[1.2rem] w-[1.2rem] scale-100 dark:scale-0" />
          <Moon className="absolute h-[1.2rem] w-[1.2rem] scale-0 dark:scale-100" />
        </Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent align="end">
        <DropdownMenuItem onClick={() => setTheme("light")}>Light</DropdownMenuItem>
        <DropdownMenuItem onClick={() => setTheme("dark")}>Dark</DropdownMenuItem>
        <DropdownMenuItem onClick={() => setTheme("system")}>System</DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  );
}

このコンポーネントをヘッダーなどに配置すれば、ダークテーマ対応は完了です。CSS 変数をうまく使うことで、非常にクリーンで拡張性の高い実装が可能になります。

5. おまけ 2:スクロール追従型の目次を実装する

長い記事には、今どこを読んでいるかを示すスクロール追従型の目次があると格段に読みやすくなります。このブログではtocbotというライブラリを使って実装しています。

ステップ 1: ライブラリのインストール

まずtocbotをインストールします。

pnpm install tocbot

ステップ 2: 目次コンポーネントの作成

tocbotは DOM を直接操作するため、"use client"を宣言したクライアントコンポーネントとして実装する必要があります。useEffect内で初期化と破棄を行うのが React の作法です。

src/app/components/table-of-contents.tsx
"use client";

import { useEffect } from "react";
import tocbot from "tocbot";

export function TableOfContents() {
  useEffect(() => {
    tocbot.init({
      tocSelector: ".toc", // 目次を生成する要素のセレクタ
      contentSelector: ".znc", // 目次の対象となる記事本文のセレクタ
      headingSelector: "h2, h3, h4, h5", // 目次に含める見出し
    });

    // コンポーネントがアンマウントされる時にtocbotを破棄
    return () => tocbot.destroy();
  }, []);

  return (
    <div className="p-4 bg-muted text-muted-foreground rounded-lg">
      <h4 className="mb-2 font-bold">目次</h4>
      {/* このdivの中に目次が自動的に生成される */}
      <div className="toc" />
    </div>
  );
}

ここで重要なのはcontentSelectorです。記事本文のコンテナ(このブログではzenn-content-cssのために.zncクラスを付与)を正しく指定することで、tocbotが中の見出しを検出してくれます。

ステップ 3: ページへの配置

作成したTableOfContentsコンポーネントを、記事ページ(src/app/posts/[slug]/page.tsx)に配置します。PC などの広い画面では記事の横に、スマホなどの狭い画面では記事の上に表示されるように、Tailwind CSS のレスポンシブ機能を使ってレイアウトを組みます。

src/app/posts/[slug]/page.tsx
// ...
import { PostBody } from "@/app/components/post-body";
import { TableOfContents } from "@/app/components/table-of-contents";

export default function PostPage({ params }: { params: { slug: string } }) {
  // ...
  return (
    <div className="grid grid-cols-1 lg:grid-cols-4 lg:gap-x-8">
      <div className="col-span-1 lg:order-last">
        <aside className="sticky top-24">
          <TableOfContents />
        </aside>
      </div>
      <main className="col-span-3">
        <article>
          <PostBody content={content} />
        </article>
      </main>
    </div>
  );
}

ステップ 4: スタイリング

tocbotは、現在アクティブな目次のリンクに.is-active-linkというクラスを自動で付与してくれます。このクラスに対して CSS でスタイルを定義すれば、スクロールに応じて目次のハイライトが追従するようになります。

src/app/globals.css
.toc > .toc-list > li > a.is-active-link {
  @apply font-bold text-primary;
}

これで、ユーザー体験を向上させる便利な目次の完成です。

まとめ

WordPress から Next.js + Cloudflare Pages へ移行したことで、ブログ運用は劇的に快適になりました。

  • 表示速度が爆速になった
  • Markdown + Git という快適な執筆・管理体制が手に入った
  • git pushするだけの簡単なデプロイフロー
  • サーバーメンテナンスからの解放

このブログで採用しているzenn-markdown-htmlnext-themestocbotといったライブラリを活用することで、開発体験を損なうことなく、簡単に高機能なブログを構築できます。

もしあなたが WordPress の運用に少しでも疲れを感じているなら、このモダンなブログスタックへの乗り換えを検討してみてはいかがでしょうか。