🥪

Next.js × WP REST API でブログをリニューアルする時の注意点や学んだこと

2023/02/12に公開

実際にリニューアルしたサイトはこちらです。

https://modulesss.com/


WordPress製サイトをNext.jsでリニューアルした際に色々苦戦したので、注意点や苦戦した点をまとめました。同じような境遇の方の参考になれば幸いです。

リニューアル前後の環境は以下の通りです。

  • リニューアル前
    • フロントエンド、サーバーサイドは当然WordPress
    • サーバーはロリポップ(ドメインはムームードメインで取得)
  • リニューアル後
    • フロントエンド:Next.js
    • サーバーサイド:WordPress(WordPress REST APIを利用)
    • ホスティングサービス:Netlify

ヘッドレスCMSの選定

ヘッドレスCMSとは?
ヘッドレスCMSはコンテンツの管理のみをするCMS(=Content Management System)のこと。例えばWordPressではコンテンツの作成から表示までを行えますが、ヘッドレスCMSではコンテンツの表示は別の方法で行います。ここではNext.jsを使ってみます。
参考:https://www.webcreatorbox.com/tech/wordpress-headless-cms-nextjs

ヘッドレスCMSの選択肢は2つあります。

  • WordPressをそのまま採用する
  • 外部サービスにデータを移行して利用する

WordPressをそのまま採用する場合

WordPressをそのまま採用する場合、データをそのまま使えるので特に何もすることはありません。

しかし、 リニューアル前のWordPressのドメインをリニューアル後も使う場合、リニューアル前のWordPressを別ドメインに移行する必要があります。 こちらに関しては後ほど解説します。

外部サービスを採用する場合

microcmsCMSNewtなどの外部サービスを採用する場合、データを移行しなければいけません。

microcmsCMSであればCSVによるデータインポート機能があります。

しかし、投稿データや画像(メディア)が大量にあったり、WordPressの構造によってはデータ移行がかなり大変です。私はこれが厳しくて外部サービスを使うのは諦めました。

以下の記事ではmicrocmsCMSへの移行方法をかなり詳しく解説してくれているので、外部サービスを採用する方は是非参考にしてみてください。

https://zenn.dev/kandai/articles/f6a034d166e4c977a78e

WordPress API の選定

ヘッドレスCMSにWordPressを採用する場合、どのようにデータを取得するかを決めなければいけません。

選択肢としては4つあります(本当はもっとあるかも...)。

WP REST API

一番シンプルな方法です。

fetchaxiosで純正のWP REST APIのURLを叩いてデータを取得します。

https://exapmle.com/wp-json/wp/v2/◯◯◯◯◯

参考記事が多く、困ってもググればどうにかなります。私はこの方法を採用しました。

https://designsupply-web.com/media/programming/6307/

https://wp-rest-api.mydocument.jp/

最初はどのパラメータを与えればどんなデータが返ってくるのか全然イメージできていなかったので、APIのテストツールを使って色々試してみました。

APIのテストツールはこちらのChrome拡張機能が一番手軽で使いやすかったです。

https://chrome.google.com/webstore/detail/talend-api-tester-free-ed/aejoelaoggembcahagimdiliamlcdmfm

node-wpapi

WP REST API用のJavaScript SDKです。

var WPAPI = require( 'wpapi/superagent' );
var wp = new WPAPI({ endpoint: 'http://src.wordpress-develop.dev/wp-json' });
// Request methods return Promises.
wp.posts().get()
    .then(function( data ) {
        // do something with the returned posts
    })
    .catch(function( err ) {
        // handle error
    });

https://github.com/WP-API/node-wpapi

先程のWP REST APIを扱いやすくしてくれます。

しかし、私はドキュメントを読み解く力が劣っていたので、なかなか思い通りにデータを取得することができませんでした...。ドキュメントをしっかりと読める方はこちらのほうが良いのかなと思います(参考記事が純正のWP REST APIよりはかなり少ない印象でした)。

GraphQL API for WordPress

GraphQLというプラグインを使って専用のAPIを作成してデータを取得します。

https://www.wpgraphql.com/

こちらの方法は開発中にたまたま見つけたので試してはいないのですが、もしかしたら一番いい方法かも...と思いました。

純正のWP REST APIよりレスポンス速度が早かったり、返ってくる値が綺麗だったりするみたいです。管理画面でjsonプレビューもできるっぽいです。もっと早く知りたかった...

