Vinyl

Next.jsのAppRouterでSSGする

はじめに

当ブログをNext.jsのApp Routerを利用し作り直しました。

作り直し当初はmicroCMSさんのサンプルを少し参考にしていたのですが、サンプルでは基本的にはすべてSSRしており(2023/11時点)、当ブログでは動的に扱うべきコンテンツは特にない + URLからOGPデータを引っ張ってきてカードで表示する処理(以下みたいなやつ)

「なんだろう、無駄なuseState使うのやめてもらっていいですか?」favicon iconzenn.dev

が少々重たい上に(書き方が悪いだけかもですが)以前はPages Router × SSGにて実装していたので、パフォーマンスが気になりやはりSSGが良いと思い書き直しました。

SSGの設定をする

Pages Routerの場合はnext build & next exportを実行する必要がありますが、App Routerだと下記の設定をnext.config.jsに追加し、next buildの実行だけで静的ファイルがoutディレクトリに作成されます。

/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'export'
}
module.exports = nextConfig

Deploying: Static Exports | Next.jsNext.js enables starting as a static site or Single-Page Application (SPA), then later optionally upgrading to use features that require a server.favicon iconnextjs.org

※ 余談ですが、上記の設定をせずにbuildをするとoutディレクトリにファイルは生成されませんが、.next/serverの中に静的ファイルが生成されnexr startでサーバを動かした時にこのファイルが使用されるようです。

データの取得

PagesRouterでは、getStaticPropsgetServersidePropsを使用しSSG、SSRの挙動を使い分けていましたが、App Routerではそれらを区別せずにServer Component内でデータの取得を行います。

例えば以下のようなコードでgetStaticPropsと同じ挙動を実現できます。

export default async function Page() {
// 先程の設定でSSGを有効にしているので、この処理はbuild時に実行されます。
  const res = await fetch('https://api.example.com/...')
  const data = await res.json()
  return <main>...</main>
}

動的なルートの生成

以下の例を元に確認していきます。

type Props = {
  params: {
    slug: string
  }
}
 
export async function generateStaticParams() {
  const posts = await fetch('https://.../posts').then((res) => res.json())
  return data.map((post) => ({
      slug: post.slug
    }))
}
 
export default async function Page({ params }: Props) {
  const post = await getPostDetail({ slug: params.slug })
  // ...
}

Pages RouterではgetStaticPathsにて動的ルートを生成していましたが、App RouterではgenerateStaticParamsを使用します。

Routing: Dynamic Routes | Next.jsDynamic Routes can be used to programmatically generate route segments from dynamic data.favicon iconnextjs.org

さらに以前のgetStaticPathsのようにpathsのネストや、paramsをkeyにもつ必要はなくなり、非常にシンプルに書けるようになりました。

export const getStaticPaths = (async () => {
  return {
    paths: [ {
        params: {
          slug: 'next.js'
        }
      }],
    fallback: true
  }
})

また、paramsの型に関しては、GetServerSideProps<T> のような型がなさそうなので、現時点では自分で作成し、指定をする必要があります。

type Props = {
  params: {
    slug: string
  }
}
export default async function Page({ params }: Props) {

また、以下のように複数のセグメントによるパスを作成したい場合は、以下のように記述することで実現ができます。

export function generateStaticParams() {
  return [
   { category: "electronics", product: "1" },
   { category: "electronics", product: "2" },
   { category: "books", product: "3" },
   { category: "books", product: "4" },
  ]
}
 
export default function Page({ params }: { params: { category: string; product: string } }) {
  // ...
}

例えば、以下のようなファイル構造だと仮定すると

app
├── products
│  ├── [category]
│  │  └── [product]
│  │    └── page.tsx
│  └── page.tsx
└── page.tsx

以下のようなパスが生成されます。

  • /products/electronics/1
  • /products/electronics/2
  • /products/books/3
  • /products/books/4
  • ...

さいごに

SSGに関しては多少シンプルに書けるようになった気がします。

しかし、App Routerそのものに関しては使用していてPages Routerとは完全に別物に感じます。

特にキャッシュ、Server Action(これはReactの機能ですが)あたりは複雑そうで、まだノータッチなのでいずれキャッチアップしたいですね..。