SHUNKE

Shunke Takami

Next.js + TypeScript + Headless CMS ことはじめ 2021

目次

はじめに

本記事は Next.js, (React, TypeScript,) Vercel, Headless CMS を利用して静的ブログサイトを作成するチュートリアルです。

Next.js と Vercel を利用することで、優れた DX とサイトパフォーマンスをすぐさま享受できます。また、TypeScript を用いるフロントエンド開発環境の構築も簡単に行えます。
本記事を読むことで、その開発体験とそれぞれの一部機能を理解することができます。

本サイト (shunke07.com) はまさに上記の技術スタックで構成しています。また、本サイトの Lighthouse スコア(CDN キャッシュが存在する場合の結果)は次の画像の通りです。

画像

利用する CMS について
本記事の内容は Next.js とTypeScript を利用したフロントエンドの構築にフォーカスしており、CMS 側の設定は省略しています。また、設定は各サービスごとに異なるため、それぞれの公式ドキュメントを参考にしていただけますと幸いです。

実際は REST API の API Key とレスポンスが異なるのみで、Headless CMS の設定の差異をフロントエンドで意識することはあまりないはずです。

なお、本記事では日本製の microCMS を取り扱います。設定方法は公式ドキュメントにて解説されていますのでそちらをご覧下さい。

用語

  • Next.js:React のフレームワークです。サーバーサイドレンダリング(SSR、本記事では扱いません)のほか、後述する Static Generation など、様々なモダンフロントエンドに対応する機能を備えています。
  • Vercel: Next.js を開発している会社です。また、同社が提供するホスティングサービスの名称でもあります。
  • Static Generation:HTML を生成する方法の1つです。この方法では、ビルド時に HTML を生成し、各リクエストに対して再利用されます。本記事では、Next.js と Vercel を通して利用します。
  • Headless CMS:フロントエンドから切り離された、バックエンドのみを提供する CMS です。主に REST API を介してコンテンツにアクセスします。本記事で取り扱う microCMS の公式ブログでは Headless CMS の解説記事が公開されています。

TL;DR


記事内では一部コードを省略した形で記載していますので、全体の記述は上記ソースコードをご参照下さい。また、上記ソースコードは本記事の内容に加えて、別記事で解説する内容なども含んでいます。

なお、Vercel による各 Headless CMS を利用したサンプルコードも公開されています。 GitHub の next.js リポジトリ内で公開されており、それらへのリンクは公式ドキュメントに記載されています。

バージョン

本記事の内容は、下記のパッケージバージョンを前提としています。バージョンの差異によって、一部機能が異なる可能性があります。

  • next: 10.0.5
  • react: 17.0.1
  • react-dom: 17.0.1

Next.jsプロジェクトとTypeScriptのセットアップ

本記事では yarn を使用します。npm を使用する場合は適宜コマンドを置換して、同様に行ってください。

yarn create next-app <your-project-name>
cd <your-project-name>


下記のコマンドを使用すると Next.js によって、tsconfig.json にデフォルトのルールが適用されます。また、next-env.d.ts ファイルが生成されます。ここで生成された tsconfig.jsonstrict オプションが false になっていますので、true に変更しましょう。

touch tsconfig.json
yarn add --dev typescript @types/react @types/node
yarn dev


Create Next App で作成された .js ファイル群を .tsx ファイルへと更新しましょう。型定義も提供されていますので、_app.tsx index.tsx はそれぞれ下記のように変更しましょう。

// _app.tsx
import { AppProps } from 'next/app'
//...
const App = ({ Component, pageProps }: AppProps): JSX.Element => (
 <Component {...pageProps} />
)
export default App

// index.tsx
import { NextPage } from 'next'
//...​
const Home: NextPage = () => {
 //...
}
export default Home


src ディレクトリ
Next.js では src ディレクトリがサポートされています/src を作成し、/pages, /styles /src 配下に移動しておきましょう。