https://qiita.com/izanari/items/4755019d88466cb49048

独自のAPIを作る

純正のWP REST APIだと最大投稿取得数が100件までなど、様々な制約が存在します。また、WordPressの構造は多種多様です。

要件によっては独自のAPIを作ったほうがいい場合もありそうです。

https://notes.sharesl.net/articles/751/

https://designsupply-web.com/media/programming/6327/

ホスティングサービスの選定

ホスティングサービスは数多くありますが、今回は代表的なVercelNetlifyどちらを使うべきかを解説します。

サイトの読み込み速度を比較するとVercelに軍配が上がるみたいなので、2択で迷ったらVercelを選べばいいと思います。

https://www.lambdar.me/archives/migrating-to-vercel-from-netlify-due-to-performance-issues/

しかし、next/imageを使っている場合は注意が必要です。

Vercelはnext/imageで読み込んだ画像をデプロイ時に最適化してくれるのですが、月1,000件を超えるとプランのアップグレードを促され、それを無視し続けるとアプリが停止されます。

なので、月1,000件を超える場合はNetlifyにするか、next/imageunoptimizedを与えるなどの対策が必要です。

https://mr-ozin.hatenablog.jp/entry/2021/10/15/005554

https://qiita.com/0ba/items/46709d24bc81efe1cb6c

WordPress製のサイトが画像が多くなりがちなので気をつけましょう。

私は画像データが2,000を超えていたのでNetlifyにしました...。

WordPressのサーバー移管

ヘッドレスCMSにWordPressを採用し、かつリニューアル後のドメインをそのまま使う場合、リニューアル前のWordPressを別ドメインへ移管する必要があります。

ここでは、別ドメインを用意している前提で、WordPressのデータ移管方法を解説していきます。

データ量が「少ない」場合の移管方法

移管するデータ量が少ない場合、プラグインの「All-in-One WP Migration」を使えば一瞬で移管できます。

https://ja.wordpress.org/plugins/all-in-one-wp-migration/

https://blog-bootcamp.jp/start/wordpress-allinonewpmigration/

しかし、データ量が多い場合は結構大変です。

「All-in-One WP Migration」にはインポートデータのサイズ上限があるので、それを超えてしまう場合は手動で移管する必要があります。

データ量が「多い」場合の移管方法

1. バックアップ

まずはバックアップを取ります。超重要です。

私はミスって2回ほどDBを消してしまったので、バックアップを取っていなかったら終わっていました...。

バックアップを取るにはいくつかの手段があるのですが、その全ての手段で取っておきましょう(ミスしてなにかやらかした時に復元する時の選択肢が多いに越したことはないので)。

以下の手段でバックアップを取ります。

  • WordPress標準機能のエクスポート
  • データベースのエクスポート
  • プラグインによるバックアップ

■WordPress標準機能のエクスポート

サイドバー > ツール > エクスポートから実行できます。

まずは「すべてのコンテンツ」でエクスポートをして、次に「投稿」や「フィールド」などの単体でもエクスポートしておきましょう。

また、エクスポートしたままのファイル名だと分かりにくいので、どれがどのデータなのか分かるようにファイル名を変えておくのも重要です。

■データベースのエクスポート

データベースのエクスポートは、利用しているサーバーのphpMyAdminから実行するのが一番分かりやすいと思います。

1つのデータベースで複数のWordPressを管理している場合、接頭辞でテーブル分けをしていると思います。これが地味に紛らわしいので、間違って違うサイトのDBを操作しないように気をつけましょう。

https://rilaks.jp/wordpress/table-prefix/

データベースのエクスポートも、関連テーブル全体と単体でエクスポートしておくのがおすすめです(SQLに慣れている方なら全体のみだけでも大丈夫かなと...)。

■プラグインによるバックアップ

「UpdraftPlus」が優秀過ぎました。間違ってDBのテーブルを消してしまってもワンクリックで復元できました。

https://ja.wordpress.org/plugins/updraftplus/

バックアップファイルの保存先はGoogleドライブが一番手軽で設定しやすいかなと思います。

2. メディアのダウンロード

画像やファイルのメディアを全てダウンロードします。管理画面上からはできないので、FTPやSSHで一括ダウンロードしましょう。

ダウンロードする際は/wp-content/uploads/をまるまるダウンロードします。

3. データの移管

