WordPress / Vue(Nuxt.js) / React(Next.js)の3つの方法でWebサイト構築してみたメモ
修正履歴
- (2021.9.18)開発環境をつくる > React(Next.js)の場合
- (2021.5.5)開発環境をつくる > Vue(Nuxt.js)の場合 > 4.(オプション)Sass(SCSS)が使えるようにする
はじめに
この記事はWordPressをCMSとして使用し、WordPress本体でのWebサイト構築、SSG(静的サイトジェネレータ)と呼ばれるVue(Nuxt.js)、React(Next.js)を使って構築してみて、構築方法や運用周りの違いのメリットやデメリットを書いてみます。
目的
- 開発者視点で、表示速度の早く快適なパフォーマンスを得られるwebサイトが構築できる技術選定ができるように。
- 閲覧者視点で、実際上記が体感ができるようにプロトを用意。
- 運営者視点で、新規ページ追加や、更新など柔軟にできるのかとか。
- 私の知識整理
- 私の知識整理
なぜWordPressか
WebサイトやCMSで世界一使用されているため。
(全世界のWebサイトの40.4%、CMSの64.4%)
参考:https://w3techs.com/technologies/overview/content_management
なぜVue(Nuxt.js)/React(Next.js)か
完全に個人的判断ですが、JavaScript製でVueとReactの静的サイトジェネレータで一番人気が高いのがVueのNuxt.js、ReactのNext.jsなので。
参考:https://jamstack.org/generators/
作成するもの
Webサイトとタイトルには書いてありますが、ブログっぽい構成になります。
構成
- トップページ
- 個別ページ
- カテゴリページ
- タグページ
- 固定ページ
- カスタムポストタイプページ
- ページ内検索結果ページ
完成図
ソースコードはgithubに置いてあります。
なお、プロトタイプ作成に便利なTailwind CSSというCSSライブラリを使用しています。htmlのクラスが多いのはそのためですのでご了承ください。
Lighthouseによる監査結果
最終的に作ったもののLighthouseスコアを先に出しておきます。特にスコアを上げる対策はなし。
-
WordPress
https://basic.kote2.biz/
TOPページ | 個別ページ |
---|---|
-
Vue(Nuxt.js)
https://laughing-saha-766f92.netlify.app/
TOPページ | 個別ページ |
---|---|
-
React(Next.js)
https://basicnext-kote2.vercel.app/
TOPページ | 個別ページ |
---|---|
構築やコストや運用に関する事前知識
WordPressの場合
-
構築
WordPress本体でWebサイトを構築する。
WordPress公式サイト -
言語
PHP -
静的 or 動的?
動的 -
サーバー
レンタルサーバー、VPSなど
私の例だと、さくらVPSの512MBプランで月額643円
さくらVPS
※サーバーはWordPressが使えれば可
-
メリット
- 長年親しまれているツールなのでわからなくても資料が多いので自己解決できる
- 対応できる制作会社も多い
- プラグイン(拡張機能)が豊富。けど、(デメリット参照)
-
デメリット
- 有名すぎてセキュリティの穴を狙われやすい。故に定期的アップデート必要。
- サーバーサイドでhtmlを生成してから渡すので表示が遅い
- プラグインが豊富だが、作者が個人だとアップデートに対応できない(しないでバックレる)ことがある
Vue(Nuxt.js)の場合
-
構築
WordPressをHeadlessCMSとして使用する。最終的に静的なhtmlにビルドする。ビルドしたデータは普通のサーバーにアップするか、netlifyという自動ビルド機能があるサーバーを使う。今回はnetlifyを使用する。 -
言語
JavaScript
※Vue.jsはフレームワークで、Nuxt.jsはVue.jsを更に扱いやすくしたもの
Vue.js
Nuxt.js -
静的 or 動的?
静的 -
サーバー
netlify
ネトリファイ。個人使用は無料。商用は$19〜
※サーバーは静的ファイルが置ければ可。netlifyではgitと連携して
自動ビルド(静的ファイルをビルド)ができる。
-
メリット
- SSGを使えるので表示の早い(爆速)webサイトが作れる。
- htmlが置けるサーバーさえあれば、基本的には大丈夫。
- 社内でWordPressを利用すれば、セキュリティの問題がほぼなくなる。(ただの記事更新ツールとして使うだけ)
-
デメリット
- Vueが使える必要があるので学習コストがかかる。ただし、React(Next.js)よりは易しめ。
- 更新するごとに全ビルドするので本番反映に時間がかかる。
React(Next.js)の場合
-
構築
WordPressをHeadlessCMSとして使用する。最終的に静的なhtmlにビルドする。ビルドしたデータは普通のサーバーにアップするか、Vercelという自動ビルド機能があるサーバーを使う。さらに、Next.jsはVercel社が開発したSSGフレームワークなので親和性が高く、ISR(Incremental Static Regeneration)という機能が使える。なので今回はVercelを使用する。
※Incremental Static Regeneration
通常SSGはサイト全体丸ごとビルドするのであまりページが多いとビルドに時間がかかる。
ISRはアクセスされたページの更新があった場合、そのページだけビルドされるので早い。
ただし、今の所ISRはVercelサーバーでしか利用できない。
-
言語
JavaScript
※React.jsはフレームワークで、Next.jsはReact.jsを更に扱いやすくしたもの
React.js
Next.js -
静的 or 動的?
静的 -
サーバー
Vercel
バーセル。個人使用は無料。商用は$20/per member〜
※サーバーは静的ファイルが置ければ可。vercelではgitと連携して
自動ビルド(静的ファイルをビルド)ができる。さらに部分ビルドも可能。Vue(Nuxt.js)は1ページ修正すると全ページをビルドしなければならない
-
メリット
- Vue(Nuxt.js)の場合とほぼ同じ
- Vue(Nuxt.js)のように更新したら全ビルドする必要なく、部分ビルドできる
- 基本機能で画像を圧縮してくれる機能があり、Vue(Nuxt.js)で作るより更に軽くなる。
-
デメリット
- 学習コストがかかる。JavaScript中級レベル(文法が一通り分かってる)が必要。
- Vue(Nuxt.js)の次に流行りだしたのでまだ資料が少ない
- Vercelを使わないと、Webサイト構築という面ではReact(Next.js)使うメリットがあまりない。(ただ、Vue(Nuxt.js)のように全ビルドはできるので、更新の少ないWebサイトではアリ)
開発環境をつくる
WordPressはVueでもReactでも使用するので少し長くなります。
■WordPressの場合
雑ですが以下解説することを踏まえたstarterファイルを置いておきます。ダウンロードしてnpm installでお使いください。
Node.jsが入ってない方は先にこちらから。
1.ローカル環境に開発環境を作る
Localというアプリが便利です。こちらをご参考
2.必要なプラグインを入れる(最低限です)
-
ブログ系
- Advanced Custom Fields(カスタムフィールド作成)
- Custom Post Type UI(カスタム投稿ポスト作成)
- WP Multibyte Patch(日本語文字化け回避お作法)
- Classic Editor(必要に応じて:昔の画面がいい)
- WordPress インポートツール(必要に応じて:データ移行の場合など)
-
RestAPI系
- WP REST API Menus(WP標準のメニューをRestAPI化)
- ACF to REST API(カスタムフィールドデータをAPI化)
-
セキュリティ・保守(本番環境必須!)
- Google Authenticator(Google二段階認証)
- Login rebuilder(ログインページURL変更)
- BackWPup(記事のバックアップ定期的)
3.テーマファイルを作る
最低限のテーマファイルを作ってテーマ設定しておきます。取り急ぎ必要なのは以下です。
|-- index.php
|-- header.php
|-- footer.php
|-- function.php
|-- style.css
|-- screenshot.png
※gulpを用意する
私はテーマファイル内部にgulpを使ってscssやjsのビルドを行っています。gulpについて初めての方は以下動画がわかりやすいです。udemyにも無料講座があります。(多分同じ内容)
YouTubeのvideoIDが不正です
※gulpついでにtailwindcssをインストール
github上のファイルにあります。
4.記事を用意する
めんどくさければこれ使ってインポートしてください。
■Vue(Nuxt.js)の場合
※前述のWordpressの項は共通
雑ですが以下解説することを踏まえたstarterファイルを置いておきます。ダウンロードしてnpm installでお使いください。
1.Node.jsをインストールする
入ってない方はこちらから。
2.プロジェクトを作成する。
任意のディレクトリで作成します。なお、選択肢が出ますが、tailwindcssだけ入れて、その他はデフォルトで構いません。lint系も必要ありません。
npx create-nuxt-app [プロジェクト名]
npx create-nuxt-app test
create-nuxt-app v2.15.0
✨ Generating Nuxt.js project in test
? Project name test
? Project description My superior Nuxt.js project
? Author name kote2
? Choose programming language JavaScript
? Choose the package manager Npm
? Choose UI framework Tailwind CSS
? Choose custom server framework None (Recommended)
? Choose Nuxt.js modules Axios, Progressive Web App (PWA) Support
? Choose linting tools (Press <space> to select, <a> to toggle all, <i> to inver
t selection)
? Choose test framework None
? Choose rendering mode Universal (SSR)
? Choose development tools (Press <space> to select, <a> to toggle all, <i> to i
nvert selection)
3.tailwindcssの設定をする
4.(オプション)Sass(SCSS)が使えるようにする
いろんなやり方がありますが、私は@nuxtjs/style-resourcesを使っています。
https://www.npmjs.com/package/@nuxtjs/style-resources
加えてnode-sass sass-loaderも入れます。
npm i @nuxtjs/style-resources node-sass sass-loader
npm install --save-dev sass sass-loader@10 fibers
5.環境変数
.envファイルにローカル環境と本番環境のアドレスを記述し、ビルド時は本番、開発時は開発のRestAPIをそれぞれ参照するようにします。
# develop
DEV_URL=http://localhost:5005
DEV_REST_API=http://localhost:10028/wp-json/wp/v2
DEV_MENU_API=http://localhost:10028/wp-json/wp-api-menus/v2/menus/7
# production
PROD_URL=https://basic.kote2.co
PROD_REST_API=https://basic.kote2.co/wp-json/wp/v2
PROD_MENU_API=https://basic.kote2.co/wp-json/wp-api-menus/v2/menus/7
# contents
PER_PAGES=10
nuxt.config.js内でprocess.env.の形で取り出し、publicRuntimeConfigに格納します。
publicRuntimeConfig: {
MAIN_URL:
process.env.NODE_ENV === "production"
? process.env.PROD_URL
: process.env.DEV_URL,
MAIN_REST_API:
process.env.NODE_ENV === "production"
? process.env.PROD_REST_API
: process.env.DEV_REST_API,
MAIN_MENU_API:
process.env.NODE_ENV === "production"
? process.env.PROD_MENU_API
: process.env.DEV_MENU_API,
PER_PAGES: process.env.PER_PAGES
},
各ページではthis.$config.MAIN_URLのように参照できます。
6.基本テンプレートを用意する
用意するのは以下です。
src
|-- components
|-- Header.vue
|-- Footer.vue
|-- Sidebar.vue
|-- layout/default.vue
|-- pages/index.vue
|-- footer.vue
|-- function.vue
■React(Next.js)の場合
1.Node.jsをインストールする
入ってない方はこちらから。
2.プロジェクトを作成する
任意のディレクトリで作成します。
npx create-next-app [プロジェクト名]
3.tailwindcssの設定をする
4.(オプション)Sass(SCSS)が使えるようにする
npm i sass
5.環境変数
.envファイルにローカル環境と本番環境のアドレスを記述し、ビルド時は本番、開発時は開発のRestAPIをそれぞれ参照するようにします。
# develop
DEV_URL=http://localhost:5005
DEV_REST_API=http://localhost:10028/wp-json/wp/v2
DEV_MENU_API=http://localhost:10028/wp-json/wp-api-menus/v2/menus/7
# production
PROD_URL=https://basic.kote2.co
PROD_REST_API=https://basic.kote2.co/wp-json/wp/v2
PROD_MENU_API=https://basic.kote2.co/wp-json/wp-api-menus/v2/menus/7
# contents
PER_PAGES=10
サイドメニュー
■WordPressの場合
1.管理画面でメニューをつくる
管理画面の外観 > メニューで mainmenu というメニューを作ります。
2.functionとsidebarに以下コードを書く
register_nav_menus( array(
'mainmenu' => esc_html__( 'サイドバー', 'basicwp_kote2' ),
));
wp_nav_menu( array(
'theme_location'=> 'mainmenu', // function.phpで設定したメニュー名を表示
'container'=> false,
) );
3.管理画面の外観 > メニューで位置を設定
4.メニューの設定
管理画面からメニューを並べます。
■Vue(Nuxt.js)の場合
Vue(Nuxt.js)では初回アクセス時にnuxtServerInitでメニューデータを取得し、storeという状態管理の仕組み、簡単に言っちゃえばそのサイトのどこにいても取り出せる関数群になります。ユーザー認証がイメージしやすいかも。こちらにメニューデータを格納します。
※なお、メニューデータのAPIはWordPressのプラグインのWP REST API Menusを入れると以下のように出力されます。
※WP REST API Menusの仕様として、カテゴリーページのメニューデータはスラッグ(URLに使われるユニークなアルファベット)が正しく出力できないのでWordPress本体からカテゴリデータを拾ってきてスラッグを取ってくるソースの途中で処理を行ってます。
-
固定ページ
- "object": "page", // ここはディレクトリ名に使用する
- "object_slug": "kotei",// スラッグ正しい
-
CPT(カスタム投稿ポスト)
- "object": "cpt", // ここはディレクトリ名に使用する
- "object_slug": "testpage", // スラッグ正しい
-
カテゴリページ
- "object": "category", // ここはディレクトリ名に使用する
- "object_slug": null,// スラッグおかしい
1.nuxtServerInitの編集
export const state = () => ({
menuData: [],
});
export const mutations = {
setMenuData(state, payload) {
state.menuData = payload;
}
};
export const actions = {
async nuxtServerInit({ commit, state }, { app }) {
// -------------------------------------
// get catData (スラッグ取得用)
// -------------------------------------
let tmpCatData = [];
const resCat = await fetch(
`${this.$config.MAIN_REST_API}/categories?per_page=100`
);
tmpCatData = await resCat.json();
let tmpMenuData = [];
const res = await fetch(this.$config.MAIN_MENU_API);
tmpMenuData = await res.json();
tmpMenuData = tmpMenuData.items;
// ディレクトリ名とスラッグを結合
// ================================= //
let tmpMenuDataEdit = [];
for (const n of tmpMenuData) {
// カテゴリーの場合
if (n.object === "category") {
n.dir = "category";
for (const nn of tmpCatData) {
if (n.object_id === nn.id) {
n.slug = nn.slug;
}
}
}
// 固定ページの場合
else if (n.object === "page") {
n.dir = "page";
n.slug = n.object_slug;
}
// CPTの場合
else {
n.dir = "cpt";
n.slug = n.object_slug;
}
tmpMenuDataEdit.push(n);
}
commit("setMenuData", tmpMenuDataEdit);
}
};
export const getters = {
menuData(state) {
return state.menuData;
}
};
これで以下で取り出せます。
<template>
<ul>
<li v-for="n of menuData" :key="n.id">
{ n.title }}
</li>
</ul>
</template>
(略)
computed: {
menuData() {
return this.$store.getters.menuData;
}
}
2.サイドバーコンポーネントを編集
<template>
<ul>
<li v-for="n of propsMenuData" :key="n.id">
<nuxt-link :to="`/${n.dir}/${n.slug}`">{{ n.title }}</nuxt-link>
</li>
</ul>
</template>
export default {
props: {
propsMenuData: Array
}
};
3.レイアウトファイルを編集
<template>
.
.
.
<Sidebar :propsMenuData="menuData" />
.
.
.
</template>
export default {
computed: {
menuData() {
return this.$store.getters.menuData;
},
}
};
これで以下のように表示されます
■React(Next.js)の場合
React(Next.js)では
- ページごとにメニューデータを取得し、
- lib/api.jsファイルでrestAPIにアクセスし
- メニューを表示するレイアウトファイルにメニューデータを渡します。
なお、メニューデータのAPIはWordPressのプラグインのWP REST API Menusを入れると以下のように出力されます。
※WP REST API Menusの仕様として、カテゴリーページのメニューデータはスラッグ(URLに使われるユニークなアルファベット)が正しく出力できないのでWordPress本体からカテゴリデータを拾ってきてスラッグを取ってくるソースの途中で処理を行ってます。
-
固定ページ
- "object": "page", // ここはディレクトリ名に使用する
- "object_slug": "kotei",// スラッグ正しい
-
CPT(カスタム投稿ポスト)
- "object": "cpt", // ここはディレクトリ名に使用する
- "object_slug": "testpage", // スラッグ正しい
-
カテゴリページ
- "object": "category", // ここはディレクトリ名に使用する
- "object_slug": null,// スラッグおかしい
1.各ページファイルを編集
import { getMenuData} from 'lib/api';
.
.
.
export async function getStaticProps() {
let menusData = {};
menusData = await getMenuData(); // 2.でgetMenuData()をapiファイルに記述
return {
props: { props }
};
}
2.apiファイルを編集
メニュデータを参照するgetMenuData()を定義します。
import fetch from 'node-fetch';
// ==================================================
// getCatData (スラッグ取得用)
// ==================================================
export async function getCatData(slug = '') {
const res = await fetch(`${process.env.MAIN_REST_API}/categories?_embed&slug=${slug}`);
const tmp = await res.json();
return tmp;
}
// ==================================================
// getMenuData
// ==================================================
export async function getMenuData() {
const res = await fetch(process.env.MAIN_MENU_API);
const res2 = await res.json();
const tmpMenuData = res2.items;
const tmpCatData = await getCatData(); // menuにはslugがないのでカテゴリデータのslugを結合
// --------------------------------------------------
// ディレクトリ名とスラッグを結合
// --------------------------------------------------
let tmpMenuDataEdit = [];
for (const n of tmpMenuData) {
// カテゴリーの場合
if (n.object === 'category') {
n.dir = 'category';
for (const nn of tmpCatData) {
if (n.object_id === nn.id) {
n.slug = nn.slug;
}
}
}
// 固定ページの場合
else if (n.object === 'page') {
n.dir = 'page';
n.slug = n.object_slug;
}
// CPTの場合
else {
n.dir = 'cpt';
n.slug = n.object_slug;
}
tmpMenuDataEdit.push(n);
}
return tmpMenuDataEdit;
}
3.レイアウトファイルを編集
menuDataを追加し、最終的にSidebarに渡します。
export default function LayoutDefault({ children, title = '', menuData }) {
.
.
.
return (
<Sidebar menuData={menuData} />
)
}
4.サイドバーコンポーネントを編集
export default function Sidebar({ menuData = [] }) {
return (
<ul>
{menuData.length !== 0 &&
menuData.map((n) => (
<li key={n.id}>
<Link href={`/${n.dir}/${n.slug}`}>
<a className='underline'>{n.title}</a>
</Link>
</li>
))}
</ul>
)
}
これで以下のように表示されます
全記事の一覧表示(トップページ)
記事データの取得は、以下の方法で行います。
WordPressの場合
予め決められてるWordPressの関数郡(WP Query)から取り出す
Vue(Nuxt.js) と React(Next.js)の場合
WordPRessが提供するREST APIというjsonファイルからfetchして取り出す
REST API(RESTful API)とは
根本的な概念は他記事に任せます。
WordPressに組み込まれたREST APIは以下の項目を埋めることで条件となり、jsonを返します。
- posts=>投稿タイプ。pageやカスタム投稿タイプなどがある
- _embed=>アイキャッチ画像やコメントなど、付随する情報も取ってくる
- per_page=>取ってくる数
- page=>per_pageと合わせて2なら10記事目から20記事目まで取ってくる
- categories=>取ってくるカテゴリのid 配列で[1, 3, 5]など指定。URLの場合は1+3+5
- categories_exclude=>取ってこないカテゴリのid
- tags=>取ってくるタグのid 配列で[1, 3, 5]など指定。URLの場合は1+3+
- slug=>スラッグ指定で1記事
- search=>検索ワードを指定して取ってくる
条件に何も入れなかった場合(pre_pageとpageは必須)
条件をつける(slugがpost-08を取ってくる)
※slug・・・特定のページのURL内のディレクトリ名。
■WordPressの場合
WordPressでは template-parts/list-content に繰り返し表示のテンプレートを作成し、index.php内で呼び出します。indexページではカテゴリページ、タグページなどを判別し、それにあった記事を取得して、それを繰り返し表示します。
以下ファイルを作成します。各ファイルの役割を確認します。
-
index.php
- ページ本体
-
template-parts/list-content.php
- 一覧のループ部分のテンプレート(WP Query)
<ul class="list-none">
<?php
// ==================================================
// サブループ(WP_Query)
// ==================================================
$args = array(
// -- 記事のタイプ --------------------
'post_type' => 'post',
// -- オプション --------------------
'category__not_in' => [1], // カテゴリ「未定義」は除く
'posts_per_page' => -1, // -1は全て
'no_found_rows' => true, // ページングは使用しない
);
$the_query = new WP_Query($args);
if ($the_query->have_posts()) {
while ($the_query->have_posts()) {
$the_query->the_post();
get_template_part( 'template-parts/list-content', get_post_format());
}
}
wp_reset_postdata();
?>
</ul>
<?php
// ===========> サムネイル
if(has_post_thumbnail()) {
// $thumb = the_post_thumbnail('full');
$image_id = get_post_thumbnail_id ();
$image_url = wp_get_attachment_image_src ($image_id, true);
$thumb = $image_url[0]; // アイキャッチurlだけ取得 https://on-ze.com/archives/5621
} else {
$thumb = 'https://basic.kote2.co/wp-content/uploads/2021/02/screenshot.png'; //アイキャッチを設定してなかった場合
}
?>
<li class="p-4 relative z-10 hover:bg-gray-300 cursor-pointer">
<a class="card-link" href="<?php the_permalink(); ?>">
<h4 class="c-tail mb-4"><?php the_title(); ?></h4>
<div class="flex">
<figure class="inline-block" style="width: 300px">
<img class="w-full" src="<?php echo $thumb; ?>" alt="">
</figure>
<div class="w-full px-6">
<div><?php echo mb_substr( get_the_excerpt(), 0, 80 ) . '...'; ?></div>
<div class="pt-4"><span class="font-bold">カテゴリ: </span>
<?php
// ===========> カテゴリ
$categories = get_the_category();
if(isset($categories[0])) {
?>
<span><a class="relative underline" href="/category/<?php echo $categories[0]->slug?>"><?php echo $categories[0]->cat_name?></a></span>
<?php } ?>
</div>
<div class="pt-2"><span class="font-bold">タグ: </span>
<?php
// ===========> タグ
$tags = get_the_tags();
if(isset($tags[0])) {
foreach ( $tags as $tag ) {
?>
<span><a class="relative underline" href="/tag/<?php echo $tag -> slug; ?>"><?php echo $tag -> name; ?></a></span>
<?php
}
} ?>
</div>
</div>
</div>
</a>
</li>
以上でこのように表示されます。
■Vue(Nuxt.js)の場合
以下ファイルを作成します。各ファイルの役割を確認します。
-
index.vue
- ページ本体
-
components/PostList.vue
- 一覧のループ部分
-
store/index.vue
- REST APIから記事データを取ってきて渡す
1.store/indexを編集し、getAllPostsを追加
すべての記事を取ってくる関数getAllPosts()を追加します
export const state = () => ({
allPostsData: []
});
export const mutations = {
setAllPostsData(state, payload) {
console.log(payload);
state.allPostsData = payload;
}
};
export const actions = {
async getAllPosts({ commit }, query) {
let i = 1;
const newQuery = `
${this.$config.MAIN_REST_API}/${
query.type ? query.type : "posts"
}?_embed&per_page=${
query.per_page ? query.per_page : this.$config.PER_PAGES
}&page=${query.page ? query.page : i}&categories=${
query.categories ? query.categories : []
}&categories_exclude=1&tags=${query.tags ? query.tags : []}&slug=${
query.slug ? query.slug : ""
}&search=${query.search ? query.search : ""}
`;
const res = await fetch(newQuery);
const tmpAllPosts = await res.json();
let tmpAllPosts2 = [];
for (const n of tmpAllPosts) {
n.thumb = n._embedded["wp:featuredmedia"][0].source_url;
tmpAllPosts2.push(n);
}
commit("setAllPostsData", tmpAllPosts2);
},
}
export const getters = {
allPostsData(state) {
return state.allPostsData;
}
};
2.indexを編集し、PostList(次作る)に渡します
<template>
<div>
<PostList :propsPosts="allPostsData" />
</div>
</template>
<script>
import PostList from "~/components/PostList";
export default {
components: {
PostList
},
async asyncData({ store }) {
const query = {
type: "posts",
info: "Index"
};
await store.dispatch("getAllPosts", query);
},
computed: {
allPostsData() {
return this.$store.getters.allPostsData;
}
}
};
</script>
3.PostListを作る
<template>
<div class="PostList">
<ul>
<!-- start loop -->
<li
v-for="n of propsPosts"
:key="n.id"
class="p-4 relative z-10 hover:bg-gray-300 cursor-pointer"
>
<a class="card-link" :href="`/post/${n.slug}`">
<h4 class="c-tail mb-4">{{ n.title.rendered }}</h4>
<div class="flex">
<figure class="inline-block" style="width: 300px">
<img class="w-full" :src="n.thumb" alt="" />
</figure>
<div class="w-full px-6">
<div v-html="setWordCount(n.excerpt.rendered)"></div>
<div class="pt-4">
<span class="font-bold">カテゴリ: </span>
<span class="inline-block px-1">
<!-- カテゴリを表示[1件のみ] -->
<nuxt-link
class="relative underline"
:to="`/category/${n.categories[0].slug}`"
>{{ n.categories[0].name }}</nuxt-link
>
</span>
</div>
<div class="pt-2">
<span class="font-bold">タグ: </span>
<!-- タグを表示[複数対応] -->
<span
v-for="nn of n.tags"
:key="nn.id"
class="inline-block px-1"
>
<nuxt-link
class="relative underline"
:to="`/tag/${nn.slug}`"
>{{ nn.name }}</nuxt-link
>
</span>
</div>
</div>
</div>
</a>
</li>
<!-- end loop -->
</ul>
</div>
</template>
<script>
export default {
name: "PostList",
props: {
propsPosts: Array
},
methods: {
// 80文字に減らす
setWordCount(str, l = 80) {
if (str.length > l) {
str = str.substring(0, l - 1) + "…";
return str;
}
}
}
};
</script>
以上でこのように表示されます。
■React(Next.js)の場合
以下ファイルを作成します。各ファイルの役割を確認します。
-
index.js
- ページ本体
-
components/common/PostList.js
- 一覧のループ部分
-
lib/api.js
- REST APIから記事データを取ってきて渡す
1.apiを編集し、getAllPostsを追加
// ==================================================
// getAllPosts
// ==================================================
export async function getAllPosts(query) {
let tmpPosts = [];
let i = 1;
const newQuery = `
${process.env.MAIN_REST_API}/${query.type ? query.type : 'posts'}?_embed&per_page=${
query.per_page ? query.per_page : process.env.PER_PAGES
}&page=${query.page ? query.page : i}&categories=${
query.categories ? query.categories : []
}&categories_exclude=1&tags=${query.tags ? query.tags : []}&search=${
query.search ? query.search : ''
}
`;
const res = await fetch(newQuery);
tmpPosts = await res.json();
return tmpPosts;
}
2.indexを編集し、PostList(次作る)に渡します
import LayoutDefault from 'components/layout/LayoutDefault';
import { getAllPosts } from 'lib/api';
import PostList from 'components/common/PostList';
export default function Index({ props }) {
return (
<LayoutDefault>
<main className='main w-full'>
<div className='inner px-8'>
<h2 className='c-tail mb-8'>すべての記事</h2>
<PostList propsPosts={props.allPostsData} />
</div>
</main>
</LayoutDefault>
);
}
export async function getStaticProps() {
let props = {};
props.menusData = await getMenuData();
const query = {
type: 'posts',
info: 'Index',
};
props.allPostsData = await getAllPosts(query);
return {
props: { props },
revalidate: 5,
};
}
3.PostListを作る
import Link from 'next/link';
import Image from 'next/image';
import uniqid from 'uniqid';
export default function PostList({ propsPosts }) {
// 80文字にして返す
function setWordCount(str, l = 80) {
if (str.length > l) {
str = str.substring(0, l - 1) + '…';
return str;
}
}
if (!propsPosts) {
// 非同期処理の記事データがまだ空の状態はローディング中表示
return <div>Loading...</div>;
}
return (
<div className='PostList'>
<ul>
{propsPosts.map((n) => (
<li key={uniqid()} className='p-4 relative z-10 hover:bg-gray-300 cursor-pointer"'>
<a className='card-link' href={`/post/${n.slug}`}>
<h4 className='c-tail mb-4'>{n.title.rendered}</h4>
<div className='flex'>
<figure className='inline-block'>
<Image
src={n.featured_image.src}
width={300}
height={200}
alt=''
className='object-cover'
/>
</figure>
<div className='w-full px-6'>
<div dangerouslySetInnerHTML={{ __html: setWordCount(n.excerpt.rendered) }}></div>
<div className='pt-4'>
<span className='font-bold'>カテゴリ: </span>
<span className='inline-block px-1'>
<Link href={`/category/${n.categories[0].slug}`}>
<a className='relative underline'>{n.categories[0].name}</a>
</Link>
</span>
</div>
<div className='pt-2'>
<span className='font-bold'>タグ: </span>
{n.tags.map((nn) => (
<span key={uniqid()} className='inline-block px-1'>
<Link href={`/tag/${nn.slug}`}>
<a className='relative underline'>{nn.name}</a>
</Link>
</span>
))}
</div>
</div>
</div>
</a>
</li>
))}
</ul>
</div>
);
}
以上でこのように表示されます。
カテゴリ/タグ/検索結果の一覧ページ
■概要
全記事の一覧表示ができたら今度は全記事ではなく、あるカテゴリや、あるタグ、または検索用語を入力し、それか含まれる記事を一覧に出すページを作成します。
WordPressの場合
WordPressの場合は、is_category()やis_tag()などを利用して、カテゴリ一覧なのかタグ一覧なのか判別できます。またURL一部のslugを利用して、カテゴリの何なのかを指定して記事を取得できます。このようにすると、先ほど作成したindex.phpを使い回すことができます。
Vue(Nuxt.js) と React(Next.js)の場合
REST APIでcategoryies=カテゴリid(Array形式)のように渡してあげれば取得できます。
※どちらも仕様上、WordPressのように、index.phpを使い回す事はできません。
カテゴリはcategory/***.vueのように各ディレクトリにファイルを作ります。
■WordPressの場合
以下ファイルを作成します。各ファイルの役割を確認します。
-
index.php
- ページ本体
// -- カテゴリページチェック -------------- //
$cat_id =null;
if( is_category() ){
$cat = get_the_category();
$cat_id = $cat[0]->term_id; // $cat_idにカテゴリidを入れる
}
// -- タグページチェック -------------- //
$tag_id = null;
if( is_tag() ){
$tag = get_the_tags();
$tag_id = $tag[0]->term_id; // $tag_idにタグidを入れる
}
// -- 検索ページチェック -------------- //
$search_word = '';
if( get_search_query() ){
$search_word = get_search_query(); // $search_word に検索ワードを渡す
}
// ==================================================
// WP_Query
// ==================================================
$args = array(
// -- 記事のタイプ --------------------
'post_type' => 'post',
// -- オプション --------------------
'cat' => $cat_id,
'category__not_in' => 1, // acfのカテゴリと未定義は除く
'tag_id' => $tag_id,
's' => $search_word,
'posts_per_page' => -1, // -1は全て
'no_found_rows' => true, // ページングは使用しない
);
$the_query = new WP_Query($args);
if ($the_query->have_posts()) {
while ($the_query->have_posts()) {
$the_query->the_post();
get_template_part( 'template-parts/list-content', get_post_format());
}
}
wp_reset_postdata();
これで以下赤枠をクリックすると、該当するカテゴリページまたはタグページが表示されます。
■Vue(Nuxt.js)の場合
以下ファイルを作成します。各ファイルの役割を確認します。
-
pages/category/_category.vue
- カテゴリページ
-
pages/tag/_tag.vue
- タグページ
-
pages/search/_searchWord.vue
- 検索結果ページ
_(アンダースコア)で始まるページは動的なページ
/category/_categoryの場合Nuxt.jsの仕様として、
/category/abc
/category/cba
のように、category以下が変わっても、元は1枚のページで動きます。
もしabcだったらabcの内容を表示、cbaならcbaの内容を表示させるようにできます。
params.categoryでabcやcbaの値が取得できます。
<template>
<div>
<h2>カテゴリ: {{ catName }}</h2>
<PostList :propsPosts="allPostsData" />
</div>
</template>
<script>
import PostList from "~/components/PostList";
export default {
name: "Category",
components: {
PostList
},
async asyncData({ store, params, state }) {
let catID = [];
let catName = "";
// 登録されてるカテゴリとslugの照合で存在するカテゴリならidを取得
for (const n of store.state.catData) {
if (n.slug === params.category) {
catID.push(n.id);
catName = n.name;
}
}
// idをstoreに渡す
const query = {
categories: catID,
};
await store.dispatch("getAllPosts", query); // storeに記事を要求
return { catName: catName }; // カテゴリ名は返す
}
};
</script>
タグページをと検索結果ページも同様です。省略します。
async asyncData({ store, params}) {
const query = {
tags: tagID,
};
await store.dispatch("getAllPosts", query); // storeに記事を要求
}
async asyncData({ store, params}) {
const keyword = encodeURI(params.searchWord);
const query = {
search: keyword,
};
await store.dispatch("getAllPosts", query); // storeに記事を要求
}
これで以下赤枠をクリックすると、該当するカテゴリページまたはタグページが表示されます。
■React(Next.js)の場合
以下ファイルを作成します。各ファイルの役割を確認します。
-
lib/api.js
- REST APIからカテゴリやタグデータを取ってきて渡す
-
pages/category/[category].js
- カテゴリページ
-
pages/tag/[tag].js
- タグページ
-
pages/search/[search_word].js
- 検索結果ページ
[]で囲まれたページは動的なページ
/category/[category].jsの場合Next.jsの仕様として、
/category/abc
/category/cba
のように、category以下が変わっても、元は1枚のページで動きます。
もしabcだったらabcの内容を表示、cbaならcbaの内容を表示させるようにできます。
params.categoryでabcやcbaの値が取得できます。
※なお、Next.jsでは動的なページをダイナミックルーティングと呼びます。
getStaticPaths()
- export async function getStaticPaths()
ダイナミックルーティングには必ず必要。カテゴリならカテゴリのスラッグ、タグならタグのスラッグを全てここで渡します。これは後々ビルドするときに1枚1枚静的なページを生成するためです。
では最初にlib/api.jsを作成します。以下内容を記述します。
- getAllCatSlugs()
- カテゴリの全てのスラッグを渡す
- getCatData(slug)
- slugのカテゴリのデータを渡す
- getAllTagSlugs
- タグの全てのスラッグを渡す
- getTagData(slug)
- slugのタグのデータを渡す
// ==================================================
// getAllCatSlugs
// ==================================================
export async function getAllCatSlugs() {
const res = await fetch(`${process.env.MAIN_REST_API}/categories?_embed&per_page=100`);
const tmp = await res.json();
// console.log(tmp);
let tmpCatSlugs = [];
for (let n of tmp) {
tmpCatSlugs.push(n.slug);
}
// console.log(tmpCatSlugs);
return tmpCatSlugs.map((slug) => {
// console.log(slug);
return {
params: {
category: String(slug),
},
};
});
}
// ==================================================
// getCatData
// ==================================================
export async function getCatData(slug = '') {
const res = await fetch(`${process.env.MAIN_REST_API}/categories?_embed&slug=${slug}`);
const tmp = await res.json();
// console.log(tmp);
// const tmpCatData = tmp[0];
// console.log(tmpCatData.name);
return tmp;
}
// ==================================================
// getAllTagSlugs
// ==================================================
export async function getAllTagSlugs() {
const res = await fetch(`${process.env.MAIN_REST_API}/tags?_embed&per_page=100`);
const tmp = await res.json();
let tags = [];
for (let n of tmp) {
tags.push(n.slug);
}
return tags.map((tag) => {
return {
params: {
tag: String(tag),
},
};
});
}
// ==================================================
// getTagData
// ==================================================
export async function getTagData(slug = '') {
// console.log('tag');
// console.log(slug);
const res = await fetch(`${process.env.MAIN_REST_API}/tags?_embed&slug=${slug}&per_page=100`);
const tmp = await res.json();
return tmp;
}
カテゴリページです。
import LayoutDefault from 'components/layout/LayoutDefault';
import { getAllPosts, getCatData, getAllCatSlugs } from 'lib/api';
import PostList from 'components/common/PostList';
export default function Category({ props }) {
return (
<LayoutDefault>
<PostList propsPosts={props.allPostsData} />
</LayoutDefault>
);
}
export async function getStaticPaths() {
const paths = await getAllCatSlugs(); // カテゴリをスラッグをすべて取得してpathsに入れる
return {
paths,
fallback: true,
};
}
export async function getStaticProps({ params }) {
const tmpCatSlug = await params.category;
const tmpCatData = await getCatData(tmpCatSlug);
// 404
if (!tmpCatData.length) {
console.log('404');
return {
notFound: true,
};
}
const query = {
categories: tmpCatData[0].id,
};
const allPostsData = await getAllPosts(query);
return {
props: { allPostsData },
};
}
タグページと検索結果ページは省略します。
検索フォーム
検索フォームはユーザーが入力した文字列をそのまま検索ページのURLに貼り付けて遷移させる簡単なものです。
検索キーワード付きのURL
// WordPress
https://testdomain.com/s=[search word]
// Vue(Nuxt.js) or React(Next.js)
https://testdomain.com/search/[search word]
■WordPressの場合
<form method="get" action="<?php echo home_url('/'); ?>" class="pt-3">
<fieldset class="submenu-search-fieldset px-3 pb-8">
<label for="search" class="hidden">search</label>
<div class="relative">
<input
type="text"
name="s"
class="appearance-none rounded-full w-full py-2 pl-4 pr-10 text-gray-700 leading-tight focus:outline-none focus:bg-white focus:border-gray-500"
placeholder="search word"
/>
<button type="submit" class="inline-block w-4 absolute">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clip-rule="evenodd" />
</svg>
</button>
</div>
</fieldset>
</form>
これで以下のように表示されます。
■Vue(Nuxt.js)の場合
Vue(Nuxt.js)はv-modelというデータを入れる箱を利用します。入力フォームに記入されている文字列はすでにv-modelに入っているので、検索ボタンを押したらv-modelの内容付きで検索ページに飛ばすだけです。
なお、フォームはvee-validateというバリデーションチェックのライブラリを使用してますが、特にこのライブラリの解説は省きますのご了承ください。
npm i vee-validate
※サンプルはv3.x系
<template>
<div class="pt-3">
<fieldset class="submenu-search-fieldset px-3 pb-8">
<label for="search" class="hidden">search</label>
<div class="relative">
<ValidationProvider v-slot="{ errors }">
<input
type="text"
v-model="keyword"
class="appearance-none rounded-full w-full py-2 pl-4 pr-10 text-gray-700 leading-tight focus:outline-none focus:bg-white focus:border-gray-500"
placeholder="search word"
/>
<span>{{ errors[0] }}</span>
</ValidationProvider>
<button @click="onSearch" class="inline-block w-4 absolute">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
clip-rule="evenodd"
/>
</svg>
</button>
</div>
</fieldset>
</div>
</template>
<script>
import { ValidationProvider } from "vee-validate";
export default {
data() {
return {
keyword: "" // v-model用の箱
};
},
components: {
ValidationProvider
},
methods: {
onSearch() {
this.$router.push(`/search/${this.keyword}`); // $router.pushで飛ばす
}
}
};
</script>
■React(Next.js)の場合
React(Next.js)はReact Hook Formというバリデーションチェックのライブラリを使用してますが、特にこのライブラリの解説は省きますのご了承ください。標準で用意されてるReactのFormより高機能で軽いライブラリです。
npm i react-hook-form
import { useForm } from 'react-hook-form';
export default function Sidebar({ menuData = [] }) {
// form
const { register, handleSubmit } = useForm();
const onSubmit = (data) => {
location.href = `/search/${data.search}`;
};
return (
<div className='search'>
<h5 className='c-tail'>検索</h5>
<div className='pt-3'>
<fieldset className='submenu-search-fieldset px-3 pb-8'>
<label htmlFor='search' className='hidden'>
search
</label>
<div className='relative'>
<input
ref={register()}
id='search'
name='search'
type='text'
className='appearance-none rounded-full w-full py-2 pl-4 pr-10 text-gray-700 leading-tight focus:outline-none focus:bg-white focus:border-gray-500'
placeholder='search word'
/>
<button onClick={handleSubmit(onSubmit)} className='inline-block w-4 absolute'>
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='currentColor'>
<path
fillRule='evenodd'
d='M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z'
clipRule='evenodd'
/>
</svg>
</button>
</div>
</fieldset>
</div>
</div>
);
}
以上になります。
個別ページ、固定ページとカスタムポストタイプ
最後に固定ページとカスタムポストタイプです。
■WordPressの場合
以下ファイルを作成します。各ファイルの役割を確認します。
-
single.php
- 個別ページとカスタムポストタイプのテンプレート
-
page.php
- 固定ページのテンプレート
-
template-parts/post-content.php
- コンテンツ内容を別に分けたテンプレート
<div>
<?php
while (have_posts()) {
the_post(); // コンテンツがあれば以下テンプレートを使って表示
get_template_part( 'template-parts/post-content', get_post_type() );
}
?>
</div>
template-parts/post-content.phpは省略します。
完成データを参考にしてください。
■Vue(Nuxt.js)の場合
以下ファイルを作成します。各ファイルの役割を確認します。
-
store/index.vue
- 個別記事データを取得するgetPost()を追加。
-
pages/post/_slug.vue
- 個別ページのテンプレート
-
pages/page/_slug.vue
- 固定ページのテンプレート
-
pages/cpt/_slug.vue
- カスタムポストタイプのテンプレート
export const state = () => ({
postData: {}
});
export const mutations = {
setPostData(state, payload) {
state.postData = payload;
}
};
export const actions = {
// ==================================================
// getPost
// ==================================================
async getPost({ commit }, query) {
const res = await fetch(
`${this.$config.MAIN_REST_API}/${
query.type ? query.type : "posts"
}?_embed&slug=${query.slug}&_embed`
);
const tmp = await res.json();
const tmpPosts = tmp[0];
tmpPosts.thumb = tmpPosts._embedded["wp:featuredmedia"][0].source_url;
console.log(tmpPosts);
commit("setPostData", tmpPosts);
}
};
export const getters = {
postData(state) {
return state.postData;
}
};
<template>
<main class="main w-full">
<div class="inner px-8">
<!-- サンプルでタイトルだけ表示 -->
<h2 class="c-tail">{{ postData.title.rendered }}</h2>
</div>
</main>
</template>
<script>
export default {
async asyncData({ params, store }) {
const query = {
slug: params.slug
};
await store.dispatch("getPost", query);
},
computed: {
postData() {
return this.$store.getters.postData;
}
}
};
</script>
pages/page/_slug.vueとpages/cpt/_slug.vueは省略します。
完成データを参考にしてください。
■React(Next.js)の場合
以下ファイルを作成します。各ファイルの役割を確認します。
-
lib/api.js
- 個別記事データを取得するgetPost()を追加。
-
pages/post/[slug].js
- 個別ページのテンプレート
-
pages/page/[slug].js
- 固定ページのテンプレート
-
pages/cpt/[slug].js
- カスタムポストタイプのテンプレート
// ==================================================
// getPost
// ==================================================
export async function getPost(query) {
const res = await fetch(
`${process.env.MAIN_REST_API}/${query.type ? query.type : 'posts'}?_embed&slug=${query.slug}`
);
const tmp = await res.json();
const tmpPost = tmp[0];
tmpPost.thumb = tmpPost._embedded['wp:featuredmedia'][0].source_url;
return tmpPost;
}
import LayoutDefault from 'components/layout/LayoutDefault';
import {getAllPostSlugs, getPost } from 'lib/api';
export default function Post({ props }) {
if (!props) {
return <div>Loading...</div>;
}
return (
<LayoutDefault>
<main className='main w-full'>
<div className='inner px-8'>
{/* サンプルでタイトルだけ表示 */}
<h2 className='c-tail'>{props.postData.title.rendered}</h2>
</div>
</main>
</LayoutDefault>
);
}
export async function getStaticPaths() {
const paths = await getAllPostSlugs();
return {
paths,
fallback: true,
};
}
export async function getStaticProps({ params }) {
let query = {};
query.slug = await params.slug;
query.type = 'posts';
const postData = await getPost(query);
return {
props: {
postData,
},
revalidate: 5,
};
}
pages/page/[slug].jsとpages/cpt/[slug].jsは省略します。
完成データを参考にしてください。
Discussion
AWS Amplify も Next.js の ISR に対応されましたのでコメント落としときます
ありがとうございます!内容修正させていただきました。