Next.jsとXserverで実現するヘッドレスWordPressサイト構築ガイド

ChatGPTのDeep Research(AI)での出力結果をそのまま掲載しています。ChatGPTのDeep Researchはハルシネーション(誤った情報の生成)が少なくなるよう調整されていますが、あくまで参考程度に読んでください。当記事は検索エンジンに登録していないため、このブログ内限定の記事です。

はじめに
最近、Web制作の手法として「ヘッドレスCMS」という言葉を耳にする機会が増えました。ヘッドレスCMSとは、簡単に言うと「コンテンツ管理(バックエンド)と表示(フロントエンド)を切り離したサイト構築手法」です (ヘッドレスWordPressとは何か?)。WordPressをヘッドレスCMSとして使う場合、WordPressは記事やページなどのコンテンツ管理システム(バックエンド)として稼働し、実際のサイト表示はReactベースのNext.jsなど別の技術(フロントエンド)が担当します (ヘッドレスWordPressとは何か?) (ヘッドレスWordPressとは何か?)。

本記事では、Next.jsを用いたヘッドレスWordPressサイトを、国内ホスティングサービスのXserver環境で運用する方法を初心者向けに解説します。具体的には以下のポイントを押さえます。

  • WordPressのREST APIを使うための設定方法とセキュリティ対策(認証方法、Rate Limit(レート制限)、CORS設定など)
  • Next.jsでWordPressの記事データを取得・表示する方法(ISR*による更新などの最適化手法を含む)
  • Xserver上へのNext.jsサイトのデプロイ方法(静的エクスポートnpm run exportの活用とアップロード手順)
  • ヘッドレス化で陥りやすい課題とその解決策(カスタム投稿タイプの対応、プラグインの互換性、画像や内部リンクの扱いなど)

ISR(Incremental Static Regeneration): 更新されたコンテンツをユーザへ配信するためのNext.jsの機能。静的サイトの高速性を保ちつつ、コンテンツの変化に応じてページをバックグラウンド再生成する仕組みです (Using ISR with WordPress API in Next.js Guide) (Using ISR with WordPress API in Next.js Guide)。

それでは順番に見ていきましょう。

1. WordPress REST APIの設定とセキュリティ対策

REST APIとは?
WordPressにはREST APIという機能が標準搭載されており、URL経由でWordPress内のデータ(投稿や固定ページ、コメントなど)を取得・操作できます (ヘッドレスWordPressとは何か?)。REST APIはWordPress 4.7以降デフォルトで有効になっており、https://<あなたのドメイン>/wp-json/wp/v2/ 以下に各種エンドポイントが用意されています。例えば、/wp-json/wp/v2/postsにGETリクエストを送れば記事一覧のデータをJSON形式で取得可能です。

基本設定と動作確認: まずはREST APIが利用できる状態か確認しましょう。WordPress管理画面でパーマリンク設定が「投稿名」等のリッチなパーマリンク形式になっていることを推奨します(通常REST APIは標準で有効ですが、まれにパーマリンク設定が影響する場合があります)。ブラウザでhttps://あなたのサイトURL/wp-json/wp/v2/postsにアクセスし、記事データがJSONで表示されれば準備OKです。

1.1 REST APIの有効化(カスタム投稿タイプの場合)

通常の投稿や固定ページは最初からREST API経由で取得できます。しかし、カスタム投稿タイプ(Custom Post Type)を使っている場合は注意が必要です。WordPressではカスタム投稿タイプはデフォルトでREST APIが無効になっており、このままではAPI経由で取得できません (〖WordPress〗WP REST APIで「カスタム投稿タイプ」の記事一覧を取得する | 株式会社エンタースクウェア | Web制作)。対応策は、カスタム投稿タイプ登録時に設定を追加することです。

  // 例:カスタム投稿タイプ「news」の登録
  register_post_type('news', array(
      'public'      => true,
      // ...(省略)...
      'show_in_rest' => true,       // これを追加:REST APIで使用可能にする
      'rest_base'   => 'news'      // (任意)REST API上でのエンドポイント名
  ));

上記のように設定しなおして保存すると、/wp-json/wp/v2/news からカスタム投稿 "news" のデータ取得が可能になります (〖WordPress〗WP REST APIで「カスタム投稿タイプ」の記事一覧を取得する | 株式会社エンタースクウェア | Web制作) (〖WordPress〗WP REST APIで「カスタム投稿タイプ」の記事一覧を取得する | 株式会社エンタースクウェア | Web制作)。

1.2 WordPress REST APIの公開範囲とアクセス制御

REST APIを有効化すると、誰でもエンドポイントにアクセスすれば公開記事のデータを取得できるようになります。これはヘッドレスCMSとしてデータを取り出す上で便利な反面、不要なアクセスや悪用も考えられるため、適切なセキュリティ対策を講じましょう。

