🪬

Next.jsでJamstackな個人サイトを作った

2022/12/09に公開

この記事は ミライトデザイン Advent Calendar 2022 の 9 日目の記事です。

https://qiita.com/advent-calendar/2022/miraito-inc

前日の nyokkiさん の記事に続いて書いていきます。

https://qiita.com/Nyokki/items/2a9b9683fbc5fb70d8ee

はじめに

個人のブログは以前 Wordpress で作っていたのですが、 Next.js を使用してなにか作りたいと思いました。
そこで、今回は Next.js × microCMS というみんながよくやるであろう Jamstack 構成での個人サイトを作成することにしました。

作成した個人サイトの構成や軽いデモを行いたいと思います。

構成

システム構成図

ヘッドレスCMS

コンテンツを管理するために、ヘッドレス CMS の microCMS というサービスを使用しています。

Wordpress などの CMS はコンテンツを管理する部分(バックエンド部分)と表示する部分(フロント部分)が統合されていますが、ヘッドレス CMS はコンテンツを管理する部分(バックエンド)だけの CMS となります。

フロント部分は自分で用意する必要がある分、自由度が高いので Next.js を使用して作成します。

ヘッドレス CMS は Spotify などで使われている Contentful や、Meta 社が開発した GraphQL の活用に特化した GraphCMS などがあります。

今回は日本の会社で日本語対応していることから microCMS を選択しました。

https://microcms.io/

静的サイトジェネレーター

HTML を作成するものとしては Next.js を使用します。

その他には Gatsby などが有名ですが、一番の目的は Next.js で作るということだったので、今回は対象外となりました。

https://nextjs.org/

ホスティングサービス

Next.js の開発元でもある Vercel 社が運営している Vercel というホスティングサービスを使用。

個人使用(Hobby プラン)は無料で、 Next.js と開発元が同じなので相性が良く、 Next.js の新しい機能を使用できたりするため採用しています。

社名と同じでややこしいです。

https://vercel.com/

CI/CD

基本的には Vercel の設定ですべて完結します。

コンテンツ管理するのは投稿記事のみのため、 SSG を採用しソースコード修正時とコンテンツ作成・更新時にデプロイします。

ソースコード修正時は Vercel の設定で指定したブランチ(main)を push したときに自動でビルド・デプロイするようにしています。

コンテンツ作成・更新時は microCMS で Vercel の deploy 用の Webhook を設定して自動でビルド・デプロイされます。

スタイル

ほんとは figma などでデザインを作成してからコーディングしたいのですが、デザインスキルは皆無なので、コンポーネントライブラリの chakra-ui を使ってよしなにスタイリングしてます。

提供されているテンプレートのデザインや、その他のサービスなどを参考に作成しています。

https://chakra-ui.com/

サイト作成手順

今回は作成した個人サイトと同じ様に、 microCMS のコンテンツを取得して表示させる所までをデモしたいと思います。

個人サイトでは chakra-ui を導入してスタイリングしていますが、デモでは API の取得及び静的ファイルの生成・デプロイまでとしたいと思います。

github のサンプルはこちら。

https://github.com/yukimasa/homepage-sample

環境

  • Next.js: 13.0.6
  • React: 18.2.0
  • TypeScript: 4.9.3

Next.js でプロジェクト作成

公式 Getting Started のコマンドを実行して、プロジェクト名の設定と ESLint の設定をします。

ESLint は Yes を選択しましょう。

yarn create next-app --typescript

? What is your project named? › homepage-sample
? Would you like to use ESLint with this project? … No / Yes

プロジェクト作成したら yarn devhttp://localhost:3000/ が立ち上がることを確認します。

スクリーンショット

microCMS の設定

https://microcms.io/ でアカウント作成したら、サービス名、サービス ID を入力してサービスを作成します。

スクリーンショット

API を作成は「自分で決める」を選択し、API 名を「投稿」とし、エンドポイントを posts に設定して次へ進みます。

スクリーンショット

リスト形式を選択し、以下の内容で API スキーマを定義します。

スクリーンショット

テスト用に最低限内容がわかるぐらいで適当にコンテンツを作成しておきます。

スクリーンショット

API で microCMSからコンテンツを取得

microCMS から API でコンテンツを取得するために、公式が提供しているライブラリ(microcms-js-sdk)をインストールしましょう。

https://github.com/microcmsio/microcms-js-sdk

yarn add microcms-js-sdk

.env.local を作成して、microCMS の API キーとドメインを追加します。

こちらの情報は外部公開しないシークレットなデータなので git 管理はしないように注意してください。

.env.local
MICROCMS_SERVICE_DOMAIN=homepage-sample
MICROCMS_API_KEY=hogehogehoge

libs フォルダに client.ts を作成して sdk の設定をします。

libs/client.ts
import { createClient } from "microcms-js-sdk";

export const microcmsClient = createClient({
  serviceDomain: process.env.MICROCMS_SERVICE_DOMAIN ?? "",
  apiKey: process.env.MICROCMS_API_KEY ?? "",
});

