Faust.js+WPGraphQLで、カスタムタクソノミーを扱う
WPGraphQLプラグインとFaust.jsを組み合わせれば、クエリを書く手間を省きながら、WordPressをHeadless CMS化できます。しかしカスタムタクソノミーを使おうとすると少し手間がかかるので、手順をまとめました。
WordPress側
WPGraphQLの準備
Composer使うので環境用意して下さい。sage/bedrockがおすすめです。
composer require wp-graphql/wp-graphql
wp plugin activate wp-graphql
タクソノミーを作成し、GraphQLに露出
カスタムタクソノミー「ゲーム」を作るとします。
function custom_register_taxes_games() {
$labels = [
'name' => __( 'ゲーム' ),
'singular_name' => __( 'ゲームズ' ),
];
$args = [
'label' => __( 'ゲーム' ),
'labels' => $labels,
'public' => true,
'publicly_queryable' => true,
'hierarchical' => false,
'show_ui' => true,
'show_in_menu' => true,
'show_in_nav_menus' => true,
'query_var' => true,
'rewrite' => [ 'slug' => 'games', 'with_front' => true, ],
'show_admin_column' => true,
'show_in_rest' => true,
'show_tagcloud' => false,
'rest_base' => 'games',
'rest_controller_class' => 'WP_REST_Terms_Controller',
'show_in_quick_edit' => true,
'sort' => false,
'show_in_graphql' => true,
'graphql_single_name' => 'Game',
'graphql_plural_name' => 'Games',
];
register_taxonomy( 'games', [ 'post' ], $args );
}
add_action( 'init', 'custom_register_taxes_games' );
まあこれはCPTUIで追加してもいいのですが、いずれにせよ設定はコードに書いて下さい。(WordPress詳しくないからCPTUIで作ってコード生成させた。無駄なオプションが多いかも)
'show_in_graphql' => true,
'graphql_single_name' => 'Game',
'graphql_plural_name' => 'Games',
重要なのはここで、これをGraphQL側で使います。
query game {
game(id: "minecraft", idType: SLUG) {
id
name
}
}
例えばminecraft
というスラッグのゲームはこうやって取得できます。
タクソノミープラグインのインストールと有効化
2022年4月30日現在のFaust.jsのusePosts
では、where
にカスタムタクソノミーが使えません。
そこでこちらのプラグインを使います。2022年4月30日現在、WordPress Packagistに登録されてないので、リポジトリをcomposerに追加してください。
composer config repositories.wp-graphql/wp-graphql-tax-query \
vcs https://github.com/wp-graphql/wp-graphql-tax-query.git
composer require wp-graphql/wp-graphql-tax-query:master
wp plugin activate wp-graphql-tax-query
参考: https://scrapbox.io/namaozi/composerでGitHubのリポジトリからパッケージ追加する方法
Next.js側
ここから環境を用意すれば、最速でWPGraphQLの環境を構築できます。
Schemaの生成
npx gqty generate
sage/bedrockを使っているなどの理由で/wp
がURLに含まれる場合、適宜src/faust.config.js
を編集する必要があります。
記事一覧ページの用意
Faust.jsはApollo ClientではなくGQtyで通信しています。
日本語情報が全然ないのですが、このライブラリでは単にuseQuery
しただけではSymbol(gqty-proxy)
が返されます。これによりクライアント側では一旦スケルトンを描画し、データ到着後に表示の更新が行われます。
サーバーサイドでデータを扱う場合はややこしくなります。client.useQuery().game(...)
で取得すると、gqty-proxyが返ってくるせいで404の判定やビルド時のパス生成ができません。
そこで、GQtyClient
のinlineResolved
という関数を使えばいいそうです。
mkdir src/utils
touch src/utils/game.ts
import { client, GameIdType } from '@/client';
import { getFields, getArrayFields } from 'gqty';
/**
* ゲームが存在するか否か
* @param slug スラッグ
*/
const checkGameExistsBySlug = async (slug: string) => {
const { inlineResolved, query } = client.client;
const game = await inlineResolved(() => {
return getFields(
query.game({
id: slug,
idType: GameIdType.SLUG,
}),
'id'
);
});
return game !== null;
};
/**
* getStaticPaths用のpathsを返す
* @param paramName ルートパラメータの名前
*/
const getAllGameSlugParams = async (paramName: string) => {
const games = await client.client.inlineResolved(() => {
return getArrayFields(client.client.query.games()?.nodes, 'slug');
});
return games
? games.map((game) => {
return {
params: {
[paramName]: game.slug,
},
};
})
: [];
};
export { checkGameExistsBySlug, getAllGameSlugParams };
これらのユーティリティを、カスタムタクソノミーがルーティングに絡んでくる場所で使います。
inlineResolved
のコールバック関数でgetFields
またはgetArrayFields
を使うことで、データの実体を取得します。これらを用意することでやっと、適切な404判定やパスの生成ができます。
DIR=src/pages/games/[gameSlug]
mkdir -p $DIR && touch $DIR/index.tsx
import { getNextStaticProps } from '@faustjs/next';
import { GetStaticPropsContext } from 'next';
import { useRouter } from 'next/router';
import {
client,
GameIdType,
RelationEnum,
RootQueryToPostConnectionWhereArgsTaxQueryField,
RootQueryToPostConnectionWhereArgsTaxQueryOperator,
TaxonomyEnum,
} from 'client';
import { checkGameExistsBySlug, getAllGameSlugParams } from '@/utils/game';
const POSTS_PER_PAGE = 6;
export default function Page() {
const { useQuery, usePosts } = client;
const { query = {} } = useRouter();
const { gameSlug, paginationTerm, gameCursor } = query;
const game = useQuery().game({ id: gameSlug.toString(), idType: GameIdType.SLUG });
const isBefore = paginationTerm === 'before';
const posts = usePosts({
after: !isBefore ? (gameCursor as string) : undefined,
before: isBefore ? (gameCursor as string) : undefined,
first: !isBefore ? POSTS_PER_PAGE : undefined,
last: isBefore ? POSTS_PER_PAGE : undefined,
/**
* wp-graphql-tax-queryプラグインで絞り込む
* @see https://github.com/wp-graphql/wp-graphql-tax-query
*/
where: {
taxQuery: {
relation: RelationEnum.AND,
taxArray: [
{
terms: [game?.slug],
taxonomy: TaxonomyEnum.GAME,
operator: RootQueryToPostConnectionWhereArgsTaxQueryOperator.IN,
field: RootQueryToPostConnectionWhereArgsTaxQueryField.SLUG,
},
],
},
},
});
return (
// 省略
);
}
export async function getStaticProps(context: GetStaticPropsContext) {
const notFound = !(await checkGameExistsBySlug(context.params.gameSlug.toString()));
return getNextStaticProps(context, {
Page,
client,
notFound,
});
}
export async function getStaticPaths() {
const paths = await getAllGameSlugParams('gameSlug');
return {
paths,
fallback: 'blocking',
};
}
なお、クライアント側では普通にusePosts
を使い、taxQuery
で絞り込みます。これは2022年4月30日現在、wp-graphql-tax-queryプラグインがないと使えないため注意して下さい。
カーソルを使ったページネーションについては公式のexampleをご参照下さい。
ページネーション
DIR=$DIR/[paginationTerm]
mkdir -p $DIR && touch "$DIR/[gameCursor].tsx"
import { GetStaticPropsContext } from 'next';
import Page from '@/pages/games/[gameSlug]';
import { getNextStaticProps } from '@faustjs/next';
import { client } from 'client';
import { checkGameExistsBySlug } from '@/utils/game';
export default Page;
export async function getStaticProps(context: GetStaticPropsContext) {
const { paginationTerm, gameSlug } = context.params;
const notFound = !(paginationTerm === 'after' || paginationTerm === 'before') || !(await checkGameExistsBySlug(gameSlug.toString()));
return getNextStaticProps(context, {
Page,
client,
notFound,
});
}
export function getStaticPaths() {
return {
paths: [],
fallback: 'blocking',
};
}
これはFaust.jsのチュートリアルと同じですが、独自にゲームの存在判定を追加しています。 これがないと存在しないスラッグでも404になりません。
なお、ここではパスを生成しません。随時変化するカーソルをビルド時に使う意義は薄いでしょう。
├ ● /games/[gameSlug] (ISR: 900 Seconds) (9804 ms) 1.76 kB 184 kB
├ ├ /games/minecraft-dungeons (1532 ms)
├ ├ /games/minecraft-console (1519 ms)
├ ├ /games/minecraft-je (1427 ms)
├ ├ /games/hytale (1330 ms)
├ └ /games/minecraft (1225 ms)
上記の実装だと、こんな感じでパスが生成されます。
以上、Faust.jsでカスタムタクソノミーを扱う手順でした。GQtyの情報がなさすぎてつらい。
Discussion