(a) 認証(Authentication)の利用:
REST APIでデータを更新・削除したり、下書き記事や非公開データを取得するには認証が必要です。公開記事を読むだけなら認証なしでも可能ですが、APIを使って管理操作を行う場合はWordPressに対してログイン済みであることを示す情報が必要となります。主な認証方法は以下の通りです (WordPress REST APIの認証方法の種類と使い方 | lifetechia):

  • クッキー認証 (Cookie認証): WordPressにログイン済みのユーザーのクッキーを利用します。同一ドメイン内のJavaScriptから呼び出す場合に使われ、wp_rest用のノンス(nonce)も利用してCSRF保護を行います。主にWordPress内でフロントエンドを動かす場合向きで、ドメインが異なるヘッドレス環境では使えません
  • ベーシック認証 (Basic認証): HTTPヘッダーにユーザー名・パスワード(またはAppパスワード)を付与するシンプルな方法です。WordPress 5.6以降なら各ユーザーのプロフィール画面でアプリケーションパスワードを発行でき、それをパスワードとして利用することでBasic認証が可能です (WordPress REST APIの認証方法の種類と使い方 | lifetechia)。ただし、この方法は平文の資格情報を送ることになるため、本番運用では推奨されません (WordPress REST APIの認証方法の種類と使い方 | lifetechia)(テスト用途向け)。
  • OAuth認証: OAuth 1.0aを使った認可方式で、コンシューマキー・シークレットを用いてトークンを取得しAPIアクセスします。公式のOAuthプラグインを導入して利用する方法ですが、設定がやや複雑です (WordPress REST APIの認証方法の種類と使い方 | lifetechia)。
  • JWT認証: JWT (JSON Web Token) を用いた認証方式です。専用のプラグイン「JWT Authentication for WP REST API」を導入することで比較的簡単に実装できます (WordPress REST APIの認証方法の種類と使い方 | lifetechia)。リクエスト時に認証用トークンをヘッダーに含め、WordPress側でその有効性を検証します。

初心者には、アプリケーションパスワード+Basic認証またはJWT認証プラグインのどちらかがおすすめです。例えばJWTプラグインを入れれば、WordPressに対してログインリクエストを送りトークンを取得し、そのトークンを以後のAPI呼び出しのヘッダー(Authorization: Bearer <token>)に付与することで認証されたAPI利用が可能になります。認証を使うことで、公開範囲の制限されたデータもNext.jsから安全に取得できるようになります。

(b) CORS設定(クロスオリジン制御):
ヘッドレスWordPressでは、WordPress(API提供元)とNext.jsサイト(データ利用側)が異なるドメインになるケースが多くあります。例えばWordPressをhttps://wp.example.comで運用し、Next.jsで生成したサイトをhttps://www.example.comで公開する、といった場合です。このようにオリジン(ドメイン)が異なると、ブラウザはセキュリティ上の理由からそのままではAPIにアクセスできません。これを解決するにはWordPress側でCORS (Cross-Origin Resource Sharing) を適切に設定し、Next.jsサイトのドメインからのリクエストを許可する必要があります (WordPress REST APIにCORSを設定する方法〖シンプル解説〗 | TOMOSHIBI Dev)。

WordPressでCORSを設定する手軽な方法は、テーマのfunctions.phpにヘッダー出力のコードを追加することです。以下は特定のオリジンを許可する例です (WordPress REST APIにCORSを設定する方法〖シンプル解説〗 | TOMOSHIBI Dev):

function add_cors_http_header() {
    header("Access-Control-Allow-Origin: https://frontend.com");  // 許可するオリジンを指定
    header("Access-Control-Allow-Methods: GET, POST, OPTIONS");
    header("Access-Control-Allow-Headers: Content-Type, Authorization");
}
add_action('rest_api_init', function () {
    add_action('send_headers', 'add_cors_http_header');
});

上記のコードをWordPress側に追加すると、https://frontend.com(例:Next.jsサイトのURL)からのAPIリクエストを許可できます (WordPress REST APIにCORSを設定する方法〖シンプル解説〗 | TOMOSHIBI Dev)。Access-Control-Allow-Origin に許可するドメイン(*を指定すれば全ドメイン許可)を記載し、必要に応じてメソッドやヘッダーも許可リストに追加します。Next.js側でfetchを行う際にも、必要ならmode: 'cors'や認証ヘッダーを設定しましょう。これでブラウザ上のNext.jsアプリからWordPress APIをコールできるようになります。

(c) レート制限(Rate Limit)と悪用防止:
公開されたREST APIは、不特定多数から大量のリクエストを受ける可能性があります。そのため、レート制限(一定時間内のリクエスト回数制限)やアクセス制限を検討しましょう。WordPress本体にはレート制限機能はありませんが、以下の方法があります。

  • WAFやセキュリティプラグインの活用: WordPress用セキュリティプラグイン(例:WordfenceやShield Securityなど)には、REST APIへの不審なアクセスをブロックしたり、未認証ユーザーからのREST APIアクセスを制限する機能があります。それらを活用して、必要な場合は認証ユーザーのみREST API利用可とする設定も検討してください。
  • REST API専用プラグインの活用: サードパーティ製でREST APIの利用を制御するプラグインも存在します(例:「WP REST API Controller」や「Disable WP REST API」など)。APIを使う必要がある投稿タイプだけ公開し、それ以外は無効化するといった細かな設定も可能です。
  • APIキーの実装: カスタム実装として、APIリクエストに独自のキー(シークレット)を含め、それが一致しない場合はWordPress側で弾くという方法もあります。実際に開発案件では、REST APIにアクセスする際に特定のクエリパラメータ(APIキー)を要求するようカスタマイズし、キーを知らない第三者からのアクセスを拒否する対策を施すことがあります (因創科技 | Next.js + WordPress - Part 2)。加えてサーバー側でファイアウォールを設定し、特定のIPやドメインからのアクセスのみ許可するというのも有効です。

これらの対策により、「自分たちのNext.jsサイトからの正規のAPI利用」は確保しつつ、「悪意あるクローラーや第三者からの過剰なアクセス」は抑制できます。特にヘッドレス運用ではWordPressの表側画面を閉じてAPIだけ公開するケースも多いので、必要最低限の穴だけ空け、それ以外は塞ぐという意識でセキュリティ設定を行いましょう。

2. Next.jsでWordPressの記事データを取得・表示する方法

バックエンド(WordPress側)の準備が整ったら、次はフロントエンドであるNext.js側でデータを取得し、サイトとして表示する部分です。ここではブログ記事一覧と記事詳細ページを例に、Next.jsでのデータ取得と表示方法、さらにパフォーマンスを高めるための技術について解説します。

2.1 Next.jsでのデータ取得手法(概要)