移管先でテーマやプラグインなどのセットアップが完了したらデータをインポートしていきます。

  1. 標準機能でエクスポートした「すべてのコンテンツ」のデータをインポート
    • データ量が大きすぎてインポートが進まない場合は、「投稿」や「フィールド」など単体のデータでひとつずつインポートすれば大丈夫だと思います
  2. メディアをアップロード(ディレクトリ構成は以前と同じにする)
    • 「1.」のエクスポートデータだとメディアは反映されないので、手動でアップロードする必要があります
    • 私は2,000件くらいのメディアをアップロードするのに1時間ほどかかりました...
  3. 管理画面からメディアを確認して、正しく表示されていなかったら「4.」を実行
  4. phpMyAdminなどでwp_postsテーブルをインポート
    • このテーブルには投稿やメディアなどの情報が入っています
  5. 管理画面からカスタムフィールド系を確認して、正しく表示されていなかったら「6.」を実行
  6. phpMyAdminなどでwp_postmetaテーブルをインポート
    • このテーブルには投稿に紐づくカスタムフィールドの情報が入っています
  7. 他にも表示がおかしい箇所があったら、それに関するDBのテーブルをインポートする

https://illustswitch.com/blog/database-table/

本当は1, 2だけで大丈夫だと思っていたのですが、私は正しくメディアが表示されなかったのでDBのテーブルを直接インポートしました。

このあたりの作業は特に慎重に行いましょう。

特にDB操作は一歩間違えたらデータが全部ぶっ飛びます。

もし怖かったらレンタルサーバーのバックアップオプションに作業時だけ加入するのもありだと思います。数百円程度で安心を買えると思えばかなり安いはずです。

ホスティングサービスにカスタムドメインの設定

VercelNetlifyにドメインの設定をします。

カスタムドメインの設定方法はVercel レンタルサーバー名 カスタムドメインのようにググればたくさん出てくるのでそんなに苦戦しないと思います。

私の場合、DNSを変更して数十分〜半日ほどで設定が反映されました。

ヘッドレスCMS側のSEO対策

ヘッドレスCMS側のWordPressは検索エンジンのクローラにインデックスされないようにします。

管理画面の設定から表示設定の一番下の「検索エンジンでの表示」で「検索エンジンがサイトをインデックスしないようにする」にチェックを入れておきましょう。

こうしないとヘッドレスCMS側のサイトもクロールされてしまい、本体側のサイトが重複コンテンツと判断されSEO評価が下がる恐れがあります。

https://honmaru.red/1657/

また、ヘッドレスCMS側はあくまでコンテンツを管理するためだけに使うので、トップページや記事一覧ページは表示されないようにしたり、basic認証やリダイレクト設定をかけておくとより安心だと思います。

カスタム投稿を使っている場合の注意点

カスタム投稿やカスタムタクソノミーはデフォルトだとAPIが許可されていません。

functions.phpでカスタム投稿やカスタムタクソノミーを定義する際にshow_in_restオプションを与えることでAPIが許可されます。

// カスタム投稿
register_post_type('news', [
  // 省略
  'show_in_rest' => true,
  'rest_base' => 'news', // この値がURLのベースとなる(省略可)
]);

// カスタムタクソノミー
register_taxonomy('news-category', [
  // 省略
  'show_in_rest' => true,
]);

https://www.tam-tam.co.jp/tipsnote/cms/post9688.html

show_in_restを追加してもAPIからデータが取得できない場合は、API自体を無効にしている場合があります。functions.phpやプラグインを見直してみましょう。

https://freelancemate.me/posts/wordpress-api#h9d1151aa11

カスタムフィールドを使っている場合の注意点

WP REST APIで投稿情報を取得する場合、カスタムフィールドの値はデフォルトだと戻り値に含まれません。

ACFでカスタムフィールドを作成しているのであれば、専用プラグインのACF to REST APIでACFの情報を戻り値に含めることができます。

https://ja.wordpress.org/plugins/acf-to-rest-api/

ACF以外の方法でカスタムフィールドを追加している場合、こちらの記事で紹介されているプラグインを使えば戻り値にカスタムフィールドの情報を含められます。

https://www.ninton.co.jp/archives/5440

投稿タイトルを呼び出す際の注意点

HTMLタグを含む投稿本文を出力する場合は、NextのdangerouslySetInnerHTMLを使うと思いますが、投稿タイトルを出力する際もdangerouslySetInnerHTMLを使いましょう。