絶対パスとパスエイリアス
Next.js では絶対パスとパスエイリアスがサポートされていますtsconfig.jsoncompilerOptions に下記を追記しましょう。この設定によって、styles 以下のファイルは @/styles/ のパスで指定できますので、各ファイルのインポート文を更新しましょう。

// tsconfig.json
"compilerOptions": {
 "baseUrl": "./src",
 "paths": {
  "@/styles/*": ["styles/*"] 
 }
 // ...
}


また、必要に応じて、/components などのディレクトリを追加するごとに paths の設定も追加していきましょう。

CMS の連携

各 CMS によって異なります。CMS の設定に従って、クライアント API Key を取得しておきます。

環境変数の設定(.env.local の作成)

Next.jsではいくつかの .env ファイルがサポートされています。本記事では、 .env.local ファイルのみを取り扱います。

ファイルを作成し、以下を記述します。<your-cms-api-key> は上記で取得した API Key に置換してください。

// .env.local
CMS_API_KEY=<your-cms-api-key>

デプロイ

Vercel は Git と連携し、CI/CD によるデプロイを行います。リポジトリを用意して Vercel の連携設定を行います。詳しくは公式ドキュメントをご参照下さい。

上記で設定した環境変数を Vercel のホスティングサイトに対しても適用します。
Vercel のダッシュボードから、対象プロジェクト > Settings > Enviroment Variables と進み、下記画像のように API Key を入力して保存してください。

画像

その後、GitHub の main ブランチにコードをプッシュすることでビルド・デプロイが行われ、サイトが公開されます。

クライアントロジック

ここからは、クライアントのロジックをコーディングしていきます。

Static Generation に関する API

GetStaticProps

getStaticProps という非同期関数を pages 内のコンポーネントで export すると、そのページはその関数の戻り値を伴い静的ビルドされます。公式ドキュメントはこちら

getStaticProps の戻り値はページコンポーネントの引数(props)として受け取れます。なお、props の型を定義するには下記のように Props 型を定義し、 NextPage 型を拡張します。

index.tsx では各記事へのリンクを表示しましょう。内部リンクには next/link より import できる Link コンポーネントを使用します。

// index.tsx
// 一部コードは省略しています
// importしている関数と型定義については後述します
import { GetStaticProps } from 'next'
import Link from 'next/link'
import { fetchArticles } from '@/repositories/cms'
import type { Article } from '@/types/cms'type Props = {
 articles: Article[]
}
​
export const getStaticProps: GetStaticProps = async ({ params }) => {
 const article = await fetchArticles()
 return {
  props: articles,
 }
}
​
const Home: NextPage<Props> = ({ articles }) => (
 <div className={styles.container}>
  <ul>
  {articles.map((article) => (
    <li key={article.id}>
     <Link href={`/articles/${article.id}`}>
      <p>{article.title}</p>
     </Link>
    </li>
  ))}
  </ul>
 </div>
)
​
export default Home


ダイナミックルート
Next.js ではダイナミックルートがサポートされています。ファイル名にブラケットを使用したページを追加することで実現できますので、/pages 以下に articles/[slug].tsx を作成しましょう。

GetStaticPaths

上記で作成した articles/[slug].tsx で、前述の getStaticProps を使用するには (すなわちダイナミックルートを静的ビルドするには)、それに加えて getStaticPaths という非同期関数を使用する必要があります。公式ドキュメントはこちら

getStaticPathsでは、生成したいページのパスを配列にした paths プロパティと、ビルドされたページが存在しない場合のフォールバックをどのように行うかを指定する fallback プロパティを、戻り値として必ず指定する必要があります。

それぞれの値の指定にはいくつかの選択肢がありますが、今回は下記コードの通りに指定しましょう(詳しくは別記事で解説します)。下記のようにfallbackfalse とした場合、その URL(slug)の記事が存在しない場合には、404 ページが表示されます。

