Tags: Headless, Next.js, Shifter JA, Tutorial

Next.jsを使った静的なHeadless WordPressサイトの作り方

Next.jsはReactを使ったアプリケーションフレームワークの1つです。Vercel, Incというホスティング会社を中心にOSSとして開発・公開されています。

事例もhuluやNIKE、Netflixなど数多く、Reactを使ってWebサイトを公開する際に選択されることの多いフレームワークの1つです。

この記事では、このNext.jsを用いてWordPressサイトのフロントエンドを構築する方法を紹介します。

完成イメージ

すべてのソースコードを紹介することは大変です。そのため、サンプルサイトのソースコードをGitHubに用意しました。

このサイトのデータを利用してビルドした場合、以下のようなサイトが出来上がります。

Next.jsで「静的サイト」を構築する

Next.jsでは2種類の方法でWebサイトを作ることができます。

  • Static Generation: ビルド時に静的なHTMLを生成する
  • Server-side Rendering: Node.js上でHTMLを生成する

今回はNetlifyやAWS Amplifyなどで公開できる「Static Generation」を利用し、静的サイトを作ります。

どちらもメリット・デメリットがあり、またIncremental Static Regenerationやserverless modeなどより高度な設定も可能ですが、まずはもっともシンプルな方法からはじめましょう。

Next.jsアプリケーションのセットアップ

Next.jsでアプリケーションをセットアップするには、create-next-appコマンドを利用します。

$ npx create-next-app
# or
$ yarn create next-app

はじめに紹介したサンプルアプリケーションをそのまま利用する場合は、以下のようにリポジトリ名も指定しましょう。

$ npx create-next-app YOUR_APP_NAME --example "https://github.com/getshifter/headless-example-nextjs-typescript/tree/main"

[Appendix] TypeScriptを利用する

TypeScriptを用いることで、誤って存在しない値を利用するような事故を防ぐことができます。また、IDE上でオブジェクトや引数の値を簡単に調べることができるなど、開発上のメリットもあります。

通常、以下のコマンドを利用することでTypeSciprtを使う準備ができます。

$ touch tsconfig.json
$ npm install --save-dev typescript @types/react @types/node

しかしこの手順に加えて既存のファイルをTypeScriptに書き換えるのはすこし手間ですので、個人的に公開しているStarterを利用するのも1つです。

$ npx create-next-app YOUR_APP_NAME --example "https://github.com/wpkyoto/nextjs-starter-typescript/tree/main"

TOPページにWordPressの投稿一覧を表示する

Next.jsでは、公開するページのテンプレートをpagesディレクトリ配下に作成します。例えば、pages/index.js(またはpages/index.[jsx|ts|tsx])では、サイトのTOPページに表示するコンテンツを記述できます。

TOPページにWordPressの投稿一覧を表示したい場合、このファイルの中でWP APIのデータを取得して表示するコードを書く必要があります。

データの取得には、swrを利用する

外部のAPIからデータを取得するためのライブラリはAxiosやsuperagent、fetch polyfilなど数多く存在します。が、ここではNext.jsと同じ会社・チームがリリースしているswrというライブラリを利用します。

$ npm install --save swr isomorphic-unfetch

pages/index.jsを実装する

以下のコードが、シンプルな記事一覧表示のための実装です。

import useSWR from 'swr'
import Head from 'next/head'

export const Home = () => {
  const {data: posts} = useSWR(
      'https://YOUR_SITE_URL/wp-json/wp/v2/posts'
  )
  return (
    <div>
      <Head>
        <title>Create Next App</title>
      </Head>
      {posts.map(post => {
          return (
              <article key={post.id}>
                  <h2 dangerouslySetInnerHTML={{__html: post.title.rendered}} />
                  <div dangerouslySetInnerHTML={{__html: post.excerpt.rendered}} />
              </article>
          )
      })}
    </div>
  )
}

export default Home

swrが提供するuseSWRというReact Hookを利用してWP APIからデータを取得します。その後、取得したデータを利用して各記事のHTMLを定義しています。

[For production] カテゴリ・サムネイル・著者情報などが必要な場合は?_embedクエリをつけよう

WP APIで記事情報を取得する場合、デフォルトではその記事のカテゴリ・タグやサムネイル・著者情報などがレスポンスから除外されています。

もしこれらのデータを利用したい場合は、_embedクエリをAPIリクエストに追加するようにしましょう。

記事一覧の表示を静的化する

一般的なReactアプリケーションであれば、先程のステップで記事一覧の表示は完成です。しかしNext.jsで静的サイトを作る場合、もう一手間加えることでより早いサイトにすることができます。

Next.jsでサイトを静的化する場合、getStaticPropsという関数を利用することで、サイトをビルドする時に記事データを取得することができます。

