🦎

デベロッパー向けヘッドレスCMS、Sanityを始めよう

2024/06/05に公開

はじめに

SanityというヘッドレスCMSを紹介します。以下ヘッドレスCMSのことをCMSと表記します。(そもそもコンテンツ管理システムにコンテンツ表示側のシステムが乗っかってるほうがおかしいんだからわざわざヘッドレスCMSって言うのもそろそろやめね?という気持ちがある。ちなみにWordPressみたいなヘッド有(?)CMSのことモノリシックCMSっていうらしい。WordPressのくせにかっこよくて生意気!!)
https://www.sanity.io/
日本国内ではmicroCMSなどが有名で、ここZennではSanityに関する記事は現在1つしかないという現状を嘆いてこの記事を贈ります。

なぜ推すのか

前提として僕は大昔にContetfulというCMSを1度だけ使ったことがあるだけで、その他CMSはWordPressしか使ったことがありません。なので他CMSとの比較を実際に使用した人目線で書くことはできないことをご了承ください。

タイトルにもある通りSanityは非常にデベロッパー向けな印象です。実際にコンテンツを編集する画面のことをSanity Studio(以下Studio)というのですが、このStudioを立ち上げた時点では画面は全くの空っぽの状態で、そこからCMSの要件に従って必要な機能や入力項目を必要なだけ追加していくことができ、完成したものは何一つ不要なものがないという状態にできます。Studioのカスタマイズもすべてコードで行うのでGit管理が可能で再利用性も高く、Sanityに慣れておりCMSの要件も決まっていればCMS実装は1時間ほどで終わってしまいます(もちろん要件によります)。料金プランも非常に良心的で、Studioのデプロイ環境もSanity自体が用意してくれておりsanity deployというコマンドを実行すればサーバー上でStudioを操作可能です。

Jamstack Community Surveyによる調査でCMSの利用者数と利用満足度を示したグラフによるとSanityの利用満足度が頭1つ抜けて1位というスコアになっていますね。
https://jamstack.org/survey/2022/#content-management-systems

料金プラン

現在SanityにはFree, Growth, Enterpriseの3プランが存在しています。詳しくはこちら。
https://www.sanity.io/pricing
Freeプランの気前の良さが魅力的ですね。個人のプロジェクトやクライアントが小規模の場合はFreeプランで全然間に合ってしまう。唯一Freeプランで厳しいとすればFreeプランでは権限タイプが、全ての権限を持つ「Administrator」と見るしかできない「Viewer」の2つしかないというところでしょうか。「Editor」権限が使いたければGrowthプランを使いましょう。Freeプランの次が月15ドル(記事執筆時で2,342円)なのも他CMSと比べて良心的ですね。

Sanity Studioのインストール/初期化

https://www.sanity.io/docs/installation
Sanity.ioでアカウントを作成したら、コマンドラインからプロジェクトを立ち上げます。Webページから対話式でプロジェクトを立ち上げることもできるようですが使ったことがないので説明しません。

次のコマンドをどこかのディレクトリで実行

npm create sanity@latest

プロジェクト名やTypeScriptを使うか?などの質問項目に答えていく。
途中テンプレートを選ぶことができるので、まっさらな状態で始める場合はClean project...を選択。ここでは解説用にMovie projectを使用します。

? Select project template (Use arrow keys)
❯ Movie project (schema + sample data) 
  E-commerce (Shopify) 
  Blog (schema) 
  Clean project with no predefined schema types 

node_modulesのインストールが終われば準備完了。できあがったファイルやフォルダについては下記ドキュメントを一読しましょう。
https://www.sanity.io/docs/project-structure
npm run devでlocalhostが立ち上がる。

StudioはとてもシンプルなUIなので操作説明は割愛。

Schema概要

SchemaはStudioで使用するドキュメントや入力項目を定義したもので、Schemaを作成し組み合わせることで要件に沿ったCMSを実装していくことになります。SchemaはデフォルトではschemaTypesフォルダに作成することになっており、./schemaTypes(のindex.js)をsanity.config.jsでimportすることでStudioに追加できます。

sanity.config.js

import { defineConfig } from 'sanity'
import { structureTool } from 'sanity/structure'
import { visionTool } from '@sanity/vision'
import { schemaTypes } from './schemaTypes'

export default defineConfig({
  name: 'default',
  title: 'Project Title',

  projectId: 'your-projectId-here',
  dataset: 'production',

  plugins: [
    structureTool(),
    visionTool(),
  ],

  schema: {
    types: schemaTypes,
  },
})

./shemaTypes/index.js

import blockContent from './blockContent'
import crewMember from './crewMember'
import castMember from './castMember'
import movie from './movie'
import person from './person'
import screening from './screening'
import plotSummary from './plotSummary'
import plotSummaries from './plotSummaries'

