🐸

ちょっとした工夫でNext.jsでのSSGサイト開発体験を爆上げする

2024/10/09に公開

SSGサイトを作るとき、大体がgetStaticPropsでWordPressやmicroCMSなどのCMSからデータを取ってくる形になると思うが、返却値に型を付けるのが結構面倒。

そこで今回、ビルド時はCMSを参照せず、ビルド前に生成したJSONファイル元にビルドするようにしたら開発体験がめちゃくちゃ良くなったので紹介したい。

今回はNext.jsで作ったが、Astroなど他の言語でも使えるはず。

この記事でできること

CMSからの返却値のフィールド名と型が全てわかるようになる!

イメージとしてはこんな感じ。

  1. ビルドする前にCMSから必要なデータを全てJSON形式で取ってくる
  2. 取得したJSONを元にビルド

これで開発時は手元にあるJSONファイルを参照することになり、VSCodeなどのエディタでコーディングする時、CMSからの返却値のフィールド名と型が全てわかる状態になる。

実装方法

ビルド前にCMSと通信してJSONファイルを生成

Next.jsのプロジェクト直下で、npmプロジェクトを初期化する。

npm init

before_buildという名前でinitしたとすると、配置としては以下のようになる。

next
┣━before_build(作成したnpmプロジェクト)
┃ ┣━src
┃ ┃┗━index.ts
┃ ┣━…
┃ ┗━package.json
┣━public
┣━pages
┣━…
┗━package.json

before_build/src/index.tsに以下のようなコードを書く。

import * as fs from 'fs'

const JSON_DIR = '../public/json' // プロジェクト直下のリソースフォルダ
const createJsonFile = (fileName: string, data: any) => {
  // jsonディレクトリがなければ作成
  if (!fs.existsSync(JSON_DIR)) {
    fs.mkdirSync(JSON_DIR)
  }
  // 指定の名前でjsonファイルを作成
  fs.writeFileSync(`${JSON_DIR}/${fileName}.json`, JSON.stringify(data))
}
const fetchAllColumnPosts = async () => {
  const res = await fetch('https://hoge.com/posts')
  return res.json()
}

try {
  // 全記事取得してJSONファイルを作成
  const posts = await fetchAllColumnPosts()
  createJsonFile('columnPosts', posts)
} catch (e) {
  console.error('an error occurred. error = ', e)
}

package.jsonのscriptsにこんな感じでスクリプトを追加。

"scripts": {
  "exec": "ts-node src/index.ts"
}

これでnpm run execを実行すると、next/public/json配下にjsonファイルが生成されるようになる。

取得したJSONを参照してNext側でコードを記述

先ほどnext/publicフォルダにJSONを保存したので、後はそれを参照してコードを書くだけ。
pages/columns/[id].tsxの記述例としてはこんな感じ。

import columns from '@/public/json/columnPosts.json'
import { GetStaticPropsContext } from 'next'

export const getStaticPaths = async () => {
  const paths = columns.map((column) => ({
    params: { id: column.id }
  }))
  return { paths, fallback: false }
}

export const getStaticProps = async (context: GetStaticPropsContext) => {
  const column = columns.find(
    (c) => context.params && c.id.toString() === context.params.id
  )!
  const columnForBuild = formatColumn([column])
  return {
    props: {
      column: columnForBuild
    }
  }
}

const formatColumn = (
  columnList: typeof columns
): ColumnForBuild | undefined => {
  // ここでデータの整形を行う
  if (columnList.length === 0) return undefined
  const column = columnList[0]
  return {
    title: column.title.rendered
  }
}

type ColumnForBuild = {
  title: string
}
type Props = {
  column: ColumnForBuild
}
export default function Page(props: Props) {
  // 整形されたデータを使ってページを構築
  return <></>
}

ビルド直前に毎回JSONファイルを更新するよう設定

next/package.jsonのscriptsに以下コードを追加。
これでnpm run buildが走る前に、postbuildに記述した処理が走るようになる。

"scripts": {
  "postbuild": "npm run exec --prefix ./before_build"
}

その他メリット

ビルドが速くなる

  • はじめに全取得する時にリスト形式でデータを取得できるので、各ページで記事などをid指定して個別に取得する場合よりも総通信時間が短くなる
    • リクエスト数自体が少なくなるので、サーバー側(CMS側)にかかる負荷も少なくなる
  • はじめに全てのデータをローカルに落としてからビルドするので、無駄なデータ取得が無くなる
    • 例えば記事ページでカテゴリ一覧と記事の内容が必要な場合、設計に気をつけないと各ページのビルド時に毎回同じカテゴリ一覧のリクエストを送ってしまうことになる

ビルド完了まで検索結果一覧などに更新内容を表示させたく無い時に便利

コンテンツの検索機能を実装する時、APIのフィルタ機能を使うのが楽なので、検索部分だけ動的にすることも多い。
しかしこのようにクライアント側からAPIを直接呼び出す場合、動的に作っている一覧ページには結果が出るが、クリックして静的な詳細ページに飛ぼうとしても、ビルドが完了しておらず404になるケースが出てくる。

この場合も、public/jsonフォルダ内にビルド対象のidが入ったJSONファイルを作成するようにして、検索時にidチェックを行えば、ビルドが完了していないページの結果が簡単に除外できる。

ただ、自分が検索機能を実装する時は、検索対象のフィールドに絞って検索用JSONを用意しておけば例え記事が大量になってもパフォーマンスに大した影響は無いのと、CMSへの負荷をあまり考えたくないので、
APIは使用せずクライアント側でJSONを取得して自前で絞り込む形式にする方が好みではある。

最後に

この実装自体は本当に簡単にできるし、返却値の型が見られるのは感動的なのでぜひ試してみて欲しい。
これで開発速度が1.2倍くらいには速くなるはず!

Discussion