import useSWR from 'swr'
import Head from 'next/head'
import unfetch from 'isomorphic-unfetch'

const fetcher = url => unfetch(url).then(r => r.json())
const url =  'https://YOUR_SITE_URL/wp-json/wp/v2/posts'

export const Home = ({posts: initialPosts}) => {
  const {data: posts} = useSWR(
     url,
      fetcher,
      initialPosts
  )
  return (...)
}

export const getStaticProps = async () => {
     const posts = await fetcher(url)
     return {
       props: { posts }
     }
}

export default Home

いくつかの変更が加わりました。

getStaticPropsの中でWP APIを呼び出すことで、ビルド時に記事一覧を取得します。その後、useSWRの第三引数に取得した値をいれています。これによって、フロントエンド側で記事一覧を取得しなおす処理が完了するまでの間、ビルド時に取得した記事一覧が表示されるようになります。

このようにNext.jsでは通常のReactアプリケーションのような振る舞いに加えて、ビルド時にデータを事前取得してしまうという方法が使えます。

記事詳細へのリンクを設定する

最後に、表示した記事の詳細ページへのリンクを設定しましょう。Next.jsではnext/linkライブラリを利用してサイト内のリンクを作ります。

import useSWR from 'swr'
import Head from 'next/head'
import Link from 'next/link'

