【1448日目】Next.js × WordPressでつまづいたこと

2023.01.20

前回の記事でNext.js × WordPressで自分のウェブサイトを一新しようという話をしましたが、仕事がそこまで多くなかったこともあり1ヶ月足らずで一旦完成することができました!

簡単にウェブサイトの特徴を整理すると

  • WordPressをヘッドレスCMSとして使用し、記事の管理はWordPressで行う
  • ウェブサイトはNext.jsで作り、WordPress REST APIを使って記事の取得を行う
  • Bogoを使った多言語対応
  • WP Githuber MDを使ってマークダウンで記事投稿可能

最初はVue.jsと全然違っていて戸惑いましたが、色々と触っていくうちに似ている考え方などもでてきてすんなり理解することができたと思います。

あと以外にもFlutterと似ている点が多く、助かりました(特にHook周り)。色々な言語やフレームワークを使えると新しい技術の勉強もスムーズでいいですね。

とはいえ新しい機能を追加するたびにつまづいては調べての繰り返しでした。きっとNext.jsに精通している人だったら2~3日くらいで作れるんじゃないかなと思います。

今回はつまづいたところを備忘録もかねて記事にしたいと思います。

WordPress REST APIから全記事取得する時の書き方

WordPressの記事を表示する際にgetStaticPathsでそれぞれの記事のidを取得する必要があり、そのためWordpress REST APIで全記事を取得する必要があります。

ただ残念ながらWordpress REST APIのパラメータで「全記事取得」みたいなものは存在しないので、

  • 1.全記事取得すると何ページあるか取得する
  • 2.それぞれのページごとにAPIを投げて記事を取得し、最後に取得した記事をマージする

というダサい手順が必要でした。

リクエストできるパラメータの中にページ数があることは知っていたので、-1とか0とか指定して送信すれば全記事取得できるだろと考えていたのですが、全然そんなことはなかったです。

色々と頑張った結果最終的には下記コードで動かすことができました。

// 最大ページ数を取得
const maxPage: number = await axios
  .get(process.env.WP_ROOT! + '/wp-json/wp/v2/posts')
  .then((response) => {
    // x-wp-totalpagesに最大ページ数が入っている
    return Number(response.headers['x-wp-totalpages']);
  });

// 投稿一覧を格納する
const posts: WPPost[] = await Promise.all(
  createRangeArray(1, maxPage).map(
    async (page: number) =>
      await axios
        .get(process.env.WP_ROOT! + '/wp-json/wp/v2/posts')
        .then((response) => response.data),
  ),
).then((addPostsList: WPPost[][]) =>
  addPostsList.reduce((prePosts: WPPost[], currentPosts: WPPost[]) =>
    prePosts.concat(currentPosts),
  ),
);

// startからendまでの連番の値が入った配列を作る関数
const createRangeArray = (start: number, end: number) => {
  return [...Array(end - start + 1)].map((_, i) => start + i);
};

// 投稿の型
export type WPPost = {
  id: string;
  slug: string;
  title: {
    rendered: string;
  };
  content: {
    rendered: string;
  };
  excerpt: {
    rendered: string;
  };
  date: string;
};

詰まったのはpostsを取得するところで、最初はmapを使って下記の様に書いていたのですが、残念ながら非同期で動いてしまい、うまく記事を取得することができませんでした。

let posts: WPPost[] = [];

// 非同期で動くのでダメ
await createRangeArray(1, maxPage).map(
  async (page: number) => {
    const addPosts : WPPost[] = await axios
      .get(process.env.WP_ROOT! + '/wp-json/wp/v2/posts')
      .then((response) => response.data),
  posts = posts.concat(addPosts);
  }
),

Promise.allがないとダメなんですね。Javascriptのasync/await周りは何となく使えてきたので理解が浅かったです。反省です。

フェードアウトが効かない問題

次にウェブサイト全体のフェードアウトが効かない問題です。

Next.jsで作るのでページ遷移にも何かしらのアニメーションを加えたいところです。

そうなると1番鉄板なのはフェードイン・フェードアウトでしょう。ウェブサイトのアニメーションはとりあえずフェードイン・フェードアウトしておけばかっこよくなると思っています。

というわけで何かいいライブラリがないかと探していたのですが、すぐに見つかりました。

Reactすら初めてなのであまりライブラリについては詳しくないですが、検索してでてくる記事の多さなどから有名なライブラリなんだろうなということで採用しました。

使い方はとても簡単でyarn add framer-motionした後に、こんな感じでmotion.divを指定するだけです。

import { motion, useCycle } from "framer-motion";

const Sample = () => {
  const [x, cycleX] = useCycle(0, 50, 100, 150);

  return (
    <motion.div
      style={{ width: "200px", height: "200px", background: "skyblue" }}
      animate={{ x: x }}
      onClick={() => cycleX()}
    />
  );
};

export default Sample;