Next.jsにはデータ取得のためのいくつかの手法がありますが、ヘッドレスCMSの文脈では主に静的サイト生成(SSG: Static Site Generation)サーバーサイドレンダリング(SSR: Server-Side Rendering)を用いることになります。特にサイトのパフォーマンスや安定性を重視する場合、多くのケースでSSGが採用されます。SSGではビルド時(サイト公開前)に必要なデータを取得し、あらかじめHTMLファイルを生成しておくため、ユーザーのアクセス時には非常に高速にページを提供できます (Using ISR with WordPress API in Next.js Guide)。一方、SSRはリクエストごとにサーバーでデータ取得・ページ生成を行うため、最新データが即時反映される利点はありますが、サーバー負荷や応答速度の面でデメリットがあります。

今回Xserverでのホスティングを想定しており、Xserverは基本的に静的ファイルのホスティング環境(後述)なので、Next.jsで静的なHTMLを出力するSSG一択と考えて良いでしょう。Next.jsではgetStaticPropsという関数を使ってSSG用のデータ取得が実装できます (Next.jsを使った静的なHeadless WordPressサイトの作り方 - Shifter)。これを利用すると、「サイトのビルド時にWordPress APIから記事データを取り出し、その内容でHTMLを生成する」という処理を自動化できます (Next.jsを使った静的なHeadless WordPressサイトの作り方 - Shifter)。

2.2 記事一覧ページの実装例

まず、トップページまたはブログ一覧ページでWordPressの記事一覧を表示する方法を見てみましょう。大まかな流れは以下の通りです。

  1. WordPress REST APIにリクエストして記事一覧データを取得する(例: fetchで/wp-json/wp/v2/postsにアクセス)。
  2. 取得したデータをReactコンポーネント内でループ表示する(タイトルや抜粋、サムネイルなどを表示)。
  3. Next.jsの機能を使って、この処理をビルド時に実行し静的HTML化する。

具体例として、Next.jsのページコンポーネントpages/index.js(トップページ)内での実装を考えてみます。

(A) データ取得部分のコード例:

// WordPressのエンドポイントURLを指定
const WP_API_URL = 'https://your-wordpress-site.com/wp-json/wp/v2/posts?_embed';

// ビルド時に実行される関数
export async function getStaticProps() {
  const res = await fetch(WP_API_URL);
  const posts = await res.json();
  return {
    props: { posts }  // 投稿データをpropsとしてページコンポーネントに渡す
  };
}

上記のgetStaticPropsはビルド時に一度だけ実行され、WordPressから記事一覧を取得しています。ポイントは、URLに?_embedクエリを付けていることです。これによりカテゴリやアイキャッチ画像などの関連データも一緒に埋め込んで取得できます (Next.jsを使った静的なHeadless WordPressサイトの作り方 - Shifter)。WordPress REST APIは標準ではレスポンスを軽量にするためこれら情報を省略していますが、_embedを付けることで例えばpost._embedded['wp:featuredmedia']にアイキャッチ画像情報が含まれるようになります (〖WordPress〗WP REST APIで「カスタム投稿タイプ」の記事一覧を取得する | 株式会社エンタースクウェア | Web制作)。

(B) 表示部分のコード例:

// ページコンポーネント(記事一覧の表示)
export default function Home({ posts }) {
  return (
    <main>
      <h1>ブログ記事一覧</h1>
      {posts.map(post => (
        <article key={post.id}>
          {/* タイトルと抜粋を表示(HTMLエンコードされているのでdangerouslySetInnerHTMLで表示) */}
          <h2 dangerouslySetInnerHTML={{ __html: post.title.rendered }} />
          <div dangerouslySetInnerHTML={{ __html: post.excerpt.rendered }} />
          {/* アイキャッチ画像がある場合 */}
          {post._embedded?.['wp:featuredmedia'] && (
            <img 
              src={post._embedded['wp:featuredmedia'][0].source_url} 
              alt={post._embedded['wp:featuredmedia'][0].alt_text || post.title.rendered} 
            />
          )}
          {/* 記事詳細ページへのリンク */}
          <a href={`/posts/${post.slug}`}>続きを読む</a>
        </article>
      ))}
    </main>
  );
}

上記では、受け取ったposts配列を.mapで回して記事ごとのタイトル・抜粋・画像を表示しています。WordPressのAPIから返ってくるタイトルや本文はHTML文字列(例えば見出しの<h1>タグなどを含む)になっているため、そのまま表示するにはdangerouslySetInnerHTMLを使っています。これはReactでHTML文字列をレンダリングする方法ですが、XSS等に注意が必要です。今回は自分のWordPressからのコンテンツ(信頼できるデータ)を表示する前提なので問題ありませんが、不特定のユーザ投稿を表示する場合はサニタイズが必要になるでしょう。

また、アイキャッチ画像はREST APIの返すデータの中に画像URLが含まれていますので、それをそのまま<img>タグのsrcに使っています。Next.jsのImageコンポーネントnext/image)は残念ながら静的エクスポートでは利用できません。Next.jsを完全静的に出力する場合、画像最適化機能は使えないため<img>タグで直接配置しています (blog.masizime.com)。Next.jsで静的書き出しを行う際は、next/imageを使うとビルドエラーになるか、機能しません。そのため静的サイトではnext/imageはオフにし、普通のを使う必要があります (blog.masizime.com)。

(C) 生成されたページ:
上記のgetStaticPropsとコンポーネントの組み合わせにより、Next.jsはビルド時にWordPressから取得した記事一覧データを用いて、このページ(index.html)を生成します (Next.jsを使った静的なHeadless WordPressサイトの作り方 - Shifter)。通常のReactアプリであれば初回アクセス時にクライアント側でデータ取得→レンダリングとなりますが、Next.js SSGの場合は既に完成したHTMLとして出力されているため、ユーザーは非常に速くコンテンツを閲覧できます (Next.jsを使った静的なHeadless WordPressサイトの作り方 - Shifter)。