export const schemaTypes = [
  // Document types
  // Stuido上では「Content」タブに表示される。
  movie,
  person,
  screening,

  // Other types
  // 上記Document Type内部で参照され使用されるType
  blockContent,
  plotSummary,
  plotSummaries,
  castMember,
  crewMember,
]

Schemaの作り方

movie.jsを見ながらSchemaの作り方を解説していきます。

まずdocumentというSchema Typeを作ることから始まります。Sanityにおける他の全てのSchema Typeはdocumentの中で使用されます。movie.jsは映画の情報(概要、ポスター画像、キャストやクルー)をまとめたdocumentになっています。

movie.js

import { defineField, defineType } from 'sanity'
import { MdLocalMovies as icon } from 'react-icons/md'

export default defineType({
  name: 'movie',
  title: 'Movie',
  type: 'document',
  icon,
  fields: [
    // このdocumentで使用する入力項目を記述
    defineField({
      name: 'title',
      title: 'Title',
      type: 'string',
    }),
    // 省略
  ],
})

SchemaはdefineTypeでそのSchemaのTypeを指定し、fields内にdefineFieldで入力項目を作成していくという仕組みになっています。nameはそのShema Typeを識別するためのidで、titleはStudio上で表示される名称、typeはSchema Typeを表します。movie.jsではdocumentを作りたいのでtypeにはdocumentを指定。iconを設定すればContentタブで表示されるtitleの横にアイコンが付きます。

使用できるSchema Typeの一覧はこちら、数値/文字列/テキストエリア/画像などの入力項目が揃っています。ObjectTypeを使用すればSchema Typeを自由に組み合わせて入力項目をカスタムすることもできます。
https://www.sanity.io/docs/schema-types

StudioのテンプレートであるMovie Projectを隅々まで見ればStudioの基本的な使い方は理解できるので、本格的にクライアントワークで使用する前に見ておくことをおすすめします。

Block Content

基本的なSchema Typeについてはドキュメントを読んでもらうとして、BlockというSchema Typeについてさくっと解説しておきます。Block Schema Typeはいわゆるwysiwygエディタを定義するSchema Typeです。
Movie Project Templateでは./schemaTypes/blockContent.jsの中に定義されており、movie.js内のoverviewという映画の概要説明文に使われています。

./schemaTypes/blockContent.js

import { defineArrayMember, defineType } from 'sanity'

export default defineType({
  title: 'Block Content',
  name: 'blockContent',
  type: 'array',
  of: [
    defineArrayMember({
      title: 'Block',
      type: 'block',
      // エディタ内で使用するhtml要素を定義する
      styles: [
        { title: 'Normal', value: 'normal' },
        { title: 'H1', value: 'h1' },
        { title: 'H2', value: 'h2' },
        { title: 'H3', value: 'h3' },
        { title: 'H4', value: 'h4' },
        { title: 'Quote', value: 'blockquote' },
      ],
      lists: [{ title: 'Bullet', value: 'bullet' }],
      marks: {
        decorators: [
          { title: 'Strong', value: 'strong' },
          { title: 'Emphasis', value: 'em' },
        ],
        annotations: [
          {
            title: 'URL',
            name: 'link',
            type: 'object',
            fields: [
              {
                title: 'URL',
                name: 'href',
                type: 'url',
              },
            ],
          },
        ],
      },
    }),
    defineArrayMember({
      type: 'image',
      options: { hotspot: true },
    }),
  ],
})

./schemaTypes/movie.js

defineField({
  name: 'overview',
  title: 'Overview',
  type: 'blockContent',
})

Block Schema Typeも中身を細かくカスタマイズすることができ、段落と画像のみ、段落とアンカー(aタグ)のみ使用可能にするということができます。これで「ドラえも〜ん、クライアントが想定と違うエディタの使い方してデザインが崩れちゃうよ〜」ということもある程度防げますね。

シングルページを作る

Studioで作成したdocumentはデフォルトでは複数のページを作成するためのものです。作成したdocument typeはあくまでもdocumentの型であり、そこからインスタンスとしてのdocumentを複数作れるわけですね。

例えばWebサイトで言うところのCompany、About、Contactのような1つしか存在しないページ(WordPressでいうところの固定ページ)を作るにはStudioのStructure Builderという機能を使います。具体的なコードの解説は省きますが、sanity.config.js内で以下のように書くことでシングルページを作ることができます。この例では、aboutPagecontactPageというdocumentがあり、その2つをシングルページとして扱っています。最後の

...S.documentTypeListItems().filter(listItem => !['aboutPage', 'contactPage'].includes(listItem.getId()))

のコードは、aboutPageとcontactPage以外のすべてのdocument typeをシングルページとしてではなく通常の扱いでContentタブに表示するためのコードです。

import { defineConfig } from 'sanity'
import { structureTool } from 'sanity/structure'
import { visionTool } from '@sanity/vision'
import { schemaTypes } from './schemaTypes'