<h3
  className={styles.title}
  dangerouslySetInnerHTML={{ __html: title.rendered }}
></h3>

https://nextjs.org/docs/basic-features/script#inline-scripts

投稿タイトルに半角スペースやハイフンが含まれている場合、&#8211;のような文字参照に変換されているので、それをdangerouslySetInnerHTMLで変換する必要があります。

WP REST API のURL形式と実際に使ったパラメータ

カスタム投稿系のURL形式

// カスタム投稿
https://example.com/wp-json/wp/v2/投稿タイプ名

// タクソノミー
https://example.com/wp-json/wp/v2/タクソノミースラッグ

実際に使ったパラメータ

パラメータ名 値の例 概要
context embed viewなど 取得するデータの種類。デフォルトのviewだと投稿本文が含まれているのでかなりのデータ量になってしまう。記事一覧で使うならembedで問題ない。
per_page 10 50 100など 取得する投稿件数。100が上限。
page 1 2 3など 指定したページの投稿を取得。per_pageを基準にページ数は構築される。例)投稿総数が100件でper_page30ならページ数は4になる。
orderby title id descriptionなど 並び順
_fields title id acfなど ここで指定したフィールドのみが取得される。
カテゴリースラッグ カテゴリーID ここで指定したカテゴリーを持つ投稿が取得される。

※パラメータは他にもたくさんあります

WP REST API で情報を取得するサンプルコード

以降で紹介するコードはカスタム投稿、カスタムタクソノミー前提になりますのでご注意ください(通常の投稿でもURLを少し変えれば問題ないです)。

カスタム投稿を取得

// 関数定義
export async function getPost(_addParams) {
  // APIのURL生成
  const createFetchURL = () => {
    const baseUrl = 'https://example.com/wp-json/wp/v2/投稿タイプ名'; // ドメインや投稿タイプ名は.envで定義するのが良き
    const addParams = _addParams || {};
    const per_page = _addParams.per_page || process.env.NEXT_PUBLIC_WP_POST_LIMIT;
    const context = _addParams.context || 'embed';
    const temporaryUrlParam = {
      context: context,
      per_page: per_page,
      ...addParams
    }
    const urlParam = Object.entries(temporaryUrlParam)
      .map((_param) => _param.join('='))
      .join('&');
    return `${baseUrl}?${urlParam}`;
  }
  // API実行
  const response = await fetch(createFetchURL()).then((response) => response);
  // 投稿一覧をjson化
  const posts = await response.json();
  // 投稿総数とページ総数はレスポンスヘッダーの中にあるからget()で取得
  const totalPosts = await response.headers.get('x-wp-total');
  const totalPages = await response.headers.get('x-wp-totalpages');
  // 投稿一覧、投稿総数、ページ総数をreturn
  return {
    posts: posts,
    totalPosts: Number(totalPosts),
    totalPages: Number(totalPages),
  };
}
// 関数呼び出し
export async function getStaticProps(context) {

  // 追加パラメータ無し
  const posts = await getPost();

  // 投稿10件取得
  const posts = await getPost({
    per_page: '10',
  });

  // タームに属する投稿のみ取得
  const posts = await getPost({
    タクソノミー名: 'タームID',
  });

}

カスタムタクソノミーを取得

// 関数定義
export async function getAllCategory() {
  // APIのURL生成
  const createFetchURL = () => {
    const baseUrl = 'https://example.com/wp-json/wp/v2/タクソノミースラッグ'; // ドメインやスラッグは.envで定義するのが良き
    const temporaryUrlParam = {
      per_page: 100,
      _fields: 'id, count, description, name, slug',
    }
    const urlParam = Object.entries(temporaryUrlParam)
      .map((_param) => _param.join('='))
      .join('&');
    return `${baseUrl}?${urlParam}`;
  }
  // API実行
  const response = await fetch(createFetchURL()).then((response) => response.json());
  return response;
}
// 呼び出し
export async function getStaticProps(context) {
  const categories = await getAllCategory();
}

pages/のサンプルコード

カテゴリーごとの記事一覧ページ

URLは/category/[slug]になります。

pages/category/[slug]/index.jsx
import { getAllCategory } from '@/api/getAllCategory';
import { getPost } from '@/api/getPost';

export default function Post({ posts, categories, currentCategory }) {
  return (
    <>
      // ...
    </>
  );
}