ISRによるコンテンツ更新 (Incremental Static Regeneration)

上記の静的生成では、ビルドした時点のデータが表示されます。ではWordPress側で記事を追加・更新した場合、サイトにどう反映させるかが次の課題です。ここで役立つのがISR(インクリメンタル・スタティック・リジェネレーション)です。ISRを使うと、一度デプロイした静的ページをバックグラウンドで再生成(再ビルド)し、一定時間経過後もしくはリクエストトリガーによって新しいコンテンツを配信できます (Using ISR with WordPress API in Next.js Guide)。Next.jsではgetStaticPropsの戻り値にrevalidateというプロパティを指定することでISRを設定できます。

例えば、先ほどのgetStaticPropsに以下を追記します。

return {
  props: { posts },
  revalidate: 60  // ページを生成後、60秒経過すると背景で再生成(ISR)
};

これで、このページは最大60秒おきにバックグラウンド再生成され、WordPress上の新規投稿や更新があれば次のアクセス時に自動で新しいページに差し替わります (Using ISR with WordPress API in Next.js Guide)。60秒という値はコンテンツ更新頻度に応じて調整可能です (Using ISR with WordPress API in Next.js Guide)。ニュースサイトのように頻繁に更新するならもっと短く、ブログ程度なら数分~数十分でも良いでしょう。

注意: ISRを利用するには、Next.jsアプリをサーバー環境で実行している必要があります。つまりNext.jsが自前でサーバー処理を行える状態(Vercelや自前のNode.jsサーバー上でホスティング等)でなければISRは機能しません。今回の前提はXserverで完全静的にホスティングするケースなので、厳密にはISRは使えません(静的ホスティングにはNext.jsの再生成機能のバックエンドが無いため)。そのため、Xserver環境ではWordPressのコンテンツ更新時に再度ビルドしてアップロードしなおすことで対応するか、後述するXserverの機能(Xserver StaticやCI/CD)を用いて自動デプロイする仕組みを構築する必要があります。

一方で、もしNext.jsサイトを例えばVercelなどNodeサーバー対応のホスティングに置けるならISRをフル活用できます。ISRを活かしたい場合はそちらも検討してみてください(Xserver上でISR的な運用をする裏技的手法として、Cronで定期的にサイトを再エクスポート&アップロードするという方法も考えられます)。

ISR以外の更新手段:オンデマンドISRとWebhook

もう少し高度な話題として、WordPressで記事が更新されたタイミングでNext.js側に通知し、コンテンツを最新化する方法があります。Next.jsにはOn-Demand Revalidationという機能があり、特定のAPIエンドポイントを叩くことで指定したページの再生成を即座に行えます。WordPress側には「WebHook」といって記事公開時に任意のURLに通知を送る仕組み(プラグイン利用)があるため、例えば投稿の公開/更新時にNext.jsの再生成エンドポイントを呼び出すように設定すれば、ほぼリアルタイムに静的サイトを更新することも可能です。

ただし、この仕組みをXserver上で実現するのは難しいため(Next.js自体が動いていないため)、CI/CD(後述のGitHub Actionsなど)を活用して自動ビルド&FTPアップロードするのが現実的です (blog.masizime.com)。これについてはデプロイ方法の章で触れます。

2.3 記事詳細ページの実装例

次に、各記事の詳細ページ(個別ページ)を作成しましょう。記事詳細ページはWordPress上の各投稿に対応する個別のURLを持つので、Next.jsでは動的ルーティングを使用します。具体的には、pages/posts/[slug].js というファイルを作り、URLパラメータ([slug]の部分)に応じたページを生成します。

(A) 動的ルートとgetStaticPaths:
Next.jsで複数の静的ページをビルドする場合、getStaticPathsという関数をエクスポートしておく必要があります。これは「どのパス(URLパラメータ)を事前に静的生成するか」をNext.jsに伝える役割です。WordPressから全投稿のスラッグ一覧を取得し、そのスラッグごとにページを生成するよう指定します。

// pages/posts/[slug].js
export async function getStaticPaths() {
  const res = await fetch('https://your-wordpress-site.com/wp-json/wp/v2/posts');
  const posts = await res.json();
  const paths = posts.map(post => ({
    params: { slug: post.slug }
  }));

  return {
    paths,
    fallback: 'blocking'  // ISRを利用する場合の設定。ここでは説明簡略化のためblockingに。
  };
}

上記では、まず記事一覧を取得し、その中から各投稿のslugを取り出してpaths配列を組み立てています。例えば投稿のslugがhello-worldであれば、/posts/hello-worldというページを生成する指示になります。fallback: 'blocking'はISR併用時の挙動設定で、未生成ページにアクセスがあった際一旦SSR的に生成してから返すモードですが、初心者向けの記事なので深追いしません。基本的には公開済み記事すべてをpathsに含めればfallback: falseでもOKです。

(B) 個別ページのgetStaticPropsと表示:
続いて、各slugに対して記事データを取得します。WordPress REST APIでは個別記事取得のエンドポイントとして/wp-json/wp/v2/posts?slug=<slug名>やID指定の/posts/<ID>が利用できます。ここではslugで取得する例を示します。

export async function getStaticProps({ params }) {
  const { slug } = params;
  const res = await fetch(`https://your-wordpress-site.com/wp-json/wp/v2/posts?slug=${slug}&_embed`);
  const data = await res.json();
  const post = data.length > 0 ? data[0] : null;
  if (!post) {
    return { notFound: true };  // 万一slugに該当する記事がなければ404
  }
  return {
    props: { post },
    revalidate: 60  // (ISR利用時)60秒で再検証
  };
}

