公式サイトを Next.js に移行しました

こんにちは。先日弊社Webサイトを WordPress から、WordPress をヘッドレス CMS として使った Cloudflare Pages・Next.js 構成へ移行しました。本記事では移行手順についてご紹介します。

Wordpress 構成の問題点

covelline.com はさくらインターネットのレンタルサーバに配置した Wordpress にて運用していました。Wordpress は非常に簡単にブログを構築でき、プラグインも豊富なためコーポレートサイトに必要なことが実現できました。しかし、長い間運用していると下記のような問題がありました。

  • アクセスが増加すると負荷でアクセスできなくなる
  • PHP、Wordpress に詳しい人しかサイトのメンテナンスができない (特にテーマの更新が難しい)
  • FTP で配置したサーバ上のソースを直接編集しているためバージョン管理されない

アクセスによる負荷の問題はキャッシュプラグインの導入によりかなり改善されましたが、下2つの問題によりかなり属人性が高い状態になっていました。

そこで、静的サイトジェネレータと、コベリン社員が慣れている GitHub を中心としたソース管理・リリース管理の環境を構築することとしました。

技術選定

まず考えたのは GitHub のリポジトリに記事本文や画像などのコンテンツも含めすべてを管理する方法です。私の個人サイト https://codingcafe.jp/ がそのような構成になっていました。しかし、2013年から続く弊社のすべての記事をリポジトリに含めるとサイズが非常に大きくなりそうなため、また Wordpress のような専用の管理画面で投稿するのはとても便利なため、この方法は採用しませんでした。

次に考えたのはコンテンツをヘッドレス CMS から取得する方法です。contentful や microCMS などが検討に上がりました。これらは非常に便利そうなものの、料金や移行の手間、投稿フローが大きく変わることが懸念でした。

最終的に現在使っている Wordpress をそのままヘッドレス CMS として利用することとしました。

Wordpress のヘッドレス CMS 化とは

Wordpress は記事の投稿、管理だけに利用し、公開されるページには利用しない方法です。記事データは API 経由で取得できるようにし、フロントエンドとなる Web サイトは独立したシステムとしてそれら記事データを取得、Web サイトのレンダリングを行います。

フロントエンド

次にフロントエンドですが、弊社社員は React に慣れているため、デファクトである Next.js を採用しました。テンプレートも用意されているため非常に簡単に環境構築することができました。

バックエンドサービスの選定

Next.js で構築したサイトのデプロイ先について検討しました。個人的に大好きな Vercel を利用しようと考えましたが、Vercel は無料プランでは帯域に制限があり、また複数アカウントで管理するチーム機能が利用できませんでした。Cloudflare Pages ではそれらの制限がなく、またエッジでのレンダリングによる動的な動作も可能ということで今回採用しました。Vercel と同様に GitHub 統合があるので、GitHub だけでデプロイを管理できるのも必要な機能でした。

構成

Next.js で開発されたフロントエンドのソースコードを GitHub のリポジトリに配置し、Cloudflare Pages にデプロイします。Cloudflare Pages の GitHub 統合により、PR を作成することでプレビュー環境にデプロイ、PR をマージすることで本番環境にデプロイされます。今回はビルド時にはレンダリングを行わず、アクセス時にエッジで記事データの Wordpress からの取得・キャッシュを行う構成としました。

URL 構造の維持

お知らせ記事などは外部からリンクされている可能性も高く、各ページの URL はそのまま維持されるようにしました。Next.js ではディレクトリ構造によりルーティングされるので、Wordpress で設定したパスと同じになるようにします。大まかに下記のような構造になります。

/pages/about/index.tsx
/pages/category/index.tsx
/pages/category/[category]/index.tsx
/pages/category/[category]/page/[page].tsx
/pages/news/index.tsx
/pages/news/page/[page].tsx
/pages/news/[year]/[id].tsx

Wordpress サーバの URL

いままで Wordpress に向いていた covelline.com ですが、こちらはフロントエンドの置き場になるので、Wordpress は別の URL からアクセスできるようにします。

今回はもともとさくらインターネットのレンタルサーバだったため、共有ドメインである ***.sakura.ne.jp をそのまま使うようにしました。

画像 URL の修正

記事の URL は前述のとおりいままでの構造を維持しますが、記事内の画像ファイルは Wordpress を配置したサーバにアップロードされているため、URL が変わります。

そこで画像リンクを修正するため、Wordpress の Search Regex プラグインを使って記事内の src="covelline.com/wp をすべてsrc="***.sakura.ne.jp/wp に置換します。この時点では ***.sakura.ne.jp からは Wordpress のサイトにアクセスできないので、置換作業は DNS 設定を行ったあとに行います。