次に types/post.ts を作成し記事の型を定義します。

id や作成日などは microCMS の型が用意してくれているので、交差型を利用して記事の型に追加します。

types/post.ts
import { MicroCMSListContent } from "microcms-js-sdk";

export type Post = {
  title: string;
  content: string;
} & MicroCMSListContent;

型定義をしたら pages/index.tsx を作成して記事一覧を取得するコードを記述します。

pages/index.tsx
import { GetStaticProps, InferGetStaticPropsType, NextPage } from "next";
import Link from "next/link";
import { microcmsClient } from "../libs/client";
import { Post } from "../types/post";

type DataType = {
  contents: Post[];
};

type Props = InferGetStaticPropsType<typeof getStaticProps>;

const Home: NextPage<Props> = ({ contents }) => {
  return (
    <div style={{ width: "1170px", margin: "100px auto" }}>
      <h2>投稿一覧</h2>
      <ul>
        {contents.map((post) => (
          <li key={post.id}>
            <Link href={`/posts/${post.id}`}>{post.title}</Link>
          </li>
        ))}
      </ul>
    </div>
  );
};

export default Home;

export const getStaticProps: GetStaticProps<DataType> = async () => {
  const data = await microcmsClient.get<DataType>({ endpoint: "posts" });

  return {
    props: {
      contents: data.contents,
    },
  };
};

上記のコードは getStaticProps でビルド前に API でコンテンツを取得し、コンポーネントに props として渡しています。

トップページを表示すると作成した投稿一覧が表示されていることが確認できます。(システムのテーマがダークなので暗い。)

スクリーンショット

これで microCMS からコンテンツを取得できたことは確認できましたが、ついでに詳細画面も作成します。

詳細画面は query(記事の id)によって動的に変わるページとなるので、Dynamic Routes を使用します。(取得した id ごとに 1 つの HTML が生成される。)

https://nextjs.org/docs/routing/dynamic-routes

Dynamic Routes はファイル名にブラケット釣るので、pages/posts/[id].tsx のようにして下記のコードを記述します。

pages/posts/[id].tsx
import type {
  GetStaticPaths,
  GetStaticPathsResult,
  GetStaticProps,
  InferGetStaticPropsType,
  NextPage,
} from "next";
import { Post } from "../../types/post";
import { microcmsClient } from "../../libs/client";
import { ParsedUrlQuery } from "querystring";

type Params = {
  contentId: string;
} & ParsedUrlQuery;

type DataType = { post: Post };

type Props = InferGetStaticPropsType<typeof getStaticProps>;

const PostId: NextPage<Props> = ({ post }) => {
  return (
    <main style={{ width: "1170px", margin: "100px auto" }}>
      <h2>{post.title}</h2>
      <p>{post.publishedAt}</p>
      <div
        dangerouslySetInnerHTML={{
          __html: `${post.content}`,
        }}
      />
    </main>
  );
};

export default PostId;

// 静的生成のためのパスを指定
export const getStaticPaths: GetStaticPaths = async (): Promise<
  GetStaticPathsResult<Params>
> => {
  const data = await microcmsClient.get({ endpoint: "posts" });
  const paths = data.contents.map((content: Post) => {
    return {
      params: {
        id: content.id,
      },
    };
  });

  return { paths, fallback: false };
};

export const getStaticProps: GetStaticProps<DataType, Params> = async ({
  params,
}) => {
  if (!params?.id) {
    throw new Error("Error: ID not found");
  }

  const data = await microcmsClient.get<Post>({
    endpoint: "posts",
    contentId: params.id as string | undefined,
  });

  return {
    props: { post: { ...data } },
  };
};

上記のコードは getStaticPaths で投稿一覧を取得し、 params としてその idreturn させています。

getStaticProps でその params を受け取り id から詳細 API で投稿の詳細情報を取得しています。

その後は一覧と同じでコンポーネントに props で渡してデータを表示させています。

以上から getStaticPaths 及び getStaticProps は、いずれもビルドを行う前に外部のデータが必要な際に使用する関数ということがわかります。

詳細画面を確認するためにトップページの記事リストのリンクを選択します。

すると詳細画面に遷移し、 microCMS で作成した記事が表示されていることが確認できます。

スクリーンショット

※ dangerouslySetInnerHTML はXSSを引き起こす可能性があるため非推奨とされています。今回の構成においては、入力はmicroCMSのリッチエディタからのみであり、ユーザーからの自由入力箇所ではないため安全であるとして利用しています。

microCMS の 公式ブログ からの引用

なお、開発環境ではリクエストの度に getStaticProps が呼び出されるので、yarn build を実行して実際に静的ファイルが生成されていることを確認しましょう。

❯ yarn build
yarn run v1.22.17
$ next build
info  - Loaded env from /Users/yukimasa/works/myApp/homepage-sample/.env.local
info  - Linting and checking validity of types
info  - Creating an optimized production build
info  - Compiled successfully
info  - Collecting page data
info  - Generating static pages (6/6)
info  - Finalizing page optimization