そしてページコンポーネント側では、記事タイトル・本文などを表示します。

export default function PostPage({ post }) {
  return (
    <article>
      <h1 dangerouslySetInnerHTML={{ __html: post.title.rendered }} />
      <div dangerouslySetInnerHTML={{ __html: post.content.rendered }} />
      {post._embedded?.['wp:featuredmedia'] && (
        <img 
          src={post._embedded['wp:featuredmedia'][0].source_url} 
          alt={post._embedded['wp:featuredmedia'][0].alt_text || post.title.rendered} 
        />
      )}
      {/* (カテゴリや著者情報の表示も必要に応じて追加可能) */}
    </article>
  );
}

以上で、WordPress上の全記事に対応する静的ページがNext.jsで生成されることになります。ビルド時に記事が100件あれば100ページ分のHTMLが出力されます。コンテンツ内の画像URLや内部リンクについては後述の「課題と対策」で触れますが、基本的にはWordPressから来たものをそのまま表示しています。

Next.jsでのリンクの扱い

上記のコードでは、記事一覧から詳細ページへのリンクに単純に<a href={/posts/${post.slug}}>を使いました。Next.jsにはnext/linkというコンポーネントがあり、これを使うとクライアント側でのページ遷移がパフォーマンス向上(プリフェッチ等)します (Next.jsを使った静的なHeadless WordPressサイトの作り方 - Shifter)。静的サイトとはいえ、JavaScriptが動作している状態ではシングルページアプリケーションのように遷移可能なので、開発時には<Link>コンポーネントを使用すると良いでしょう。

import Link from 'next/link';
// ...
<Link href={`/posts/${post.slug}`} legacyBehavior>
  <a>続きを読む</a>
</Link>

legacyBehaviorはNext.js 13での動作調整用(古い書き方互換)です。リンクに関しては、内部リンクはnext/linkを推奨、外部リンクは通常の<a>タグを使う、という使い分けを覚えておきましょう。

3. Next.jsサイトをXserverにデプロイする方法

Next.jsでヘッドレスフロントエンドが構築できたら、次はいよいよそれをXserver上に公開します。通常、Next.jsはNode.jsサーバー上で動かすこともできますが、Xserver(レンタルサーバー)はPHP+Apacheの環境でありNode.jsが動作しません (blog.masizime.com)。そのため、Next.jsのプロジェクトを静的なファイル群(HTML/CSS/JS)としてエクスポートし、その出力をアップロードするという手順を踏みます。

3.1 静的エクスポートの手順

  1. ビルドとエクスポートの実行: ローカルでNext.jsプロジェクトを準備し、以下のコマンドを実行します。
   npm run build   # プロダクションビルド
   npm run export  # 静的ファイルを書き出し