export const Home = () => {
      ...
      {posts.map(post => {
          return (
              <article key={post.id}>
                <Link href={`/${post.slug}`}>
                  <h2 dangerouslySetInnerHTML={{__html: post.title.rendered}} />
                </Link>
                  <div dangerouslySetInnerHTML={{__html: post.excerpt.rendered}} />
            ...

h2タグをLinkタグで挟むように実装しました。これで記事タイトルをクリックすると、「example.com/{THE_POST_SLUG}」に遷移します。パーマリンクをpost_idに設定している場合は、post.slugpost.idに変更しましょう。

個別の記事ページを作成する

記事個別ページへのリンクを作成しましたが、肝心の記事詳細ページがまだできていません。ここからはNext.jsの静的サイトとして記事詳細ページを作る方法を紹介します。

WordPressの投稿のようにNext.jsとは別の場所で動的に生成されるページを処理する場合、Dynamic Routing機能を利用します。パーマリンクを設定している場合は、pages/[slug].jsを、post_idを設定してる場合はpages/[id].jsを新しく作成しましょう。[XXX]のXXXがgetStaticPropsの引数として利用できるようになります。

import useSWR from 'swr'
import Head from 'next/head'
import unfetch from 'isomorphic-unfetch'

const fetcher = url => unfetch(url).then(r => r.json())
const url =  'https://YOUR_SITE_URL/wp-json/wp/v2/posts'

export const Home = ({post: initialPost, slug}) => {
    const {data: post} = useSWR(
        [url, `?slug=${slug}`].join('/'),
        fetcher,
        initialPost
    )
  return (
    <div>
      <Head>
        <title>{post.title.rendered}</title>
      </Head>
      <article key={post.id}>
          <h2 dangerouslySetInnerHTML={{__html: post.title.rendered}} />
          <div dangerouslySetInnerHTML={{__html: post.content.rendered}} />
      </article>
    </div>
  )
}

export default Home


export const getStaticPaths = async () => {
    const posts = await fetcher(url)
    return {
        paths: posts.map(post => ({
            params: {
                id: post.id,
                slug: decodeURI(post.slug)
            }
        })),
        fallback: false
    }
}

export const getStaticProps = async ({params}) => {
    if (!params) {
        return {
            props: {
                post: null,
            }
        }
    }
    const slug = typeof params.slug === 'string' ? params.slug : params.slug[0]
    const posts = await fetcher([url, `?slug=${slug}`].join('/'))
    return {
        props: {
            slug,
            post: Array.isArray(posts) ? posts[0] : posts
        }
    }
}

getStatisPropsでslugを利用してWP APIから記事を取得しています。React側で取得した値を使いつつ、データを再取得している部分や、データを表示させる部分は記事一覧とほとんど同じですね。

ここで新しくgetStaticPathsという関数が登場しています。これはNext.jsで静的に生成する必要のあるページのパス一覧を取得するための関数です。

この関数で指定されたパスのページが静的HTMLとして生成されますので、WP APIから生成したい記事のデータを取得して、必要な形に変換して渡しています。

[For production] getStaticPathsではWP APIの再帰呼び出しを

今回のサンプルではシンプルな例として記述していますが、この実装では10記事分のデータしか生成されません。これはWP APIが返す記事件数のデフォルト値に依存します。

実際の運用では、数百件またはそれ以上の記事を処理する必要があります。そのため以下のサンプルのように、WP APIを再帰的に呼び出して全記事のデータを取得する必要があります。

// via: https://github.com/getshifter/headless-example-nextjs-typescript/blob/3e285bb2c0378c0df035a1e0e98a67cfb42b668b/pages/%5Bslug%5D.tsx#L22-L49

export const listAllPosts = async (APIURLBuilder: WPAPIURLBuilder, posts: WPPost[] = []): Promise<WPPost[]> => {
    const perPage = 20
    try {
        const url = APIURLBuilder.perPage(20).getURL()
        const response = await fetch(url)
        if (
            response instanceof Error ||
            (
                response.data &&
                response.data.status &&
                response.data.status > 399
            )
            ) {
            throw response
        }
        const mergedPosts = uniqWPPosts([...posts,...response])
        if (canUseServerSideFeatures() || response.length < perPage) {
            return mergedPosts
        }
        APIURLBuilder.nextPage()
        return listAllPosts(APIURLBuilder, mergedPosts)
    } catch (e) {
        if (e.code && e.code === 'rest_invalid_param') {
            return posts
        }
        throw e   
    }
}

ローカル環境でテストする

ここまで準備ができれば、あとはローカルで実際に動くかどうかを確認してみましょう。Next.jsのアプリをローカルで動かすには、npm run devまたはnext devコマンドを利用します。

サイトの静的化をテストする

実際にサイトを静的化するテストは、next buildコマンドとnext exportコマンドを組み合わせて実行します。package.jsonに以下のようなコマンドを追加しておくと便利です。

...
"scripts": {
    "build:static": "next build && next export", 
    "dev": "next dev",
....

静的化した後のページの表示は、npx serve outコマンドを実行して表示されたURLにアクセスしてみましょう。

[For development] 開発時はISG modeをオンにしよう

Next.jsでフロントエンドを実装している際、ボトルネックになりやすいのがgetStatcsPathsを利用しているページです。このページではWordPressの投稿などのデータを全件またはそれに近い件数を取得するように実装されていることがあります。


export const getStaticPaths = async () => {
    const posts = await getAllWPPost()
    return {
        paths: posts.map(post => ({
            params: {
                id: post.id,
                slug: decodeURI(post.slug)
            }
        })),
        fallback: false
    }
}

しかしこの場合、この実装を利用しているページにアクセスする度にgetStaticPathsが実行され、ページ表示の遅延やAPIサーバーへの負荷につながります。

そこで導入したいのが、ISG(Incremental Static Generation)というNext.jsの機能です。この機能は静的サイトの構築では利用できませんが、ローカルでの開発時のみオンにするという使い方ができます。

const isProd = process.env.NODE_ENV === 'production'

export const getStaticPaths = async () => {
    const posts = await isProd ? getAllWPPost(): getWPPost({ limit: 10 })
    return {
        paths: posts.map(post => ({
            params: {
                id: post.id,
                slug: decodeURI(post.slug)
            }
        })),
        fallback: isProd ? false: 'blocking'
    }
}

このサンプルコードでは、NODE_ENV環境変数を利用しています。これはnext devではdevelopmentnext buildではproductionが設定される値なので、開発モードかそうでないかの判別に利用できます。

今回のケースでは、next buildした後、next exportして生成されたHTMLを利用しますので、next buildの時点で静的サイトとして振舞うようにしています。

そしてproductionでない状態の時は、getStaticPathsで取得する記事の件数を抑止します。これは戻り値のfallbackblockingまたはtrueに設定することで、事前に生成していないページにアクセスした場合でも、ISGモードによってページが自動生成されるためです。

このように実装することで、不必要なAPI呼び出しを行うことも、その処理を待つこともなくフロントエンドの開発に集中できます。

Netlifyにサイトをデプロイする

ローカルでの動作確認・ビルドが成功した場合、最後にNetlifyへデプロイしサイトを公開しましょう。

ここまでの変更をGitでコミットした後、GitHubまたはGitLabなどご利用のGitリポジトリサービスにpushしましょう。

$ git add .
$ git commit -m "feat: ready for publish"
$ git push origin main 
or
$ git push origin master

Netlifyまたはそのほかのホスティングサービスでビルドを行う場合の設定は、以下のように行います。

  • Build command: npm run export or yarn export
  • Publish directory: out

まとめ

Next.jsはGatsbyと比較して、データ取得処理を自前で書く必要があるなど、ある程度ReactおよびJavaScriptのコードが書けることが前提になっています。

ですが、その分「特定の条件でのみ動的なアプリケーションとして振舞わせる」のような柔軟性の高い利用・運用ができることがメリットではないかと思います。

また、AWSを利用したServerless Nextjs PluginVercelを利用した動的な処理やE-Commerceなど、今回の例以外にも様々な使い方ができますので、こちらも今後ブログで紹介していきたいと思います。