このページで使用する getStaticProps では、slug (記事の ID)を元に API を呼び出すため、引数の context から params プロパティを使用しています。なお、マークアップは一例です。

// index.tsx
// 一部コードは省略しています
// importしている関数と型定義については後述します
import { GetStaticPaths, GetStaticProps, NextPage } from 'next'
import Head from 'next/head'
import { fetchArticles, fetchArticle } from '@/repositories/cms'
import type { Article } from '@/types/cms'type Params = {
 slug: string
}
​
type Props = {
 article: Article
}
​
export const getStaticPaths: GetStaticPaths = async () => {
 const articles = await fetchArticles()
 const paths = articles.map((article) => `/articles/${article.id}`);
 return { 
  paths, 
  fallback: false
 }
}
​
export const getStaticProps: GetStaticProps = async ({ params }) => {
 const articleId = (params as Params).slug
 const article = await fetchArticle(articleId)
​
 return {
  props: {
   article,
  },
 }
}
​
const ArticlePage = ({ article }) => (
 <div className={styles.container}>
  <Head>
   <title>{article.title}</title>
  </Head>
  <article>
   <h1>{article.title}</h1>
   <p>カテゴリ: {article.category}</p>
   <p>作成日時: {article.publishedAt}</p>
   <img
    src={article.thumbnail.url}
    alt="サムネイル"
    width="400"
    height="200"
   />
   <div dangerouslySetInnerHTML={{ __html: article.text }} />
  </article>
 </div>
)
​
export default ArticlePage

CMS REST API の呼び出し

上記のコード内で利用している、CMS を呼び出す非同期関数については /repositories/cms.ts を作成してエクスポートしています。

リクエスト処理には fecth API を利用しています。なお、Next.js では fetch API の polyfill がデフォルトでサポートされています

// repositories/cms.ts
import type { Article, Response } from '@/types/cms'const endpoint = '<your-cms-endpoint-url>'
const config = {
 headers: {
  'X-API-KEY': process.env.CMS_API_KEY,
 },
}
​
export const api = async <T>(url: string): Promise<T> => {
 const response = await fetch(url, config)
 if (!response.ok) {
  throw new Error(response.statusText)
 }
 const json = await response.json()
 return json
}
​
export const fetchArticle = async (articleId: string): Promise<Article> => {
 const url = `${endpoint}/${articleId}`
 const article = await api<Article>(url)
 return article
}
​
export const fetchArticles = async (): Promise<Article[]> => {
 const { contents } = await api<Response>(endpoint)
 return contents
}


型定義は /types/cms.d.ts を作成し、エクスポートしています。各オブジェクトのプロパティは CMS の設定・種類(レスポンス)に応じて型を定義しましょう。
下記は microCMS を利用し、それぞれ対応するフィールドを設定した場合の型定義です。

// types/cms.d.ts
export type Article = {
 id: string
 category: string
 text: string
 title: string
 thumbnail: {
  url: string
 }
 createdAt: string
 publishedAt: string
 revisedAt: string
 updatedAt: string
}
​
export type Response = {
 contents: Article[]
 limit: number
 offset: number
}


ここまでで、CMS コンテンツの取得と表示、ルーティングが完成しました。再度 GitHub にコードをプッシュし、サイトを更新しましょう。例によって、ビルドとデプロイは Vercel が行ってくれます。

おわりに

以上で 0 から Next.js + TypeScript + Headless CMS を利用した静的ブログサイトを作成することができました。

Next.js と Vercel はフロントエンド最適化のための様々な機能を持ち合わせています。今回は Static Generation に関する基本的な API を中心にコーディングを行いました。

説明しきれなかった下記の機能については、別記事として公開したいと思います。

  • ISR(Incremental Static Regeneration)
  • next/image
  • next/head(OGP 対応)
  • Custom Document
  • Custom Error Page
  • Webhook による Vercel デプロイ(CMS コンテンツ更新時の対応)


最後までご覧いただきありがとうございました。