APIによるページ生成

ここではページへデータを渡すで使ったgetStaticData()を応用し、APIやCMSから取得した記事を一覧へ流し込み、記事ごとの詳細ページを生成します。

ここでのデータ取得は開発・ビルド時に行います。公開サイトへのアクセスごとにAPIを呼ぶ機能ではありません。

Table of Contents

APIのレスポンスを用意する

この例では、次の形のJSONを返すAPIを使います。https://api.example.com/newsは説明用のURLなので、実際のAPIへ置き換えてください。

APIのレスポンス例
[
  { "slug": "hello", "title": "サイトを公開しました", "body": "最初のお知らせです。" },
  { "slug": "update", "title": "更新しました", "body": "サービス紹介を追加しました。" }
]

取得処理をsrc/data/news.tsへまとめます。APIをまだ用意していない場合は、関数の中からこの例と同じ配列を返して試せます。

src/data/news.ts
export type Article = { slug: string; title: string; body: string }

export async function getArticles(): Promise<Article[]> {
  const response = await fetch("https://api.example.com/news")
  if (!response.ok) {
    throw new Error(`お知らせの取得に失敗しました: ${response.status}`)
  }
  const data: unknown = await response.json()
  if (!Array.isArray(data)) throw new Error("お知らせの形式が正しくありません")
  return data.map((item) => {
    if (
      typeof item !== "object" || item === null ||
      typeof item.slug !== "string" || !/^[a-z0-9-]+$/.test(item.slug) ||
      typeof item.title !== "string" || typeof item.body !== "string"
    ) {
      throw new Error("お知らせの記事データが正しくありません")
    }
    return { slug: item.slug, title: item.title, body: item.body }
  })
}

HTTPエラーとデータの形を確認し、取得できなければビルドを失敗させます。APIごとの認証や検証は、この関数内で実装します。

一覧ページへデータを渡す

一覧ページのgetStaticData()でgetArticles()を呼び、取得した記事をpropsとして渡します。

src/pages/news/index.tsx
import type { Metadata, PageProps, GetStaticData } from "minista/types"
import { getArticles, type Article } from "../../data/news"

type Props = PageProps & { articles: Article[] }

export const metadata: Metadata = { title: "お知らせ" }

export const getStaticData: GetStaticData = async () => ({
  props: { articles: await getArticles() },
})

export default function News({ title, articles }: Props) {
  return (
    <>
      <h1>{title}</h1>
      <ul>
        {articles.map((article) => (
          <li key={article.slug}>
            <a href={`/news/${article.slug}`}>{article.title}</a>
          </li>
        ))}
      </ul>
    </>
  )
}

取得した記事のタイトルとリンクは、ビルド時にHTMLへ出力されます。サイト閲覧時にこのAPIへ接続する必要はありません。

データごとのページを作る

ファイル名へ[slug]を使い、ページごとのpathsとpropsを配列で返します。ファイルベースルーティングでnews/hello.tsxを作った場合は削除して、同じURLの競合を避けてください。

src/pages/news/[slug].tsx
import type { GetStaticData, PageProps } from "minista/types"
import { getArticles, type Article } from "../../data/news"

type Props = PageProps & { article: Article }

export const getStaticData: GetStaticData = async () => {
  const articles = await getArticles()
  return articles.map((article) => ({
    paths: { slug: article.slug },
    props: { title: article.title, article },
  }))
}

export default function ArticlePage({ article }: Props) {
  return (
    <article>
      <h1>{article.title}</h1>
      <p>{article.body}</p>
    </article>
  )
}

pathsのキーは[slug]の名前と一致させます。値は文字列です。数値のIDを使う場合はString(id)へ変換します。

入力と出力を確認する

プロジェクト構成
src/
├── data/
│   └── news.ts
└── pages/
    └── news/
        ├── index.tsx
        └── [slug].tsx
ビルド結果
dist/news/
├── index.html
├── hello.html
└── update.html

[slug].htmlというファイルが出るのではなく、APIから返した各値でファイル名が決まります。APIのデータが変わったら、再ビルドしてHTMLと関連アセットを更新します。

認証情報と実行タイミング

認証が必要なAPIでは、ビルド環境に設定した環境変数を利用できます。例えばprocess.env.CMS_TOKENを取得し、リクエストのヘッダーへ渡します。

認証情報はページのprops、HTML、Islandへ渡す値に含めないでください。ブラウザへ公開するVITE_*変数にも入れません。必要な環境変数は、ターミナルやCIなどビルドを実行する環境へ設定してください。

getStaticData()は開発中のページ表示やcheck inspectでも実行されます。一覧と詳細の取得関数を共通化しても、APIリクエストが自動的に一回へまとまるわけではありません。

生成するページを検査する

npx minista check --json
npx minista inspect --json
npm run build
npm run preview

URLの重複、API取得の失敗、ページの表示を確認します。複数のパラメーターには[category]/[slug].tsxのような構成を使えます。公開型はTypeScript、APIの契約はpluginSsgを参照してください。

公開手順はビルドとデプロイを参照してください。

次はレイアウトで、各ページへ共通のヘッダーとフッターを追加します。