mooriii's blog

article icon

Next.jsとContentfulで飼い猫のLGTM画像サイトを作成する

少し前にCloudflare PagesとHonoで愛猫「らて」のLGTM画像サイトを作成したのですが、画像のメンテナンスコストが高すぎたのでCMSへ移行しました。

ヘッドレスCMSを試したことがなかったのと、Nextのアプリをちゃんと作ったことがなかったので、単純に試したかった気持ちもあります。

Cloudflare PagesとHonoで愛猫のLGTM画像集めたサイトを作った🐈|mooriii354字 · 3枚の画像
favicon of https://sizu.me/mooriii/posts/fvx05tis26bssizu.me
ogp of https://static.sizu.me/api/og-image/503a7f21cdd4?avatarUrl=https%3A%2F%2Fr2.sizu.me%2Fusers%2F5516%2Favatar.jpeg%3Fv%3D1700140354618&theme=user&username=mooriii

このブログがCloudflareなので、今回はホスティング先をVercelにしました。

構成は以下のような感じで、Contentfulへの画像アップロードをトリガーにして、VercelでのSSGビルドを走らせるようにしています。

デプロイフロー図

今回作成したサイトはこちらです。

Lgtlatte - らてのLGTM画像飼い猫らてのLGTM画像を集めました
favicon of https://images.ctfassets.net/rd8mwctho8md/CvOcilcY3CGMuYLJPqxGL/4866b02e655a7d52c1fb19afcf9eb4c1/ogp.pnglgtlatte.vercel.app
ogp of

Nextアプリケーションの作成とVercelの連携

Nextアプリケーションの作成

まずはNextのアプリケーションを作成します。

pnpm create next-app lgtlatte
// 以下色々設定を聞かれるので好みで決める

これでアプリケーションが作られたら、GitHub上でリポジトリを作成してpushします。

cd lgtlatte
echo "# lgtlatte" >> README.md
git init
git add README.md
git commit -m "first commit"
git branch -M main
git remote add origin [email protected]:mr04vv/lgtlatte.git
git push -u origin main

Vercelでリポジトリを連携

続いてVercelにログインして、リポジトリをImportします。

アカウントが連携されていれば、Importできるリポジトリの候補が出てきますのでImportをクリックします。

Vercelのリポジトリimport画面

Importが成功すると以下のような画面が開くので、Deployをクリックします

Vervelのデプロイ画面

デプロイが完了してこんな感じの画面が出ればOKです。

デプロイ成功

Contentfulの設定

今回はヘッドレスCMSとしてContentfulを使用します。

Contentfulの全体像はこちらの記事が参考になります。

[初心者向け]Contentfulの全体像・記事投稿の流れ・便利な機能をまとめてみた | DevelopersIO
favicon of https://dev.classmethod.jp/articles/contentful/dev.classmethod.jp
ogp of https://devio2023-media.developers.io/wp-content/uploads/2022/12/Untitled6-3.png

アカウントを作成するとBlankという名前のSpaceが作られているのでそれを使います。

Space Nameは画面右上の[Settings]→[General settings]で変更できます。

Mediaの追加

今回は画像を一覧で返してほしいだけなのでContent modelの定義はせずにMediaの追加のみ行います。

ヘッダーのMediaタブからアセットを追加します。

メディアの追加画面

Tokenの取得

Tokenは画面右上の[Settings]→[API Keys]から生成できます。

メディアの追加画面

Tokenが生成されたら以下のような画面が表示されるので、APIKeyをコピーしておきましょう。

トークン生成画面

Brunoで実際に叩いてみると以下のようなレスポンスが返ってきます。

レスポンス例

NextからContentfulのAPIを叩く

queryを定義する

Assetを一覧で取得するためのSchemaを定義します。

src/queries/assets.graphql
query AssetCollection($limit: Int, $skip: Int) {
  assetCollection(limit: $limit, skip: $skip) {
    items {
      title
      url
    }
  }
}

GraphQL-CodegenでContentfulのSchemaから型を自動生成する

上で定義したSchemaとContentfulのSchemaを利用して形を自動生成します。

pnpm add -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typescript-react-apollo
npx graphql-code-generator init

プロジェクトルートに codegen.ts が作られるので、以下に書き換えます。

codegen.ts
import type { CodegenConfig } from "@graphql-codegen/cli";
 
const config: CodegenConfig = {
  overwrite: true,
  schema: {
    [`https://graphql.contentful.com/content/v1/spaces/${process.env.CONTENTFUL_SPACE_ID}`]:
      {
        headers: {
          Authorization: `Bearer ${process.env.CONTENTFUL_ACCESS_TOKEN}`,
        },
      },
  },
  documents: ["src/queries/*.graphql"],
  generates: {
    "src/generated/schema.tsx": {
      plugins: [
        "typescript",
        "typescript-operations",
        "typescript-react-apollo",
      ],
    },
  },
};
 
export default config;

npx graphql-code-generator init を実行した時に、 package.jsoncodegen のスクリプトが追加されるので以下のように変更してください。

(実行時に.envからContentfulのSpaceIdとTokenを取得するようにしています。)