API の準備

サイト生成を行う際に Wordpress の記事データを API 経由で取得できるように、WPGraphQL プラグインを導入します。

フロントエンドの実装

新しく Next.js でフロントエンドを実装します。getStaticProps と getStaticPaths 内で GraphQL の API を呼び出して適切にサイト生成と URL のハンドリングを行います。GraphQL についてはこちらの記事もご覧ください。

また、Cloudflare Pages の設定を行いリポジトリの紐づけを行います。この時点で Cloudflare Pages が提供するドメイン ***.pages.dev からサイトにアクセスが可能になります。

もしあれば ads.txt や app-ads.txt のようなファイルも忘れずに public ディレクトリに配置しておきます。

固定ページの実装

固定ページは様々な URL が設定されていますが、Wordpress 上の特定の id のページを表示するという共通した機能なので、下記のようにコンポーネントを実装しました。また id を渡して getStaticProps を生成する関数を定義します。

/components/page/page.tsx

gql`
  fragment Page_post on Page {
    ...
  }
`

export function Page({ page }: PageProps) {
  ...
}

gql`
  query PageById($id: ID!) {
    page(id: $id, idType: DATABASE_ID) {
      ...Page_post
    }
  }
`

export const createGetStaticPaths =
  (id: string): GetStaticProps<PageProps> =>
  async ({ preview = false }) => {
    const data = await sdk.PageById({ id })

    return {
      props: {
        preview,
        page: data.page ?? null,
      },
      revalidate: false,
    }
  }

このようにして、固定ページを配置したいパスに index.tsx を配置して ID を指定するだけでページが作成できます。

/pages/readme/index.tsx

import { Page, createGetStaticPaths } from "../../components/page/page"
export const runtime = "experimental-edge"

export default Page
export const getStaticProps = createGetStaticPaths("1665")

DNS 設定

いよいよ covelline.com が Next.js で作ったサイトを向くようにします。作業中はサイトにアクセスできなくなりますが、あまりアクセスが多いわけではないので気にしないでちゃちゃっとやることにします。

まず Cloudflare Pages にドメインを設定するための準備として、Google Domains でカスタムネームサーバを使うようにして Cloudflare DNS を指定、レコードを Cloudflare 側で管理するようにしました。そして Cloudflare の管理画面上で Cloudflare Pages に covelline.com を割り当てます。

次に Wordpress のサイトを ***.sakura.ne.jp から開けるようにします。シングルサイトの場合は wp-config.php の WP_HOMEWP_SITEURL を書き換えるだけでよさそうでしたが、今回はマルチサイト構成だったのでデータベースを書き換えます。さくらの管理画面から phpMyAdmin を使って siteurl と home を探して書き換えます。

次に Next.js で API のアクセス先を covelline.com から ***.sakura.ne.jp に変更します。PR をマージして Cloudflare Pages にデプロイされればフロントエンドの作業は完了となります。

最後に前述の画像 URL の置換を Wordpress 上で行います。今までの URL が変わらずアクセスできるか、記事内の別記事へのリンクが開けるかなどを試し、問題なければ移行は完了です。

感想

記録としてはこのようにきれいに作業できたように見えますが、実際に私がやったときは試行錯誤しながらだったので、URL の向き先を変えたり戻したりと泥臭い作業になりました。とくにマルチサイト構成の Wordpress サイトを独自ドメインから共有ドメインに戻すのがなかなかうまく行かず思ったよりもダウンタイムが長くなってしまい焦りました。

属人性の高さやパフォーマンスの問題からモダンなスタックを導入しましたが、社員への説明がなければ属人性が高いことに変わりはないため、GraphQL プチ勉強会を実施しました。しかし Wordpress を GraphQL から利用するというのはドキュメントも整備されていませんし属人性の高さは解消されていません。そのため今回の Wordpress + Next.js という構成は現状ベストではあると思いつつも、Headless CMS サービスへの移行も視野に入れ続けていこうと考えています。

それと下書き記事のプレビューは相変わらず Wordpress 側がレンダリングした画面を見るので、そのうち Next.js で作ったページ上で見られるようにしたいなと思っていますが難しそうなので手を付けていません。

しかし TypeScript と React を使ったフロントエンド開発は書いていて楽しいですし、Wordpress という巨大な怪物を意識せずサーバー・クライアントというシンプルな構成でサイト構築ができるのも安心感があります。

また、社員が頻繁に利用する技術ブログである本サイト blog.covelline.com ははてなブログを利用しているので、移行作業中に影響がなかったのはよかったです。