引用:Developers IO

今回は下記のようなコンポーネントを作成して、それぞれのページごとに読み込む形で採用しました。

・OverallAnimation.tsx

import { AnimatePresence, motion } from 'framer-motion';
import { useRouter } from 'next/router';

type LayoutProps = {
  children: React.ReactNode;
};

// animation of all page
const OverallAnimation = ({ children }: LayoutProps): JSX.Element => {
  const router = useRouter();
  return (
    <>
      <AnimatePresence mode='wait' onExitComplete={() => window.scrollTo(0, 0)}>
        <motion.div
          animate={{ opacity: 1 }} // 初期状態
          exit={{ opacity: 0 }} // マウント時
          initial={{ opacity: 0 }} // アンマウント時
          key={router.pathname}
          transition={{
            duration: 0.5,
          }}
        >
          {children}
        </motion.div>
      </AnimatePresence>
    </>
  );
};

export default OverallAnimation;

・その他のページ

import OverallAnimation from '../components/OverallAnimation';
import type { NextPageWithLayout } from './_app';

const Home: NextPageWithLayout = () => {
  return (
    <OverallAnimation>
      ・・・
    <OverallAnimation/>
  );
};

肝心なのは

  • AnimatePresencemotion.divの一つ上に置くこと
  • mode='wait'にすること

多言語対応

今回作るウェブサイトの一つの目玉である多言語対応ですが、Wordpressの管理画面まで自前で実装することは大変だったので、管理画面側はBogoというプラグインを利用することにしました。

またWordpress REST APIの対応は下記記事を元に実装しました。

・functions.php

add_action('rest_api_init', 'rest_api_filter_add_filters');
function rest_api_filter_add_filters()
{
  foreach (get_post_types(array('show_in_rest' => true), 'objects') as $post_type) {
    add_filter('rest_' . $post_type->name . '_query', 'rest_api_filter_add_filter_param', 10, 2);
  }
}
function rest_api_filter_add_filter_param($args, $request)
{
  if (empty($request['filter']) || !is_array($request['filter'])) {
    return $args;
  }
  $filter = $request['filter'];
  if (isset($filter['posts_per_page']) && ((int) $filter['posts_per_page'] >= 1 && (int) $filter['posts_per_page'] <= 100)) {
    $args['posts_per_page'] = $filter['posts_per_page'];
  }
  global $wp;
  $vars = apply_filters('rest_query_vars', $wp->public_query_vars);
  $vars = array_unique(array_merge($vars, array('meta_query', 'meta_key', 'meta_value', 'meta_compare')));
  foreach ($vars as $var) {
    if (isset($filter[$var])) {
      $args[$var] = $filter[$var];
    }
  }
  return $args;
}

・API送信

import qs from 'qs';

await axios
  .get(process.env.WP_ROOT! + '/wp-json/wp/v2/posts', {
    params: { page: page, filter: { lang: lang } },
    paramsSerializer: (params) => qs.stringify(params),
  })

つまったとこはparamsSerializer: (params) => qs.stringify(params),です。これがないと{}が文字列に変換されてうまく動作しません。

この単純な問題でかなりの時間無駄にしたので悲しかったです。。

WP Githuber MDとの連携

僕はエンジニアであるにも関わらずマークダウンに慣れていないということもあり、Wordpressの記事もマークダウンで書くようにしています。

そのため「WP Githuber MD」というプラグインを使ってるのですが、Wordpressをヘッドレスにしてもこの運用を続けていきたいと思っていました。

調べてみると「WP Gituber MD」はhighlight.jsを使っているようだったので、同じ様にNext.js側にもhighlight.jsを導入したところ、案外すんなり動きました。

import 'highlight.js/styles/default.css';
import hljs from 'highlight.js';
import javascript from 'highlight.js/lib/languages/javascript';
hljs.registerLanguage('javascript', javascript);

export type Props = {
  post: WPPost;
};

const Post: FC<Props> = ({ post }) => {
  return (
    <>
       ・・・
       <div
           dangerouslySetInnerHTML={{ __html: post.content.rendered }}
      ></div>
       ・・・
    </>
  );
};

最初はマークダウンのことを難しく考えすぎていたんですが、よくよく考えるとWP Githuber MDは記事を保存する段階でマークダウン→HTMLの変換をしているようで、Wordpress REST APIで取得できる記事の内容については特にマークダウンのことは考える必要がありませんでした。

なので普通にCSSとしてhighlight.jsを適用しただけです。意外とシンプル。

今後も何かあれば定期的に追加していきます

一旦ここまで作成した分では以上になります。

色々ありましたがNext.jsはアニメーションなどが簡単につけれるし、デプロイもVercelを使えばすごい簡単だし、TypeScriptの導入も楽ちんだしとても素敵なフレームワークだなと思います。

今後も個人開発では是非積極的に使っていきたいと思います。

ありがとうございました!