または、package.jsonのscriptsに"export": "next build && next export"と定義しておき、npm run exportだけでビルド+エクスポートをまとめて行うようにしてもOKです (Next.jsのデプロイをXserverでやったら禿げた件について)。コマンドが成功すると、プロジェクト直下にoutというディレクトリが生成され、その中に静的サイト一式が出力されます (Next.jsのデプロイをXserverでやったら禿げた件について)。例えばトップページはout/index.html、ブログ記事ページはout/posts/<slug>.html(後述の設定により変化)というようにHTMLファイルができます。併せて必要なCSSやJS、画像などの静的アセットもコピーされています。

  1. 出力物の確認: outフォルダ内のファイル構成を確認しましょう。注意点として、Next.jsのエクスポート結果はデフォルトでは各ページをHTML拡張子付きのファイルとして出力します。例えばaboutページがあるとabout.htmlというファイルになります。これはそのままアップロードすると、https://サイトURL/about.htmlでアクセスできる形です。しかし多くの場合、URLに拡張子.htmlを含めずに/aboutといったパスでアクセスさせたいでしょう。この点についてはURLと実ファイル名の整合を取る必要があります。 URLとファイル名の整合性:
  • 方法1: Next.js側で対応 – Next.jsの設定でtrailingSlash: trueを有効にすると、エクスポート時に各ページがフォルダとして出力され、その中にindex.htmlが置かれる形式になります (javascript - How to deal with NextJS exporting files with .html extension but in there is no .html - Stack Overflow) (javascript - How to deal with NextJS exporting files with .html extension but in there is no .html - Stack Overflow)。例えばaboutページはabout/index.htmlとなり、静的ホスティングでは/about/でアクセスすればそのindex.htmlが表示されます。Apacheサーバーではディレクトリへのアクセスを自動でindex.htmlに解決するため、trailingSlash: true設定により「ディレクトリ型の出力+自動リダイレクト(/about → /about/)」が実現できます (javascript - How to deal with NextJS exporting files with .html extension but in there is no .html - Stack Overflow)。これにより、開発中<Link href="/about">としていたリンクも静的環境で問題なく動作します。
  • 方法2: サーバー設定(.htaccess)で対応 – Next.jsのデフォルト出力(ファイル名がslug.html形式)をそのまま使い、.htaccessでURLを書き換える手法です。XserverはApache互換の環境なので、.htaccessファイルをアップロードすることでリライトルールを適用できます。例えば以下のような内容です (Next.jsのデプロイをXserverでやったら禿げた件について): RewriteEngine On # ファイルやディレクトリが実在しない場合、.htmlを付けて再試行 RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^(.*)$ $1.html [L,QSA] これにより、例えばブラウザから/posts/hello-worldにアクセスが来たとき、実ファイル/posts/hello-world(ディレクトリもファイルも存在しない)が見つからなければ、代わりに/posts/hello-world.htmlを探して表示するという動作になります (Next.jsのデプロイをXserverでやったら禿げた件について)。Next.jsエクスポートのHTMLファイル名と合致するため、ユーザーは拡張子を意識せずに閲覧できます。 初心者には方法1(Next.js設定)の方が手軽ですが、既にエクスポート済みで方法2を取りたい場合は.htaccessを自分で用意してoutフォルダ内に入れておくと良いでしょう。もちろん両方やる必要はありません。どちらか一方でOKです。
  1. Xserverで公開用ディレクトリの用意: Xserverでは契約時にメインドメインが割り当てられるほか、サブドメインや追加ドメインを設定できます。それぞれに公開用ディレクトリ(ドキュメントルート)が指定されます。例として、example.comがXserverでの公開サイトURLである場合、おそらくpublic_htmlフォルダ(またはexample.com_public等)にファイルを置けば公開されます。Xserverのサーバーパネルで対象ドメインのドキュメントルートを確認しましょう。既存にWordPressサイトがある場合はそのWordPressが置かれている場所です。ヘッドレス化にあたって、WordPressを別ディレクトリに移動し、代わりにNext.js静的サイトをルートに配置するケースや、単にサブドメインを用意してNext.jsサイトを公開するケースがあります。後者(例:本番サイトはそのまま、別のURLに静的サイトをデプロイしてテスト)は安全ですが、最終的にヘッドレスに切り替える際はWordPressの表示をオフにする必要があります。
  2. ファイルのアップロード: 用意したoutディレクトリの中身を、Xserverの公開ディレクトリにアップロードします。アップロード方法はいくつかあります。
  • 手動でFTP/SFTPクライアント(FileZillaなど)を使ってアップロードする。
  • Xserverのサーバーパネルにあるファイルマネージャー機能を使ってブラウザ経由でアップロードする。
  • GitHubなどにソースをプッシュしておき、GitHub Actions等のCI/CDツールで自動ビルド&FTP転送する (blog.masizime.com) (blog.masizime.com)。 少量のサイトなら手動でも構いませんが、記事更新のたびにビルド→FTPとなると手間がかかります。microCMSの例ではGitHub Actionsでビルド→FTPデプロイを自動化しています (blog.masizime.com) (blog.masizime.com)。WordPressの場合も、更新が頻繁なら似た仕組みを構築できると便利です。例えばWordPress側でWebhookプラグインを入れ、記事更新時にGitHubリポジトリに対してActionを起動するリクエストを送り、自動で再ビルド&アップロードする…という流れです (blog.masizime.com)。XserverはXserver Staticという静的サイトホスティングサービスも提供しており、そちらはGitHubと連携して自動デプロイできる仕組みがあります (MicroCMSのデータをNextjsのSSGをつかってXserver Staticで静的配信 #Next.js - Qiita) (MicroCMSのデータをNextjsのSSGをつかってXserver Staticで静的配信 #Next.js - Qiita)。必要に応じて検討するとよいでしょう。
  1. 動作確認: アップロードが完了したら、実際にブラウザでサイトにアクセスしてみます。トップページが表示され、各記事リンクをクリックすると記事詳細が見られるはずです。リンク切れや画像表示漏れがないか確認しましょう。もし内部リンクが正しく機能しない場合、.htaccessの記述ミスやパス指定のミスが考えられます。 補足: Xserverでは独自にキャッシュやWAF設定が有効になっていることがあります。最初はそれらを無効化してトラブルシュートし、問題なく動いたら必要に応じてキャッシュONにするなど調整するとよいでしょう。また、WordPressを同居させている場合、WordPress側の.htaccess(例えばリダイレクト設定)と競合しないよう注意が必要です。

以上が基本的なデプロイ手順です。一度静的サイトとして公開してしまえば、Xserver上では通常のHTMLサイトと同様の扱いになります。サーバーサイドで動的処理がない分、表示は高速でサーバー負荷も軽くなります。グローバルに配信する場合はCloudflareなどCDNを噛ませることでさらに高速化が可能です。実際、従来のWordPressサイトをNext.js静的サイトに移行したケースでは、TTFB(Time To First Byte)が劇的に改善し世界中どこからアクセスしても高速になったという報告もあります (因創科技 | Next.js + WordPress - Part 2) (因創科技 | Next.js + WordPress - Part 2)。

4. ヘッドレス化で直面しやすい課題とその解決策

ヘッドレスWordPressは柔軟で高速ですが、従来のWordPressテーマで完結していた環境と比べて開発や運用上の課題もいくつか存在します。最後に、特に注意すべきポイントとその対策を整理します。

4.1 カスタム投稿タイプやカスタムフィールドの扱い

課題: 前述したように、カスタム投稿タイプ(CPT)は設定次第でREST APIに露出されません (〖WordPress〗WP REST APIで「カスタム投稿タイプ」の記事一覧を取得する | 株式会社エンタースクウェア | Web制作)。またカスタムフィールド(メタデータ)も標準ではREST APIのレスポンスに含まれません。例えば投稿に紐付いたメタキーsubtitleがあっても、通常の/wp-json/wp/v2/postsのレスポンスには出てこないのです。

対策:

  add_action('rest_api_init', function() {
    register_rest_field('post', 'subtitle', [ 
      'get_callback' => function($post_arr) {
        return get_post_meta($post_arr['id'], 'subtitle', true);
      }
    ]);
  });

こうすると投稿エンドポイントの各アイテムにsubtitle項目が追加され、Next.jsからも取得できます (〖WordPress〗WP REST APIで「カスタム投稿タイプ」の記事一覧を取得する | 株式会社エンタースクウェア | Web制作)。

4.2 プラグインの互換性と代替実装

課題: WordPressには無数のプラグインがありますが、その多くはフロントエンドで動作することを前提としています。ヘッドレス化すると、プラグイン由来のショートコードやウィジェット、スクリプトなどがそのままでは機能しない場合があります。

対策:

  • ショートコード: REST APIで投稿内容を取得するとき、WordPressはデフォルトで本文に対してthe_contentフィルターを適用するため、ショートコードもレンダリングされたHTMLとして返されます。したがって、単純な埋め込みやレイアウト系のショートコード(例: <div>...</div>のギャラリーHTMLになる等)はNext.js側でもそのHTMLを表示できます。しかし、JSやCSSに依存する高度なショートコード(スライダーやタブ切り替え等)は、見た目のHTMLは出力されても期待通り動作しないことがあります。これらはNext.js側で対応するか、プラグインに依存しない実装に置き換える必要があります。
  • フォーム系プラグイン: Contact Form 7などのフォームプラグインはショートコードでフォームHTMLを出力し、さらにAjax送信のJavaScriptもテーマに挿入しています。ヘッドレス環境ではそのJSが働かないため、フォームを送信しても何も起こらない、という事態になります。対策としては、プラグインが提供するREST APIエンドポイントを直接呼び出す(CF7にはREST APIがあり、/contact-form-7/v1/contact-forms/<id>/feedbackにPOSTすることで送信処理可能)、もしくはフォーム自体を別途Reactコンポーネントとして実装し、WordPressを介さずメール送信を行う、などが考えられます。後者の場合、フォーム処理用にサーバーレスFunctionや外部のメールAPIを使うことになるでしょう。
  • SEO系プラグイン: Yoast SEOなどのプラグインは各投稿のメタタイトル・ディスクリプション、OGPタグ等を生成します。ヘッドレスではそれらをNext.js側で組み込む必要があります。Yoastの場合、REST APIでメタ情報を取得するエンドポイント(wp-json/wp/v2/posts/<id>/yoast_head など)があるので、それを利用してNext.jsの内に挿入することができます。または、WP側でYoastが出力した値をカスタムフィールドとしてREST APIに載せ、それをNext.jsで使う方法もあります。いずれにせよSEO設定はWordPress任せではなく、自分でNext.jsのページに反映させる作業が必要です。
  • その他フロントエンド依存の機能: SNSシェアボタン、コメントシステム、サイト内検索、人気記事ウィジェットなど、テーマに紐付いていた機能はすべてNext.jsで再実装することになります。コメント機能はWordPress REST API経由で取得・投稿(エンドポイントがあります)するか、Disqusのような外部サービスに任せるかです。サイト内検索は前述のREST APIのsearchパラメータをNext.jsから呼び出すか、Algoliaのようなサービス導入も検討できます。人気記事はGoogle Analyticsや別途APIを使って算出する等、WordPressのプラグインに頼らない手法に切り替えます。

ポイント: 「WordPressプラグインでやっていたことをNext.jsでどう実現するか?」を一つ一つ洗い出して埋めていく作業が必要です。場合によっては完全に諦める(機能自体削る)決断も必要になるかもしれません。ヘッドレス化のメリット(速度・自由度・セキュリティ)と、デメリット(実装コスト増・一部機能喪失)を比較衡量して、導入範囲を決めましょう。

4.3 画像やメディアの扱い

課題: 記事内で使用される画像や動画などのメディアは、通常WordPressのwp-content/uploadsに保存されています。ヘッドレス化しても、画像URLはWordPress側のドメインを指したままです。この状態だと、ユーザーが画像を表示する際にWordPressのドメインにアクセスすることになり、場合によってはWordPressの存在やディレクトリ構造が露呈します。また、WordPress側を認証制限した場合に画像まで見られなくなる恐れもあります。さらに、Next.jsで静的エクスポートする際はnext/imageの自動最適化が使えないため、画像サイズ・フォーマットの最適化を自前で考える必要も出てきます。

対策:

  • 画像の出力先: WordPressメディアライブラリの画像をそのまま使う場合、基本的には公開状態にしておく必要があります(そうでないとNext.jsから取得できない)。セキュリティ上問題があれば、画像だけ別の安全なCDNにホスティングすることも考えられます。例えばWordPressにアップロードされた画像を自動でAWS S3やGoogle Cloud Storageにコピーするプラグインを使い、配信はそちらのクラウドから行うという方法です (因創科技 | Next.js + WordPress - Part 2)。こうすればWordPressのドメインを隠しつつ、高速な画像配信が可能です。
  • 画像最適化: 静的サイトでは、事前に適切なサイズの画像を用意しておくことが重要です。大きすぎる画像はアップロード時にWordPressが自動生成するサムネイル(例: medium, largeサイズ)を利用しましょう。レスポンシブ対応で<img srcset>属性を記事HTMLに含めたい場合は、WordPressのsrcset機能が埋め込んでくれるので、REST APIのcontent.renderedにもそれが含まれます。つまりWordPressのテーマが適切なら、API経由でも<img src="...large.jpg" srcset="...small.jpg 300w, ...large.jpg 800w, ...">のように複数サイズが出てくるはずです。Next.js側ではそれをそのままdangerouslySetInnerHTMLで出力すれば、ブラウザが適切な方を選んで表示します。
  • Next/Imageの代替: どうしてもNext.jsで画像最適化をしたければ、Next.jsアプリ自体をNodeサーバーで動かし続ける必要があります。ただ今回の前提ではそれはしないので、外部の画像最適化サービス(例: Cloudinary、imgix)を利用する手もあります。例えばCloudinaryにメディアを置いておけば、自動でWebP配信やリサイズが行われます。そこまでしなくとも、適切な圧縮形式(JPEG/PNG/WebP)と圧縮率でWordPressに画像をアップロードする運用にすれば大丈夫でしょう。

4.4 内部リンクの扱い

課題: WordPressの記事本文中には、他の投稿やページへの内部リンクが含まれる場合があります。これらリンクは通常、WordPressサイトのフルURL(絶対URL)で記述されています。ヘッドレス化によってサイトURLが変わったり、公開するドメインが変わった場合、このリンク先が古いWordPressのURLのままになってしまい、クリックするとヘッドレスではない元のWPページに飛んでしまう、という問題が起こりえます。

対策:

  • コンテンツの一括置換: 最も確実なのは、WordPressのデータ側でリンクURLを新しいものに置換してしまうことです。例えばWordPressのドメインがold.example.comでNext.jsサイトはnew.example.comで公開するなら、データベース上でコンテンツ中のold.example.comnew.example.comに置換します。プラグインのBetter Search Replaceなどを使うと簡単です。ただし運用上、WordPressプレビューでリンクが機能しなくなる等の弊害もあるため慎重に。
  • APIレスポンス処理で置換: Next.js側でfetchした後、レンダリングする前に文字列置換する方法です。post.content.renderedに含まれるhttp://old.example.com/を全て/や新URLに差し替える処理をJSで行います。例えば:
  post.content.rendered = post.content.rendered.replace(/https?:\/\/old.example\.com\//g, "/");

とすれば、そのリンクはルート相対パスに置き換わります。サイトを同一ドメインで運用するならルート相対パス/slugでOKですし、別ドメインならhttps://new.example.com/slugに置換すれば良いでしょう。なお、WordPressのREST APIはリンク先のURLを<a href=\"http://...\"></a>のようにエンコードして返すので、正規表現パターンに注意してください。

  • リンククリック時のハンドリング: もう一つ、Next.jsアプリ内でクライアントサイドにJSを仕込んで対応する手もあります。例えば、レンダリング後に記事コンテンツ内のaタグを走査し、hrefが旧ドメインなら現在のサイト内リンクに書き換える、というスクリプトを動かす方法です。ただしこれはユーザーのブラウザ上で書き換えるため、一瞬とはいえ誤ったリンクが存在することになるので、あまり好ましくありません。

可能であればサーバーサイド(ビルド時)で正しいリンクにしてしまう方がSEO上も安心です。また、WordPress側のサイトURL (General SettingのSite URL) をNext.jsの公開URLに合わせてしまうという荒技もあります(ただしWP側はヘッドレスなのでフロントとしてはアクセスさせないこと前提)。

実際の開発事例では、「WordPressバックエンドのURLをNext.jsフロントのURLに置換すること」は要注意ポイントとして挙げられています (因創科技 | Next.js + WordPress - Part 2)。特にACF等でURLを入力している場合も全て置換を忘れないようにしましょう。こうした置換漏れがあると、せっかくヘッドレス化してもユーザーがWordPressのURLを直接目にしたり、最悪そこからWP管理画面の存在を辿られる可能性も出てきます。

4.5 その他の注意点

上記以外にも細かなポイントはいくつかあります。

  • サイトマップ: WordPressのプラグイン(Yoast SEOなど)が出力していたXMLサイトマップはヘッドレス化すると更新されなくなります。Next.js用に新たにサイトマップを生成する(例えばnext-sitemapパッケージを使う (因創科技 | Next.js + WordPress - Part 2))か、WP側でREST API経由で取得できる全URLを使って独自にXMLを出力する仕組みを設けます。SEOの観点ではサイトマップも忘れずに対応しましょう。
  • ページネーション: ブログ一覧のページネーション(ページ送り)も、自動ではなくNext.js側で実装が必要です。REST APIの/posts?per_page=10&page=2などのパラメータを活用し、getStaticPathsでページ番号を生成するか、クライアントサイドページネーションにするか決めます。静的サイトとしては、あらかじめ想定ページ数まで作っておく方がシンプルです。
  • 下書き・プレビュー: 編集者が記事を公開前にプレビューする機能も、自前で仕組みを組む必要があります。Next.jsにはPreview Modeがあり、WordPress側からプレビュートークン付きURLを開くことで下書き内容をNext.jsで表示できます。これにはWordPressのプラグイン「Headless Mode」や「WPGraphQL」を使う方法もありますが、初心者には難しいため、まずはプレビュー機能無しで運用し、必要になったら検討すると良いでしょう。
  • ビルド時間: 投稿数が極端に多い場合、Next.jsのビルド(静的生成)に時間がかかります。数千件規模なら問題ありませんが、数万件となるとGitHub Actionsの実行時間制限に引っかかったり、デプロイが長時間になる可能性もあります。その場合、ISR対応の環境に移行してビルド時間を分散させるか、あるいは古い記事をサイトから除外する(例えば直近○件のみ静的化し、古いものはアーカイブページだけ用意するとか)検討が必要かもしれません。

おわりに

Next.jsを用いたWordPressのヘッドレスCMS化について、概要から具体的な実装方法、運用上の注意点まで解説しました。ヘッドレス化は確かに初期構築のハードルが上がりますが、一度仕組みを作ってしまえば高速性・拡張性・安全性で多くのメリットを享受できます。特に表示速度向上の効果は大きく、ユーザー体験やSEOの面でも良い結果をもたらすでしょう (因創科技 | Next.js + WordPress - Part 2) (因創科技 | Next.js + WordPress - Part 2)。

一方で、全てのプロジェクトにヘッドレスが適しているわけではありません。サイト規模や更新頻度、求められる機能によっては従来型のWordPressの方が手軽で十分な場合もあります (Next.js/Wordpress headless CMS : r/nextjs)。ヘッドレス化は「フロントエンドを自由に開発したい」「既存のWordPressではパフォーマンスや制約に不満がある」といったケースで特に威力を発揮します。皆さんのプロジェクト要件と照らし合わせ、最適なアプローチを選択してください。

最後に、本記事で取り上げた内容の参考リンクを以下にまとめます。具体的なコード例や詳細なノウハウはこれらにも記載されていますので、ぜひ参照してみてください。

参考文献・リンク