export default defineConfig({
  // 省略
  plugins: [
    structureTool({
      structure: (S) =>
        S.list()
          .title('Content')
          .items([
            S.listItem()
              .title('About')
              .child(
                S.document()
                  .schemaType('aboutPage')
                  .documentId('aboutPage')
              ),
            S.listItem()
              .title('Contact')
              .child(
                S.document()
                  .schemaType('contactPage')
                  .documentId('contactPage')
              ),
            ...S.documentTypeListItems().filter(listItem => !['aboutPage', 'contactPage'].includes(listItem.getId()))
          ])
    }),
    visionTool()
  ],

  schema: {
    types: schemaTypes,
  },
})

Studio上では、ContentタブにあるAboutを押すと1つのdocumentが開くようになっています。

Structure Builderについての詳しい情報は下記公式ドキュメントにあります。
https://www.sanity.io/docs/structure-builder-introduction
https://www.sanity.io/docs/structure-builder-reference

シングルページの作り方は公式ドキュメント内にもあるので合わせて読んでおいてください。
※2023年1月に書かれた記事で、古いStructure Builder APIを使っています。
https://www.sanity.io/guides/singleton-document

Custom Document Actionsで「Deploy」ボタンを作る

Sanityはとてもカスタマイズ性が高く、コードさえ書ければ思いつくことはなんでもできそうな懐の深さがあります。僕もSanityのカスタマイズ機能の全貌はつかめていませんが、1つ重宝しているカスタマイズ機能があるので紹介します。

Studioでなにかドキュメントを開いたときに右下に出るページの公開や削除を行うボタン類をDocument actionsというのですが、ここにボタンを追加することができます。

僕がいつも作るボタンは「Deploy」ボタンで、ボタンを押すとGitHub Actionsにイベントを送信するようになっています。GitHub Actions側ではイベントを受け取り、更新されたSanity上のデータを元にWebサイトの再ビルドを行いサーバーにアップロードする処理をやらせています。

./actions/DeployAction.js

import axios from 'axios'

export function DeployAction(props) {
  return {
    label: 'Deploy',
    onHandle: () => {
      axios({
        method: 'post',
        url: 'https://api.github.com/repos/{owner}/{repository}/dispatches',
        headers: {
          'Accept': 'application/vnd.github.v3+json',
          'Authorization': 'token {Your Access Token Here}',
        },
        data: {
          event_type: 'update-contents',
        }
      })

      window.alert('Webサイトへの反映プロセスを開始しました。数分後に完了します。')
    }
  }
}

sanity.config.js

document: {
  actions: [DeployAction],
},

僕はサーバーのコストを抑えつつWebサイトの表示速度を向上させるためServer Side Renderingは使用せず、全ページ静的HTML化してサーバーに配信する手法を取っています。クライアントにはCMSを編集したあと、最後に1回だけ「Webサイトを更新する」ボタンを押してほしいので、Deployボタンを作れるCustom Document Actionsが大好きです。

Sanity Studioのデプロイ

冒頭でもちらっと言いましたが、Sanityは現在どの料金プランでもSanityが用意してくれているSanity Studioのデプロイ環境を利用することができ、sanity deploy(npm run deploy)コマンドを実行してURLのサブドメイン部分を決めるだけでデプロイ完了です。

フロントエンドでの利用

実際にWebフロントエンドでデータを取得するには@sanity/clientを使います。
詳しくは公式ドキュメント読んでください。
https://www.sanity.io/docs/js-client

フレームワークとの連携に関しても公式ドキュメントに各フレームワークでの使い方を書いてくれています。
https://www.sanity.io/exchange/frameworks

上記はSanity側のドキュメントですが、フレームワーク側のドキュメントにもSanityとの連携についての記事が載っている場合があるので両方確かめてみることをおすすめします。

画像配信

Studio上に追加した画像や動画、その他ファイル類はAssetと呼ばれ、SanityのAssetはCDNを通じて配信されます。SanityのCDNは内部でGoogle Cloud CDNを利用しているようです。
https://www.sanity.io/docs/asset-cdn

モダンなCMSには画像URLにパラメーターをつけて画像をゴニョゴニョする機能がついていますが、Sanityにももちろんあります。利用できるURLパラメーターとその機能については下記ドキュメントを参照してください。
https://www.sanity.io/docs/image-urls

必ず使っているのは?auto=formatですね。これでWebP画像が使える環境では画像をWebPに変換して配信してくれます。これを書いてるときに思いましたがもうWebP使えない環境無視して良さそうなので、?fm=webpで強制WebP配信にしてもよさそうです。

Block Contentのhtml化

Block Contentはhtml変換しないとWebサイト側では使えません。
これに関しても公式ドキュメント側でhtmlへの変換方法、各フレームワーク側での使い方などを説明してくれています。
https://www.sanity.io/docs/presenting-block-text

おわりに

以上かなり簡単にですがヘッドレスCMSのSanityについて解説させて頂きました。国内利用者が増えてくれると嬉しいです。他CMSと比べてどこが良い/悪いなどのコメントもお待ちしてます!

Discussion