😎

ヘッドレスCMS Sanityを紹介する

2024/12/02に公開

※この記事は「COUNTERWORKS Advent Calendar」の2日目の記事です。

はじめに

はじめまして、株式会社COUNTERWORKSでフロントエンドエンジニアをしている小木曽です。
今回はヘッドレスCMSのSanityを紹介したいと思います。
ヘッドレスCMSとはWordPressのようなモノシリックなサービスとは異なり、バックエンドの機能(データベース、管理画面、コンテンツ配信)に特化したCMSです。
SanityではSanity Studioというコンテンツを作成、編集、管理するためのインターフェースを提供しております。
また、コンテンツ内容の定義はJavaScript(TypeScript)で実装することができるためデベロッパーには扱いやすいCMSとなっております。
本記事では、Sanityでユーザー情報を管理できるようにして、Next.jsでコンテンツを表示するシンプルなアプリケーションを作成したいと思います。
※ヘッドレスCMSは他にもいくつかあります(有名どころでいうとcontentfulmicroCMS
※ヘッドレスCMSの詳しい解説はこちらの記事を読んでください。

Sanityの使い方

Sanity Studioのインストール

Sanity Studioとはコンテンツを管理するワークスペースです。
まずはSanity Studioをインストールしましょう。

$ pnpm create sanity@latest

プロジェクト名、データセット名、TypeScriptを利用するか、パッケージマネージャーなどを聞かれるので選択していきます。

? Select project to use Create new project
? Your project name: article-test 
Your content will be stored in a dataset that can be public or private, depending on
whether you want to query your content with or without authentication.
The default dataset configuration has a public dataset named "production".
? Use the default dataset configuration? Yes
✅ Creating dataset
? Project output path: /article-test
? Select project template Movie project (schema + sample data)
? Do you want to use TypeScript? Yes
? Add a sampling of sci-fi movies to your dataset on the hosted backend? Yes
✅ Bootstrapping files from template
✅ Resolving latest module versions
✅ Creating default project files
? Package manager to use for installing dependencies? pnpm

テンプレートはどれを選択してもいいですが、その後にサンプルデータをセットするか聞かれます。ここでは一旦Yesにします。
実際に利用するときはNoで問題ないと思います。Noにした場合はまっさらな状態から始めることができます。

? Add a sampling of sci-fi movies to your dataset on the hosted backend? (Y/n)

インストール完了したら、ローカルで開いてみましょう。
Sanityはデフォルトポートが3333です。
http://localhost:3333/structure

$ pnpm dev

Sanity Studioを開くと以下のような画面が表示されます。

Sanity Studioの説明

Sanity Studioの上部のタブはStructure、Vision、Schedulesにわかれます。

それぞれ簡単に説明したいと思います。

Structure

画面を開くと初期画面でStructureが表示されます。
ここではコンテンツの表示方法を定義します。
スキーマ(スキーマは後ほど説明します)で分かれていて、それぞれスキーマ定義に基づいて編集することができます。
サンプルデータではMovie、Person、Screeningのスキーマ定義がされております。

スキーマとは

スキーマとはコンテンツ管理するドキュメントの種類、フィールドを定義するところです。
フィールドのタイプ、説明、検証、初期値などの定義も同時にすることができます。

Vision

リアルタイムでデータベースに対するクエリを実行し、データの取得やフィルタリングを行うことができます。
SanityではGraphQLとGROQの2つのクエリ言語を使うことができます。

Schedules

予定公開をすることができます。
ただ、予定公開は有料になるので無料で使う場合は見ることないと思います。

スキーマを定義して入力画面を作成してみよう

スキーマを定義していきましょう。
スキーマ定義はschemaTypesフォルダで行います(デフォルトで作成されています)。

以下のように定義します。
sanityのdefineTypeでスキーマ、defineFieldでフィールドを定義します。
基本はプロパティのルールに従って入力していきます。

schemaTypes/user.ts
import { defineField, defineType } from "sanity";
import {MdPerson as icon} from 'react-icons/md'

export default defineType({
  name: "user", // スキーマキー
  title: "ユーザー", // スキーマのタイトル
  type: "document", // スキーマタイプ
  icon, // スキーマのアイコン
  fields: [ // このスキーマで管理する項目を定義します
    defineField({
      name: "name", // フィールドキー
      title: "名前", // フィールド名
      type: "string", // フィールドタイプ
      description: "ユーザーの名前", // フィールドの説明
      validation: (rule) => rule.required().error("名前は必須です"), // バリデーション
    })
  ]
})

ローカルで確認すると以下のようにユーザースキーマと、ユーザー名フィールドが作成されます。

フィールドタイプの詳しい情報はこちらをご覧ください。

ユーザースキーマをより詳しく以下のように定義してみました。

schemaTypes/user.ts
import { defineField, defineType } from "sanity";
import {MdPerson as icon} from 'react-icons/md'

export default defineType({
  name: "user",
  title: "ユーザー",
  type: "document",
  icon,
  fields: [
    defineField({
      name: "name",
      title: "名前",
      type: "string",
      validation: (rule) => [rule.required().error("名前は必須です"), rule.max(50).error("名前は50文字以内である必要があります")]
    }),
    defineField({
      name: "email",
      title: "メールアドレス",
      type: "string",
      description: "メールアドレスを入力してください",
      validation: (rule) => [rule.required().error("メールアドレスは必須です"), rule.email().error("メールアドレスが不正です")]  
    }),
    defineField({
      name: "introduction",
      title: "自己紹介",
      type: "text",
      rows: 5,
      description: "自己紹介を入力してください",
      validation: (rule) => [rule.max(1000).error("自己紹介は1000文字以内である必要があります")]  
    }),
    defineField({
      name: "user_image",
      title: "アイコン",
      type: "image",
      description: "アイコンを選択してください",
      options: {
        accept: "image/png, image/jpeg, image/webp"
      }
    })
  ],
  preview: {
    select: {
      title: "name",
      subtitle: "introduction",
      media: "user_image",
    }
  }
})

previewはリストの表示をカスタマイズすることができます

公開してみよう

右下のPublishボタンで公開することができます。

ちなみにバリデーション制限があるときは非活性になります。

GROQを使って作成した情報を取得してみよう

先ほど説明した通り、Visionでクエリを実行することができます。
GROQの書き方はこちらがわかりやすいです。

*[] // 全てのリソース
*[_type == "user"] // ユーザースキーマでフィルタ
*[name == "鈴木"] // フィールドキーでフィルタ

右側に取得結果が表示されます。

_idは作成すると自動で付与される一意の値です。
_revはドキュメントが更新されるたびに変更されるユニークな識別子で、ドキュメントのバージョン管理や変更履歴の追跡に役立ちます。

Sanity Studioにデプロイしてみよう

デプロイは以下のコマンドで簡単にすることができます。

$ pnpm run deploy

Next.jsでコンテンツを表示してみよう

Sanityでスキーマを定義し、APIでリソースを取得できるようになったのでアプリケーションで表示してみましょう。
今回はNext.js(Reactのフレームワーク)を利用して表示したいと思います。
SanityではNext.js用のライブラリが用意されているのでそちらを使いたいと思います。
(Next.jsのインストールなどは割愛させていただきます)

ライブラリをインストール

Next.jsに以下のライブラリを追加しましょう。

$ pnpm add next-sanity @sanity/image-url

next-sanity
SanityのNext.js向けのライブラリ
@sanity/image-url
Sanityの画像レコードからURLを生成するライブラリ

Sanityとの連携

SanityにはプロジェクトIdという一意のキーがあります。
Sanityの右のユーザーアイコンをクリックするとManagement ProjectというタブがあるのでそこからプロジェクトIdをコピーしてください。

Next.jsでsrc直下にsanityフォルダとclient.tsファイルを作成しましょう。
先ほどインストールしたnext-sanityでSanityを定義することができます。
また、通常のNext.jsと同様にキャッシュ管理することができます。

sanity/client.ts
import "server-only";

import { createClient, type QueryParams } from "next-sanity";

export const client = createClient({
  projectId: {projectId}, // Sanityで管理されているプロジェクトId
  dataset: {dataset}, // Sanityのデータベース名(もし環境が複数ある場合は指定したい環境のデータセット名をセットします。デフォルトは`production`です)
  apiVersion: {apiVersion}, // APIのバージョン
  token: {token} // APIトークン(Sanityの管理画面で生成することができます)
});

export async function sanityFetch<QueryResponse>({
  query,
  params = {},
  tags,
}: {
  query: string;
  params?: QueryParams;
  tags?: string[];
}) {
  return client.fetch<QueryResponse>(query, params, {
    next: {
      tags,
    },
  });
}

コンテンツ情報を取得して表示する

sanityFetchでSanityのクエリを実行してコンテンツ情報を取得することができます。

app/page.tsx
import { SanityDocument } from "next-sanity";
import { sanityFetch } from "@/sanity/client";
import Image from "next/image";
import { SanityImage } from "@/_components/common";

const USERS_QUERY = `*[_type == "user"]`;

export default async function IndexPage() {
  const users = await sanityFetch<SanityDocument[]>({ query: USERS_QUERY, tags: ["users"] });

  return (
    <main className="flex bg-slate-200 min-h-screen flex-col p-24 gap-12">
      <h1 className="text-4xl font-bold tracking-tighter">ユーザー一覧</h1>
      <ul className="grid grid-cols-1 gap-12 lg:grid-cols-2">
        {users.map((user) => (
          <li className="bg-white p-4 rounded-sm flex items-center gap-4" key={user._id}>
            <div>
              {user.user_image ? <SanityImage image={user.user_image} className="rounded-full" sanityWidth={80} sanityHeight={80} width={80} height={80} alt={user.name}/>:
              <Image src="/no-image-user-icon.png" alt="no image user" width={80} height={80} className="rounded-full"/>}
            </div>
            <div>   
              <p>{user.name}</p>
              <p>{user.email}</p>
              <p>{user.introduction}</p>
            </div>
          </li>
        ))}
      </ul>
    </main>
  );
}

画像の表示は@sanity/image-urlを使って画像URLを取得します。

utils/sanityImageUrl.ts
import type { SanityImageSource } from "@sanity/image-url/lib/types/types";
import Image, { type ImageProps } from "next/image";
import type { FC } from "react";
import imageUrlBuilder from "@sanity/image-url";

export const SanityImage: FC<
	Omit<ImageProps, "src"> & {
		image: SanityImageSource;
		sanityWidth?: number;
		sanityHeight?: number;
	}
> = ({ image, sanityWidth = 300, sanityHeight = 200, ...props }) => {
	const imageUrl =imageUrlBuilder({
		projectId: {projectId},
		dataset: {dataset},
	}).image(image).width(sanityWidth).height(sanityHeight).url();

	return <Image {...props} src={imageUrl} />;
};

表示されました!

おわりに

シンプルな使い方ではありますが、Sanityについてご紹介しました。
Next.jsで扱いやすいのも良いな思っております。

最後に、株式会社カウンターワークスでは、共に運用しやすいアプリケーション作りを考えながら事業を前進させるメンバーを募集しています!
興味のある方はぜひ以下のリンクからご応募ください!

COUNTERWORKS テックブログ

Discussion