Route (pages)                              Size     First Load JS
┌ ● /                                      2.34 kB        75.5 kB
├   /_app                                  0 B            73.2 kB
├ ○ /404                                   181 B          73.4 kB
├ λ /api/hello                             0 B            73.2 kB
└ ● /posts/[id] (1576 ms)                  399 B          73.6 kB
    ├ /posts/28l3ltjqm (666 ms)
    ├ /posts/g_gpm2rryitr (663 ms)
    └ /posts/ufcpmp8hc
+ First Load JS shared by all              73.4 kB
  ├ chunks/framework-8c5acb0054140387.js   45.4 kB
  ├ chunks/main-f2e125da23ccdc4a.js        26.7 kB
  ├ chunks/pages/_app-3893aca8cac41098.js  296 B
  ├ chunks/webpack-8fa1640cc84ba8fe.js     750 B
  └ css/ab44ce7add5c3d11.css               247 B

λ  (Server)  server-side renders at runtime (uses getInitialProps or getServerSideProps)
○  (Static)  automatically rendered as static HTML (uses no initial props)
●  (SSG)     automatically generated as static HTML + JSON (uses getStaticProps)

✨  Done in 7.34s.

上記ログを見ると、//posts/[id] が SSG になっていることが確認できます。(● マーク)

実際のビルドされたファイルは .next/ フォルダに出力されており、 yarn start を実行すると production mode で開発環境が実行されます。

本番の様に事前ビルドした静的ファイルを使用して確認する場合は yarn build してから yarn start して確認しましょう。

Vercel にデプロイ

API からコンテンツを取得することが確認できたので、静的ページのビルドを行い Vercel にデプロイします。

Vercel へのデプロイは Github との連携で簡単にできるので、 Github にリポジトリを作成して push しておきます。

Vercel に Github アカウントでログインして「Add new project」を選択し、 Github のリポジトリと連携をしてください。

スクリーンショット

Github に Vercel がない場合はインストールし、その後 Github 上で Repository access の設定をする必要があります。

連携ができたら import を選択するとプロジェクトの情報が表示されます。

スクリーンショット

「Build and Output Settings」は何もせず、「Environment Variables」に環境変数を設定していきます。

現在は microcms-sdk の createClient で使用している MICROCMS*SERVICE*DOMAINMICROCMS_API_KEY の設定が必要なので追加しましょう。

スクリーンショット

現状の設定はこれだけなのでこのままデプロイを実行します。

デプロイが完了すると下記の画面が表示されるので、左の画面プレビューをクリックして実際にホスティングされていることを確認しましょう。

スクリーンショット

vercel に割り振られたドメインでプロジェクトを確認できます。

Vercel と microCMS の連携

このままだと microCMS のコンテンツを更新してもホスティングされたファイルには反映されないので、記事更新のタイミングでもビルド・デプロイされるように Webhook を使用して Vercel と microCMS の連携をします。

Vercel の Dashboard から Settings タブを選択し、Git > Deploy Hooks で hook を作成します。

hook の名前は何でもいいですが、「microCMS」とし、対象ブランチを「main」としておきます。

スクリーンショット

「Create Hook」で作成し、表示された URL をコピーしておきます。

次に microCMS の管理画面で、「投稿」の API 設定を開きます。

Webhook のメニューを開き、追加を選択するとサービスの選択肢が表示されるので Vercel を選択します。

Webhook の識別名と先ほどコピーした Webhook の URL を入力し設定を完了します。

トリガーの細かい設定は好みで構いません。

スクリーンショット

設定が完了したあとは記事を更新するとビルド・デプロイが自動で行われます。(Vercel の Deployments でログが確認できる)

※ Webhook の設定はコンテンツごとの API 設定で行うため、別のコンテンツを追加し「更新したらビルド・デプロイ」と同じ様に行いたい場合は、別で Webhook の設定が必要です。

ドメインの設定

ドメインは Vercel によって作られたものが設定されているのでこのままでも問題ないですが、独自ドメインを設定したい場合は Vercel の管理画面から設定をします。

Dashboard の Settings タブから Domains を選択し、取得済みのドメインを追加します。

スクリーンショット

追加方法は何でも良いのですが、一番上が Recommended されているのでこちらで追加します。

スクリーンショット

A レコードや CNAME が作成されるので、取得したドメインのサーバーでレコードの設定をしてください。

スクリーンショット

しばらく時間がかかりますが、設定が完了すると Invalid から Valid にステータスが変わり、設定したドメインでのアクセスが可能となります。

おわりに

Next.js での SSR や API 、Dynamic Routes などの機能を使用したことが無かったので、勉強がてらサイトを作ってみました。

今後も色々アップデートできていければと思います。

明日は CSS in JS と Stitches について書いてくれる polidog さんの記事です。

https://qiita.com/polidog/items/63eb8bce10fd42b30f79

Discussion