package.json
{
    // 略
  "scripts": {
    "dev": "next dev",
    "build": "next build ",
    "start": "next start",
    "lint": "next lint",
    "codegen": "graphql-codegen --require dotenv/config --config codegen.ts"
  },
}

実行するとsrc/generated配下に型定義ファイルが自動生成されます。

pnpm run codegen

ApolloClientの追加

pnpm add @apollo/client

パッケージが追加できたら以下のファイルを追加します。

src/lib/apolloClient.ts
import { ApolloClient, InMemoryCache, HttpLink } from "@apollo/client";
 
const TOKEN = process.env.CONTENTFUL_ACCESS_TOKEN;
const SPACE = process.env.CONTENTFUL_SPACE_ID;
const URL = `https://graphql.contentful.com/content/v1/spaces/${SPACE}`;
 
const link = new HttpLink({
  uri: URL,
  headers: {
    Authorization: `Bearer ${TOKEN}`,
  },
  fetchOptions: {
    cache: "force-cache",
  },
});
 
const cache = new InMemoryCache();
 
export const apolloClient = new ApolloClient({
  link,
  cache,
});
 

page.tsxを修正

定義したapolloClientを用いて画像を取得して表示するために、page.tsxを以下のように編集します。

import { apolloClient } from "@/lib/apolloClient";
import {
  AssetCollectionDocument,
  Query,
  QueryAssetCollectionArgs,
} from "@/generated/schema";
 
export default async function Home() {
  const res = await apolloClient.query<Query, QueryAssetCollectionArgs>({
    query: AssetCollectionDocument,
    variables: {
      skip: 0,
      limit: 20,
    },
  });
 
  const items = res.data.assetCollection?.items ?? [];
 
  return (
    <main>
      <div>
        {items.map((item) => (
          <div
            style={{
              display: "block",
              position: "relative",
              width: "500px",
              height: "300px",
            }}
          >
            <img src={item?.url ?? ""} />
            // この時点ではnext/imageがSSGだと使えないので一旦imgタグで代用
          </div>
        ))}
      </div>
    </main>
  );
}

ここまで来たら一度pushしてみます。 Vercelの環境変数を設定しないとビルドが失敗するので、.envに定義している変数を忘れずに追加しましょう。

ContentfulにAssetが追加されたらビルドされるようにする

ContentfulはEntryやAssetなどの追加や変更時にトリガーをセットして、Webhookを発火させることができます。

それを利用して、今回はAssetの追加・更新時にVercelのWebhookを発火させるようにします。

VercelでWebhookのURLを生成する

Vercelのプロジェクトの設定画面からWebhookのURLを生成し、コピーしておきます。

VercelのWebhookURL生成画面

ContentfulでWebhookの設定を追加する

画面右上の[Setting]→[Webhooks]から設定を追加します。

ContentfulのWebhook設定画面

この状態でMediaを追加してPublishするとVercel上でビルドが走るようになります。

今回はVercel上でビルドを走らせましたが、Github ActionsでCDを回したい場合もContentful側の変更をトリガーにしてWebhook経由で実行できるみたいです。

Running static site builds with GitHub Actions and ContentfulLearn how to combine Github Actions with Contentful’s webhook features so you can trigger static site builds every time your team publishes content.
favicon of https://www.contentful.com/blog/running-static-site-builds-with-github-actions-and-contentful/www.contentful.com
ogp of https://images.ctfassets.net/fo9twyrwpveg/51ZQgwKZnCJGMNCkXrysXY/8d1c1fc203fe6d3cf185724294cfc0bd/0kLlXY1w.png

今回作成したサイトとGitHubのリポジトリはこちらです。

Lgtlatte - らてのLGTM画像飼い猫らてのLGTM画像を集めました
favicon of https://images.ctfassets.net/rd8mwctho8md/CvOcilcY3CGMuYLJPqxGL/4866b02e655a7d52c1fb19afcf9eb4c1/ogp.pnglgtlatte.vercel.app
ogp of
GitHub - mr04vv/lgtlatteContribute to mr04vv/lgtlatte development by creating an account on GitHub.
favicon of https://github.com/mr04vv/lgtlattegithub.com
ogp of https://opengraph.githubassets.com/f374471d31dcaf0d67d1d4019682c8a21d3656437d8af79d7e274fc3e86a2cba/mr04vv/lgtlatte

おわりに

ヘッドレスCMS初挑戦だったんですが、かなり便利でした。 特にWebhook経由でコンテンツの更新時にSSGのビルドを自動で走らせることができるので、更新の手間が省けてとても楽になりました。

NextはgenerateMetadataがとても便利で、ContentfulからOgpの情報を取得してmetadataを生成するようできました。後からOgpを変更したくなってもコードをいじらなくて済むのは体験良きです。

GraphQLもかなり久しぶりに触ったので知識があやふやでした。また時間をとって復習します。

フロントエンダーのはずなのにフロントエンドの知識が浅くて恥ずかしくなってくるので、色々試しつつ基礎もしっかり勉強していかないとな〜となっているところです。

ではまた次回お会いしましょう。

ぬいぐるみと並んでいるらてさん

この記事をシェアするx icon
アイコン画像
Takuto Mori@_mooriii

Wevoxというサービスのフロントエンジニアをしています。趣味は猫を眺めることです🐱