Next.js × WP REST API でブログをリニューアルする時の注意点や学んだこと
実際にリニューアルしたサイトはこちらです。
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を別ドメインに移行する必要があります。 こちらに関しては後ほど解説します。
外部サービスを採用する場合
microcmsCMSやNewtなどの外部サービスを採用する場合、データを移行しなければいけません。
microcmsCMSであればCSVによるデータインポート機能があります。
しかし、投稿データや画像(メディア)が大量にあったり、WordPressの構造によってはデータ移行がかなり大変です。私はこれが厳しくて外部サービスを使うのは諦めました。
以下の記事ではmicrocmsCMSへの移行方法をかなり詳しく解説してくれているので、外部サービスを採用する方は是非参考にしてみてください。
WordPress API の選定
ヘッドレスCMSにWordPressを採用する場合、どのようにデータを取得するかを決めなければいけません。
選択肢としては4つあります(本当はもっとあるかも...)。
- WP REST API
- node-wpapi
- GraphQL API for WordPress
- 独自のAPIを作る
WP REST API
一番シンプルな方法です。
fetch
やaxios
で純正のWP REST APIのURLを叩いてデータを取得します。
https://exapmle.com/wp-json/wp/v2/◯◯◯◯◯
参考記事が多く、困ってもググればどうにかなります。私はこの方法を採用しました。
最初はどのパラメータを与えればどんなデータが返ってくるのか全然イメージできていなかったので、APIのテストツールを使って色々試してみました。
APIのテストツールはこちらのChrome拡張機能が一番手軽で使いやすかったです。
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
});
先程のWP REST APIを扱いやすくしてくれます。
しかし、私はドキュメントを読み解く力が劣っていたので、なかなか思い通りにデータを取得することができませんでした...。ドキュメントをしっかりと読める方はこちらのほうが良いのかなと思います(参考記事が純正のWP REST APIよりはかなり少ない印象でした)。
GraphQL API for WordPress
GraphQLというプラグインを使って専用のAPIを作成してデータを取得します。
こちらの方法は開発中にたまたま見つけたので試してはいないのですが、もしかしたら一番いい方法かも...と思いました。
純正のWP REST APIよりレスポンス速度が早かったり、返ってくる値が綺麗だったりするみたいです。管理画面でjsonプレビューもできるっぽいです。もっと早く知りたかった...
独自のAPIを作る
純正のWP REST APIだと最大投稿取得数が100件までなど、様々な制約が存在します。また、WordPressの構造は多種多様です。
要件によっては独自のAPIを作ったほうがいい場合もありそうです。
ホスティングサービスの選定
ホスティングサービスは数多くありますが、今回は代表的なVercelとNetlifyどちらを使うべきかを解説します。
サイトの読み込み速度を比較するとVercelに軍配が上がるみたいなので、2択で迷ったらVercelを選べばいいと思います。
しかし、next/image
を使っている場合は注意が必要です。
Vercelはnext/image
で読み込んだ画像をデプロイ時に最適化してくれるのですが、月1,000件を超えるとプランのアップグレードを促され、それを無視し続けるとアプリが停止されます。
なので、月1,000件を超える場合はNetlifyにするか、next/image
にunoptimized
を与えるなどの対策が必要です。
WordPress製のサイトが画像が多くなりがちなので気をつけましょう。
私は画像データが2,000を超えていたのでNetlifyにしました...。
WordPressのサーバー移管
ヘッドレスCMSにWordPressを採用し、かつリニューアル後のドメインをそのまま使う場合、リニューアル前のWordPressを別ドメインへ移管する必要があります。
ここでは、別ドメインを用意している前提で、WordPressのデータ移管方法を解説していきます。
データ量が「少ない」場合の移管方法
移管するデータ量が少ない場合、プラグインの「All-in-One WP Migration」を使えば一瞬で移管できます。
しかし、データ量が多い場合は結構大変です。
「All-in-One WP Migration」にはインポートデータのサイズ上限があるので、それを超えてしまう場合は手動で移管する必要があります。
データ量が「多い」場合の移管方法
1. バックアップ
まずはバックアップを取ります。超重要です。
私はミスって2回ほどDBを消してしまったので、バックアップを取っていなかったら終わっていました...。
バックアップを取るにはいくつかの手段があるのですが、その全ての手段で取っておきましょう(ミスしてなにかやらかした時に復元する時の選択肢が多いに越したことはないので)。
以下の手段でバックアップを取ります。
- WordPress標準機能のエクスポート
- データベースのエクスポート
- プラグインによるバックアップ
■WordPress標準機能のエクスポート
サイドバー > ツール > エクスポート
から実行できます。
まずは「すべてのコンテンツ」でエクスポートをして、次に「投稿」や「フィールド」などの単体でもエクスポートしておきましょう。
また、エクスポートしたままのファイル名だと分かりにくいので、どれがどのデータなのか分かるようにファイル名を変えておくのも重要です。
■データベースのエクスポート
データベースのエクスポートは、利用しているサーバーのphpMyAdminから実行するのが一番分かりやすいと思います。
1つのデータベースで複数のWordPressを管理している場合、接頭辞でテーブル分けをしていると思います。これが地味に紛らわしいので、間違って違うサイトのDBを操作しないように気をつけましょう。
データベースのエクスポートも、関連テーブル全体と単体でエクスポートしておくのがおすすめです(SQLに慣れている方なら全体のみだけでも大丈夫かなと...)。
■プラグインによるバックアップ
「UpdraftPlus」が優秀過ぎました。間違ってDBのテーブルを消してしまってもワンクリックで復元できました。
バックアップファイルの保存先はGoogleドライブが一番手軽で設定しやすいかなと思います。
2. メディアのダウンロード
画像やファイルのメディアを全てダウンロードします。管理画面上からはできないので、FTPやSSHで一括ダウンロードしましょう。
ダウンロードする際は/wp-content/uploads/
をまるまるダウンロードします。
3. データの移管
移管先でテーマやプラグインなどのセットアップが完了したらデータをインポートしていきます。
- 標準機能でエクスポートした「すべてのコンテンツ」のデータをインポート
- データ量が大きすぎてインポートが進まない場合は、「投稿」や「フィールド」など単体のデータでひとつずつインポートすれば大丈夫だと思います
- メディアをアップロード(ディレクトリ構成は以前と同じにする)
- 「1.」のエクスポートデータだとメディアは反映されないので、手動でアップロードする必要があります
- 私は2,000件くらいのメディアをアップロードするのに1時間ほどかかりました...
- 管理画面からメディアを確認して、正しく表示されていなかったら「4.」を実行
- phpMyAdminなどで
wp_posts
テーブルをインポート- このテーブルには投稿やメディアなどの情報が入っています
- 管理画面からカスタムフィールド系を確認して、正しく表示されていなかったら「6.」を実行
- phpMyAdminなどで
wp_postmeta
テーブルをインポート- このテーブルには投稿に紐づくカスタムフィールドの情報が入っています
- 他にも表示がおかしい箇所があったら、それに関するDBのテーブルをインポートする
本当は1, 2だけで大丈夫だと思っていたのですが、私は正しくメディアが表示されなかったのでDBのテーブルを直接インポートしました。
このあたりの作業は特に慎重に行いましょう。
特にDB操作は一歩間違えたらデータが全部ぶっ飛びます。
もし怖かったらレンタルサーバーのバックアップオプションに作業時だけ加入するのもありだと思います。数百円程度で安心を買えると思えばかなり安いはずです。
ホスティングサービスにカスタムドメインの設定
カスタムドメインの設定方法はVercel レンタルサーバー名 カスタムドメイン
のようにググればたくさん出てくるのでそんなに苦戦しないと思います。
私の場合、DNSを変更して数十分〜半日ほどで設定が反映されました。
ヘッドレスCMS側のSEO対策
ヘッドレスCMS側のWordPressは検索エンジンのクローラにインデックスされないようにします。
管理画面の設定から表示設定の一番下の「検索エンジンでの表示」で「検索エンジンがサイトをインデックスしないようにする」にチェックを入れておきましょう。
こうしないとヘッドレスCMS側のサイトもクロールされてしまい、本体側のサイトが重複コンテンツと判断されSEO評価が下がる恐れがあります。
また、ヘッドレス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,
]);
show_in_rest
を追加してもAPIからデータが取得できない場合は、API自体を無効にしている場合があります。functions.php
やプラグインを見直してみましょう。
カスタムフィールドを使っている場合の注意点
WP REST APIで投稿情報を取得する場合、カスタムフィールドの値はデフォルトだと戻り値に含まれません。
ACFでカスタムフィールドを作成しているのであれば、専用プラグインのACF to REST APIでACFの情報を戻り値に含めることができます。
ACF以外の方法でカスタムフィールドを追加している場合、こちらの記事で紹介されているプラグインを使えば戻り値にカスタムフィールドの情報を含められます。
投稿タイトルを呼び出す際の注意点
HTMLタグを含む投稿本文を出力する場合は、NextのdangerouslySetInnerHTML
を使うと思いますが、投稿タイトルを出力する際もdangerouslySetInnerHTML
を使いましょう。
<h3
className={styles.title}
dangerouslySetInnerHTML={{ __html: title.rendered }}
></h3>
投稿タイトルに半角スペースやハイフンが含まれている場合、–
のような文字参照に変換されているので、それを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_page が30 ならページ数は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]
になります。
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]
になります。
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倍慎重に、バックアップは必ず取る、指差し確認声出し確認などを意識することをおすすめします。
実際にリニューアルしたサイトはこちらです。
周りのデザイナーの方に共有頂けると嬉しいです🙇♂️
Discussion