export async function getStaticPaths() {
  const categories = await getAllCategory();
  const paths = categories.map(({ slug }) => {
    return {
      params: {
        slug: slug,
      },
    };
  });
  return {
    paths: paths,
    fallback: false,
  };
}

export async function getStaticProps(context) {
  // カテゴリー一覧を取得
  const categories = await getAllCategory();
  // 今のページのカテゴリースラッグ
  const currentCategorySlug = context.params.slug;
  // 今のページのカテゴリーオブジェクトを取得
  const currentCategory = categories.find(({ slug }) => slug === currentCategorySlug);
  // 今のページのカテゴリーに属する投稿一覧を取得
  const posts = await getPost({
    タクソノミースラッグ: currentCategory.id,
    _fields: 'title, id, acf', // 必要な情報のみ取得
  });
  return {
    props: {
      posts: posts,
      categories: categories,
      currentCategory: currentCategory,
    },
  };
}

カテゴリーごとの記事一覧ページ(◯ページバージョン)

ページネーションで使う2ページ目, 3ページ目...用のテンプレートです。

URLは/category/[slug]/page/[page]になります。

pages/category/[slug]/page/[page].jsx
import { getAllCategory } from '@/features/post/api/getAllCategory';
import { getPost } from '@/features/post/api/getPost';

export default function Post({ posts, categories, currentCategory, currentPage }) {
  return (
    <>
      // ...
    </>
  );
}

export async function getStaticPaths() {
  const categories = await getAllCategory();
  let paths = [];
  await categories.reduce(async (_prevPromise, _category) => {
    await _prevPromise;
    const { id } = _category;
    const posts = await getPost({
      タクソノミースラッグ: id,
      per_page: 1, // 返ってくる値にページ総数が含まれているから、取得する投稿数は1件のみでOK
      _fields: 'id',
    });
    const { totalPosts } = posts;
    const arrayTotalPages = Array(
      Math.ceil(totalPosts / Number(100)), // 100 = 1ページあたりに表示する投稿数。本来は.envファイルで定義している
    )
      .fill(true)
      .map((_value, _index) => _index + 1);
    arrayTotalPages.forEach((_page) => {
      paths.push({
        params: {
          page: _page.toString(),
          slug: _category.slug,
        },
      });
    });
  }, Promise.resolve());
  return {
    paths: paths,
    fallback: false,
  };
}

export async function getStaticProps(context) {
  // カテゴリー一覧を取得
  const categories = await getAllCategory();
  // 今のページの情報
  const { slug: currentCategorySlug, page: currentPage } = context.params;
  // 今のページのカテゴリーオブジェクトを取得
  const currentCategory = categories.find(({ slug }) => slug === currentCategorySlug);
  // 今のページのカテゴリーとページ数に属する投稿一覧を取得
  const posts = await getPost({
    [process.env.NEXT_PUBLIC_WP_CATEGORY_SLUG]: currentCategory.id,
    page: currentPage,
  });
  return {
    props: {
      posts: posts,
      categories: categories,
      currentCategory: currentCategory,
      currentPage: currentPage,
    },
  };
}

まとめ

Next.js × WP REST API リニューアルで一番苦戦したのは、ぶっちぎりで WordPressの移行作業でした。

最初はmicroCMSに移行させようと思ったのですが、データ量が多すぎて断念。その後、既存ドメインはリニューアル後も使いたかったので別ドメインへ移行を試みましたが、それが実装よりも圧倒的に大変でした。

投稿数、メディア数共に2,000を超えていたのでダウンロードするだけで1, 2時間ほどかかり、慣れないデータベース手動移行作業で間違ってテーブルを削除した時は「終わった...」と思いました。
(バックアップに救われました)

私の場合、ひとつのデータベースで20以上の複数サイトを一元管理していたので、接頭辞が違うだけで似たようなテーブル名が大量にあり、それにより誤操作も連発していました。

とにかく移行作業は普段の10倍慎重に、バックアップは必ず取る、指差し確認声出し確認などを意識することをおすすめします。


実際にリニューアルしたサイトはこちらです。

周りのデザイナーの方に共有頂けると嬉しいです🙇‍♂️

https://modulesss.com/

https://twitter.com/dai_webp/status/1623975057130426369?s=20&t=jAuNaiI5JB6Jlg1hSsYRvA

ネクスキャット テックブログ

Discussion